packages/cli/src/commands/add.ts 4.7 K raw
1
import * as fs from "node:fs/promises";
2
import { existsSync } from "node:fs";
3
import * as path from "node:path";
4
import { command, positional, string } from "cmd-ts";
5
import { intro, outro, text, spinner, log, note } from "@clack/prompts";
6
import { fileURLToPath } from "node:url";
7
import { dirname } from "node:path";
8
import { findConfig, loadConfig } from "../lib/config";
9
import type { PublisherConfig } from "../lib/types";
10
11
const __filename = fileURLToPath(import.meta.url);
12
const __dirname = dirname(__filename);
13
const COMPONENTS_DIR = path.join(__dirname, "components");
14
15
const DEFAULT_COMPONENTS_PATH = "src/components";
16
17
const AVAILABLE_COMPONENTS = ["sequoia-comments"];
18
19
export const addCommand = command({
20
	name: "add",
21
	description: "Add a UI component to your project",
22
	args: {
23
		componentName: positional({
24
			type: string,
25
			displayName: "component",
26
			description: "The name of the component to add",
27
		}),
28
	},
29
	handler: async ({ componentName }) => {
30
		intro("Add Sequoia Component");
31
32
		// Validate component name
33
		if (!AVAILABLE_COMPONENTS.includes(componentName)) {
34
			log.error(`Component '${componentName}' not found`);
35
			log.info("Available components:");
36
			for (const comp of AVAILABLE_COMPONENTS) {
37
				log.info(`  - ${comp}`);
38
			}
39
			process.exit(1);
40
		}
41
42
		// Try to load existing config
43
		const configPath = await findConfig();
44
		let config: PublisherConfig | null = null;
45
		let componentsDir = DEFAULT_COMPONENTS_PATH;
46
47
		if (configPath) {
48
			try {
49
				config = await loadConfig(configPath);
50
				if (config.ui?.components) {
51
					componentsDir = config.ui.components;
52
				}
53
			} catch {
54
				// Config exists but may be incomplete - that's ok for UI components
55
			}
56
		}
57
58
		// If no UI config, prompt for components directory
59
		if (!config?.ui?.components) {
60
			log.info("No UI configuration found in sequoia.json");
61
62
			const inputPath = await text({
63
				message: "Where would you like to install components?",
64
				placeholder: DEFAULT_COMPONENTS_PATH,
65
				defaultValue: DEFAULT_COMPONENTS_PATH,
66
			});
67
68
			if (inputPath === Symbol.for("cancel")) {
69
				outro("Cancelled");
70
				process.exit(0);
71
			}
72
73
			componentsDir = inputPath as string;
74
75
			// Update or create config with UI settings
76
			if (configPath) {
77
				const s = spinner();
78
				s.start("Updating sequoia.json...");
79
				try {
80
					const configContent = await fs.readFile(configPath, "utf-8");
81
					const existingConfig = JSON.parse(configContent);
82
					existingConfig.ui = { components: componentsDir };
83
					await fs.writeFile(
84
						configPath,
85
						JSON.stringify(existingConfig, null, 2),
86
						"utf-8",
87
					);
88
					s.stop("Updated sequoia.json with UI configuration");
89
				} catch (error) {
90
					s.stop("Failed to update sequoia.json");
91
					log.warn(`Could not update config: ${error}`);
92
				}
93
			} else {
94
				// Create minimal config just for UI
95
				const s = spinner();
96
				s.start("Creating sequoia.json...");
97
				const minimalConfig = {
98
					ui: { components: componentsDir },
99
				};
100
				await fs.writeFile(
101
					path.join(process.cwd(), "sequoia.json"),
102
					JSON.stringify(minimalConfig, null, 2),
103
					"utf-8",
104
				);
105
				s.stop("Created sequoia.json with UI configuration");
106
			}
107
		}
108
109
		// Resolve components directory
110
		const resolvedComponentsDir = path.isAbsolute(componentsDir)
111
			? componentsDir
112
			: path.join(process.cwd(), componentsDir);
113
114
		// Create components directory if it doesn't exist
115
		if (!existsSync(resolvedComponentsDir)) {
116
			const s = spinner();
117
			s.start(`Creating ${componentsDir} directory...`);
118
			await fs.mkdir(resolvedComponentsDir, { recursive: true });
119
			s.stop(`Created ${componentsDir}`);
120
		}
121
122
		// Copy the component
123
		const sourceFile = path.join(COMPONENTS_DIR, `${componentName}.js`);
124
		const destFile = path.join(resolvedComponentsDir, `${componentName}.js`);
125
126
		if (!existsSync(sourceFile)) {
127
			log.error(`Component source file not found: ${sourceFile}`);
128
			log.info("This may be a build issue. Try reinstalling sequoia-cli.");
129
			process.exit(1);
130
		}
131
132
		const s = spinner();
133
		s.start(`Installing ${componentName}...`);
134
135
		try {
136
			const componentCode = await fs.readFile(sourceFile, "utf-8");
137
			await fs.writeFile(destFile, componentCode, "utf-8");
138
			s.stop(`Installed ${componentName}`);
139
		} catch (error) {
140
			s.stop("Failed to install component");
141
			log.error(`Error: ${error}`);
142
			process.exit(1);
143
		}
144
145
		// Show usage instructions
146
		note(
147
			`Add to your HTML:\n\n` +
148
				`<script type="module" src="${componentsDir}/${componentName}.js"></script>\n` +
149
				`<${componentName}></${componentName}>\n\n` +
150
				`The component will automatically read the document URI from:\n` +
151
				`<link rel="site.standard.document" href="at://...">`,
152
			"Usage",
153
		);
154
155
		outro(`${componentName} added successfully!`);
156
	},
157
});