UNPKG

@fission-ai/openspec

Version:

AI-native system for spec-driven development

198 lines 7.63 kB
import { spawn } from 'node:child_process'; import * as fs from 'node:fs'; import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js'; import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js'; /** * Register the config command and all its subcommands. * * @param program - The Commander program instance */ export function registerConfigCommand(program) { const configCmd = program .command('config') .description('View and modify global OpenSpec configuration') .option('--scope <scope>', 'Config scope (only "global" supported currently)') .hook('preAction', (thisCommand) => { const opts = thisCommand.opts(); if (opts.scope && opts.scope !== 'global') { console.error('Error: Project-local config is not yet implemented'); process.exit(1); } }); // config path configCmd .command('path') .description('Show config file location') .action(() => { console.log(getGlobalConfigPath()); }); // config list configCmd .command('list') .description('Show all current settings') .option('--json', 'Output as JSON') .action((options) => { const config = getGlobalConfig(); if (options.json) { console.log(JSON.stringify(config, null, 2)); } else { console.log(formatValueYaml(config)); } }); // config get configCmd .command('get <key>') .description('Get a specific value (raw, scriptable)') .action((key) => { const config = getGlobalConfig(); const value = getNestedValue(config, key); if (value === undefined) { process.exitCode = 1; return; } if (typeof value === 'object' && value !== null) { console.log(JSON.stringify(value)); } else { console.log(String(value)); } }); // config set configCmd .command('set <key> <value>') .description('Set a value (auto-coerce types)') .option('--string', 'Force value to be stored as string') .option('--allow-unknown', 'Allow setting unknown keys') .action((key, value, options) => { const allowUnknown = Boolean(options.allowUnknown); const keyValidation = validateConfigKeyPath(key); if (!keyValidation.valid && !allowUnknown) { const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : ''; console.error(`Error: Invalid configuration key "${key}".${reason}`); console.error('Use "openspec config list" to see available keys.'); console.error('Pass --allow-unknown to bypass this check.'); process.exitCode = 1; return; } const config = getGlobalConfig(); const coercedValue = coerceValue(value, options.string || false); // Create a copy to validate before saving const newConfig = JSON.parse(JSON.stringify(config)); setNestedValue(newConfig, key, coercedValue); // Validate the new config const validation = validateConfig(newConfig); if (!validation.success) { console.error(`Error: Invalid configuration - ${validation.error}`); process.exitCode = 1; return; } // Apply changes and save setNestedValue(config, key, coercedValue); saveGlobalConfig(config); const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue); console.log(`Set ${key} = ${displayValue}`); }); // config unset configCmd .command('unset <key>') .description('Remove a key (revert to default)') .action((key) => { const config = getGlobalConfig(); const existed = deleteNestedValue(config, key); if (existed) { saveGlobalConfig(config); console.log(`Unset ${key} (reverted to default)`); } else { console.log(`Key "${key}" was not set`); } }); // config reset configCmd .command('reset') .description('Reset configuration to defaults') .option('--all', 'Reset all configuration (required)') .option('-y, --yes', 'Skip confirmation prompts') .action(async (options) => { if (!options.all) { console.error('Error: --all flag is required for reset'); console.error('Usage: openspec config reset --all [-y]'); process.exitCode = 1; return; } if (!options.yes) { const { confirm } = await import('@inquirer/prompts'); const confirmed = await confirm({ message: 'Reset all configuration to defaults?', default: false, }); if (!confirmed) { console.log('Reset cancelled.'); return; } } saveGlobalConfig({ ...DEFAULT_CONFIG }); console.log('Configuration reset to defaults'); }); // config edit configCmd .command('edit') .description('Open config in $EDITOR') .action(async () => { const editor = process.env.EDITOR || process.env.VISUAL; if (!editor) { console.error('Error: No editor configured'); console.error('Set the EDITOR or VISUAL environment variable to your preferred editor'); console.error('Example: export EDITOR=vim'); process.exitCode = 1; return; } const configPath = getGlobalConfigPath(); // Ensure config file exists with defaults if (!fs.existsSync(configPath)) { saveGlobalConfig({ ...DEFAULT_CONFIG }); } // Spawn editor and wait for it to close // Avoid shell parsing to correctly handle paths with spaces in both // the editor path and config path const child = spawn(editor, [configPath], { stdio: 'inherit', shell: false, }); await new Promise((resolve, reject) => { child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Editor exited with code ${code}`)); } }); child.on('error', reject); }); try { const rawConfig = fs.readFileSync(configPath, 'utf-8'); const parsedConfig = JSON.parse(rawConfig); const validation = validateConfig(parsedConfig); if (!validation.success) { console.error(`Error: Invalid configuration - ${validation.error}`); process.exitCode = 1; } } catch (error) { if (error.code === 'ENOENT') { console.error(`Error: Config file not found at ${configPath}`); } else if (error instanceof SyntaxError) { console.error(`Error: Invalid JSON in ${configPath}`); console.error(error.message); } else { console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`); } process.exitCode = 1; } }); } //# sourceMappingURL=config.js.map