A CLI for publishing standard.site documents to ATProto
at main 157 lines 4.8 kB view raw
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});