forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
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 = ["sequoia-comments"];
18
19export 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});