import * as fs from "node:fs/promises"; import { existsSync } from "node:fs"; import * as path from "node:path"; import { command, positional, string } from "cmd-ts"; import { intro, outro, text, spinner, log, note } from "@clack/prompts"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; import { findConfig, loadConfig } from "../lib/config"; import type { PublisherConfig } from "../lib/types"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const COMPONENTS_DIR = path.join(__dirname, "components"); const DEFAULT_COMPONENTS_PATH = "src/components"; const AVAILABLE_COMPONENTS: { name: string; notes?: string }[] = [ { name: "sequoia-comments", notes: `The component will automatically read the document URI from:\n` + ``, }, { name: "sequoia-subscribe", }, ]; export const addCommand = command({ name: "add", description: "Add a UI component to your project", args: { componentName: positional({ type: string, displayName: "component", description: "The name of the component to add", }), }, handler: async ({ componentName }) => { intro("Add Sequoia Component"); // Validate component name const component = AVAILABLE_COMPONENTS.find( (c) => c.name === componentName, ); if (!component) { log.error(`Component '${componentName}' not found`); log.info("Available components:"); for (const comp of AVAILABLE_COMPONENTS) { log.info(` - ${comp.name}`); } process.exit(1); } // Try to load existing config const configPath = await findConfig(); let config: PublisherConfig | null = null; let componentsDir = DEFAULT_COMPONENTS_PATH; if (configPath) { try { config = await loadConfig(configPath); if (config.ui?.components) { componentsDir = config.ui.components; } } catch { // Config exists but may be incomplete - that's ok for UI components } } // If no UI config, prompt for components directory if (!config?.ui?.components) { log.info("No UI configuration found in sequoia.json"); const inputPath = await text({ message: "Where would you like to install components?", placeholder: DEFAULT_COMPONENTS_PATH, defaultValue: DEFAULT_COMPONENTS_PATH, }); if (inputPath === Symbol.for("cancel")) { outro("Cancelled"); process.exit(0); } componentsDir = inputPath as string; // Update or create config with UI settings if (configPath) { const s = spinner(); s.start("Updating sequoia.json..."); try { const configContent = await fs.readFile(configPath, "utf-8"); const existingConfig = JSON.parse(configContent); existingConfig.ui = { components: componentsDir }; await fs.writeFile( configPath, JSON.stringify(existingConfig, null, 2), "utf-8", ); s.stop("Updated sequoia.json with UI configuration"); } catch (error) { s.stop("Failed to update sequoia.json"); log.warn(`Could not update config: ${error}`); } } else { // Create minimal config just for UI const s = spinner(); s.start("Creating sequoia.json..."); const minimalConfig = { ui: { components: componentsDir }, }; await fs.writeFile( path.join(process.cwd(), "sequoia.json"), JSON.stringify(minimalConfig, null, 2), "utf-8", ); s.stop("Created sequoia.json with UI configuration"); } } // Resolve components directory const resolvedComponentsDir = path.isAbsolute(componentsDir) ? componentsDir : path.join(process.cwd(), componentsDir); // Create components directory if it doesn't exist if (!existsSync(resolvedComponentsDir)) { const s = spinner(); s.start(`Creating ${componentsDir} directory...`); await fs.mkdir(resolvedComponentsDir, { recursive: true }); s.stop(`Created ${componentsDir}`); } // Copy the component const sourceFile = path.join(COMPONENTS_DIR, `${componentName}.js`); const destFile = path.join(resolvedComponentsDir, `${componentName}.js`); if (!existsSync(sourceFile)) { log.error(`Component source file not found: ${sourceFile}`); log.info("This may be a build issue. Try reinstalling sequoia-cli."); process.exit(1); } const s = spinner(); s.start(`Installing ${componentName}...`); try { const componentCode = await fs.readFile(sourceFile, "utf-8"); await fs.writeFile(destFile, componentCode, "utf-8"); s.stop(`Installed ${componentName}`); } catch (error) { s.stop("Failed to install component"); log.error(`Error: ${error}`); process.exit(1); } // Show usage instructions let notes = `Add to your HTML:\n\n` + `\n` + `<${componentName}>\n`; if (component.notes) { notes += `\n${component.notes}`; } note(notes, "Usage"); outro(`${componentName} added successfully!`); }, });