@fission-ai/openspec
Version:
AI-native system for spec-driven development
198 lines • 7.63 kB
JavaScript
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