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