import { Command, CommanderError } from 'commander'; import { getCliName, getCliId, getCliVersion } from "./cliName.js"; import { red, cyan } from "./colors.js"; import { login } from "../commands/login.js"; import { pull } from "../commands/pull.js"; import { push } from "../commands/push.js"; import { serve } from "../commands/serve.js"; import { host } from "../commands/host.js"; import { render } from "../commands/render.js"; import { init } from "../commands/init.js"; import { sessions } from "../commands/sessions.js"; import { switchServer } from "../commands/switchServer.js"; import { removeServer } from "../commands/removeServer.js"; import { logout } from "../commands/logout.js"; import { whoami } from "../commands/whoami.js"; import { vscodeInit } from "../commands/vscodeInit.js"; import { settings } from "../commands/settings.js"; function createProgram() { const program = new Command(); program .name(getCliName()) .description('Open source html host and tool for syncing webpages to that host.') .version(getCliVersion()) .option('-s, --silent', 'Suppress all output') .option('--json', 'Output as JSON (for commands that support it)') .option('--log-level ', 'Set log level (error, warn, notice, http, info, verbose, silly)', 'info'); const LEVEL_OPTIONS = { silent: { index: 0 }, error: { index: 1 }, warn: { index: 2 }, notice: { index: 3 }, http: { index: 4 }, info: { index: 5 }, verbose: { index: 6 }, silly: { index: 7 }, }; // Set up proc-log listener before any command runs, using Commander-parsed options program.hook('preAction', () => { const opts = program.opts(); const logLevel = opts.silent ? 'silent' : (opts.logLevel ?? 'info'); const maxLevel = LEVEL_OPTIONS[logLevel]; if (!maxLevel || maxLevel.index <= 0) return; const cliName = getCliName(); process.on('log', (level, ...args) => { const levelOption = LEVEL_OPTIONS[level]; if (!levelOption || levelOption.index > maxLevel.index) return; const message = args.map(arg => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' '); if (level === 'error') { for (const line of message.split('\n')) { process.stderr.write(`${cyan(cliName)} ${red('error')} ${line}\n`); } } else if (level === 'warn') { process.stderr.write(message + '\n'); } else { process.stdout.write(message + '\n'); } }); }); // Create context getter that includes parsed global options const getCtx = () => { const opts = program.opts(); return { cliName: getCliName(), cliId: getCliId(), cwd: process.cwd(), silent: opts.silent, json: opts.json, }; }; program .command('whoami') .description('Show current logged-in server and user') .action(async () => { await whoami(getCtx()); }); program .command('init') .description('Initialize a new document with default files') .argument('[path]', 'Document path (optional)') .option('-e, --extension ', 'Content file extension', '.eta') .option('-d, --draft', 'Mark as draft', false) .option('-p, --published', 'Mark as published', true) .option('--template [name]', 'Initialize template/ in the current directory') .option('--slot [name]', 'Initialize slot/ in the current directory') .action(async (path, options) => { await init(getCtx(), { path, ...options }); }); program .command('vscode') .description('VS Code workspace management') .argument('[directory]', 'Project directory (defaults to current directory)') .option('--init', 'Initialize VS Code workspace with copilot instructions') .option('--open', 'Open workspace in VS Code') .action(async (directory, opts) => { const ctx = getCtx(); await vscodeInit(directory ? { ...ctx, cwd: directory } : ctx, { init: opts?.init, open: opts?.open }); }); // --- shared command builders --- const addLogin = (parent) => parent .command('login') .description('Login to server and save credentials securely') .argument('[url]', 'Server URL with username (e.g. http://user@host.com)') .option('-y, --yes', 'Set as default server without prompting') .option('--use-env', 'Read credentials from SKYWRITER_SECRET env var') .option('--auth-store ', 'Credential storage backend ("file" to store in config)') .action(async (url, opts) => { const authStore = opts?.authStore === 'file' ? 'file' : undefined; if (opts?.authStore && opts.authStore !== 'file') { throw new Error(`Invalid --auth-store value: "${opts.authStore}". Only "file" is allowed.`); } await login(getCtx(), { url, yes: opts?.yes, useEnv: opts?.useEnv, authStore }); }); const addLogout = (parent) => parent .command('logout') .description('Remove credentials for a server') .argument('[url]', 'Server URL with username (e.g. http://user@host.com)') .option('-y, --yes', 'Skip confirmation prompt') .action(async (url, opts) => { await logout(getCtx(), { url, yes: opts?.yes }); }); // --- remote subcommand group --- const remote = program.command('remote').description('Manage remote server connections'); remote .command('list') .description('List all configured remotes') .action(async () => { await sessions(getCtx()); }); addLogin(remote); addLogout(remote); remote .command('switch') .description('Switch the default server') .argument('[url]', 'Server URL with username (e.g. http://user@host.com)') .action(async (url) => { await switchServer(getCtx(), url); }); remote .command('remove') .description('Remove a remote server connection') .argument('[url]', 'Server URL with username (e.g. http://user@host.com)') .action(async (url) => { await removeServer(getCtx(), url); }); // --- top-level aliases --- addLogin(program); addLogout(program); program .command('pull') .description('Update document from server') .argument('[source]', 'Document path (/meow) or full URL (http://host/meow)') .argument('[destination]', 'Target directory (defaults to basename of path)') .option('--via ', 'Transport method: "git" or "tar"') .option('--no-git', 'Skip git init when using tar transport') .option('--prompt', 'Prompt before exec operations') .action(async (source, destination, options) => { const ctx = getCtx(); const prompt = options?.prompt; await pull({ ...ctx, prompt }, source, destination, options); }); program .command('clone') .description('Clone document from server') .argument('', 'Document path (/meow) or full URL (http://host/meow)') .argument('[destination]', 'Target directory (defaults to basename of path)') .option('--via ', 'Transport method: "git" or "tar"') .option('--prompt', 'Prompt before exec operations') .action(async (source, destination, options) => { const ctx = getCtx(); const prompt = options?.prompt; await pull({ ...ctx, prompt }, source, destination, options); }); program .command('push') .description('Push document to server (auto-detects git or tar transport)') .argument('[path]', 'Document path to push (optional, reads from settings.json if not provided)') .option('--via ', 'Transport method: "git" or "tar"') .option('--no-git', 'Use tar transport') .option('--prompt', 'Prompt before exec operations') .action(async (path, options) => { const ctx = getCtx(); const prompt = options?.prompt; await push({ ...ctx, prompt }, path, options); }); program .command('serve') .description('Serve local document from current directory') .option('-p, --port ', 'Port to serve on', '3001') .option('-w, --watch ', 'Watch for file changes', 'true') .option('--clear-cache', 'Clear the cache before serving') .option('--fn ', 'Function context mode: api (remote server) or fs (local filesystem)', 'api') .action(async (options) => { const port = parseInt(options.port, 10); const watch = options.watch !== 'false'; const clearCache = options.clearCache || false; const fnMode = options.fn === 'fs' ? 'fs' : 'api'; await serve(getCtx(), port, watch, clearCache, fnMode); }); program .command('host') .description('Start the production server') .option('-p, --port ', 'Port to serve on', process.env.PORT || '3000') .option('--migrate', 'Run pending database migrations before starting') .option('--no-seed', 'Skip seeding demo content on empty database') .action(async (options) => { const port = parseInt(options.port, 10); const migrateEnv = process.env.MIGRATE; const envMigrate = migrateEnv !== undefined ? migrateEnv !== 'false' : undefined; let migrate; if (envMigrate !== undefined && options.migrate && envMigrate === false) { console.warn(`Warning: MIGRATE=${migrateEnv} env var overrides --migrate flag`); migrate = false; } else if (envMigrate !== undefined) { migrate = envMigrate; } else if (options.migrate) { migrate = true; } else { migrate = false; } const seed = options.seed !== false; await host(getCtx(), port, migrate, seed); }); program .command('settings') .description('Display local settings.json') .option('--fix', 'Auto-fix issues') .action(async (options) => { await settings(getCtx(), { fix: options?.fix }); }); program .command('render') .description('Render local document and output as JSON') .action(async () => { await render(getCtx()); }); return program; } export async function program(args, options = {}) { const stdout = options.stdout ?? process.stdout; const stderr = options.stderr ?? process.stderr; const program = createProgram(); program.configureOutput({ writeOut: str => { stdout.write(str); }, writeErr: str => { stderr.write(str); }, }); program.exitOverride(); try { await program.parseAsync(args, { from: 'user' }); return 0; } catch (error) { if (error instanceof CommanderError) { return error.exitCode; } const err = error; const opts = program.opts(); if (opts.json) { stdout.write(JSON.stringify({ error: err.message }) + '\n'); } else { const isVerbose = opts.logLevel === 'verbose' || opts.logLevel === 'silly'; const text = isVerbose && err.stack ? err.stack : err.message; const cliName = getCliName(); for (const line of text.split('\n')) { stderr.write(`${cyan(cliName)} ${red('error')} ${line}\n`); } } return 1; } } //# sourceMappingURL=program.js.map