this repo has no description
1import * as fs from "node:fs/promises";
2import { existsSync } from "node:fs";
3import * as path from "node:path";
4import { command, positional, string } from "cmd-ts";
5import { intro, outro, text, spinner, log, note } from "@clack/prompts";
6import { fileURLToPath } from "node:url";
7import { dirname } from "node:path";
8import { findConfig, loadConfig } from "../lib/config";
9import type { PublisherConfig } from "../lib/types";
10
11const __filename = fileURLToPath(import.meta.url);
12const __dirname = dirname(__filename);
13const COMPONENTS_DIR = path.join(__dirname, "components");
14
15const DEFAULT_COMPONENTS_PATH = "src/components";
16
17const 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
29export 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});