@fission-ai/openspec 1.14.0 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +15 -31
  2. package/dist/cli/commands/config.d.ts +8 -0
  3. package/dist/cli/commands/config.js +88 -0
  4. package/dist/cli/commands/context.d.ts +3 -0
  5. package/dist/cli/commands/context.js +20 -0
  6. package/dist/cli/commands/doctor.d.ts +3 -0
  7. package/dist/cli/commands/doctor.js +18 -0
  8. package/dist/cli/commands/schema.d.ts +6 -0
  9. package/dist/cli/commands/schema.js +57 -0
  10. package/dist/cli/commands/spec.d.ts +3 -0
  11. package/dist/cli/commands/spec.js +42 -0
  12. package/dist/cli/commands/store.d.ts +3 -0
  13. package/dist/cli/commands/store.js +130 -0
  14. package/dist/cli/commands/workset.d.ts +3 -0
  15. package/dist/cli/commands/workset.js +96 -0
  16. package/dist/cli/index.d.ts +5 -0
  17. package/dist/cli/index.js +86 -43
  18. package/dist/commands/completion.js +4 -1
  19. package/dist/commands/config.d.ts +16 -7
  20. package/dist/commands/config.js +342 -395
  21. package/dist/commands/context.d.ts +8 -2
  22. package/dist/commands/context.js +22 -37
  23. package/dist/commands/doctor.d.ts +6 -7
  24. package/dist/commands/doctor.js +14 -27
  25. package/dist/commands/schema.d.ts +19 -5
  26. package/dist/commands/schema.js +662 -702
  27. package/dist/commands/spec.d.ts +13 -4
  28. package/dist/commands/spec.js +91 -120
  29. package/dist/commands/store.d.ts +27 -2
  30. package/dist/commands/store.js +1 -120
  31. package/dist/commands/workflow/default-schema.d.ts +2 -0
  32. package/dist/commands/workflow/default-schema.js +4 -0
  33. package/dist/commands/workflow/instructions.js +1 -1
  34. package/dist/commands/workflow/shared.d.ts +1 -1
  35. package/dist/commands/workflow/shared.js +1 -1
  36. package/dist/commands/workset.d.ts +22 -2
  37. package/dist/commands/workset.js +1 -88
  38. package/dist/core/archive.js +32 -12
  39. package/dist/core/artifact-graph/outputs.js +39 -1
  40. package/dist/core/completion-tip.js +3 -1
  41. package/dist/core/completions/completion-provider.js +4 -2
  42. package/dist/core/config-schema.d.ts +1 -1
  43. package/dist/core/parsers/requirement-blocks.d.ts +11 -0
  44. package/dist/core/parsers/requirement-blocks.js +57 -0
  45. package/dist/core/specs-apply.js +5 -1
  46. package/dist/core/templates/workflows/apply-change.js +4 -3
  47. package/dist/core/templates/workflows/continue-change.js +17 -0
  48. package/dist/core/templates/workflows/explore.js +8 -4
  49. package/dist/core/templates/workflows/update-change.js +4 -4
  50. package/dist/core/templates/workflows/verify-change.js +26 -20
  51. package/dist/core/validation/validator.js +23 -2
  52. package/dist/core/view.js +2 -26
  53. package/dist/utils/change-metadata.d.ts +17 -0
  54. package/dist/utils/change-metadata.js +27 -2
  55. package/dist/utils/file-system.d.ts +7 -0
  56. package/dist/utils/file-system.js +25 -0
  57. package/package.json +1 -1
  58. package/schemas/spec-driven/schema.yaml +28 -4
@@ -5,8 +5,6 @@ import { getGlobalConfigPath, getGlobalConfig, isConfigRootObject, isGlobalConfi
5
5
  import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, hasUnsafeKeySegment, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
6
6
  import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
7
7
  import { OPENSPEC_DIR_NAME } from '../core/config.js';
8
- import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
9
- import { UpdateCommand } from '../core/update.js';
10
8
  import { asErrorMessage, isPromptCancellationError } from './shared-output.js';
11
9
  // cross-spawn finds `.cmd` shims such as `code.cmd` on Windows and escapes each
12
10
  // argument for cmd.exe; elsewhere it is plain spawn. Loaded lazily so other
@@ -269,11 +267,14 @@ export function diffProfileState(before, after) {
269
267
  lines,
270
268
  };
271
269
  }
272
- function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
270
+ async function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
273
271
  const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
274
272
  if (!fs.existsSync(openspecDir)) {
275
273
  return;
276
274
  }
275
+ // Loaded here, not at the top: it pulls in every tool's command adapter,
276
+ // which `config path` and `config list` never need.
277
+ const { hasProjectConfigDrift } = await import('../core/profile-sync-drift.js');
277
278
  if (!hasProjectConfigDrift(projectDir, state.workflows, state.delivery)) {
278
279
  return;
279
280
  }
@@ -282,431 +283,377 @@ function maybeWarnProjectConfigDrift(projectDir, state, colorize) {
282
283
  function printConfigProfileApplyGuidance() {
283
284
  console.log('Config updated. Run `openspec update` in your projects to apply.');
284
285
  }
285
- /**
286
- * Register the config command and all its subcommands.
287
- *
288
- * @param program - The Commander program instance
289
- */
290
- export function registerConfigCommand(program) {
291
- const configCmd = program
292
- .command('config')
293
- .description('View and modify global OpenSpec configuration')
294
- .option('--scope <scope>', 'Config scope (only "global" supported currently)')
295
- .hook('preAction', (thisCommand) => {
296
- const opts = thisCommand.opts();
297
- if (opts.scope && opts.scope !== 'global') {
298
- console.error('Error: Project-local config is not yet implemented');
299
- process.exit(1);
300
- }
301
- });
302
- // config path
303
- configCmd
304
- .command('path')
305
- .description('Show config file location')
306
- .action(() => {
307
- console.log(getGlobalConfigPath());
308
- });
309
- // config list
310
- configCmd
311
- .command('list')
312
- .description('Show all current settings')
313
- .option('--json', 'Output as JSON')
314
- .action((options) => {
315
- const config = getGlobalConfig();
316
- if (options.json) {
317
- console.log(JSON.stringify(config, null, 2));
318
- }
319
- else {
320
- // Read raw config to determine which values are explicit vs defaults
321
- const configPath = getGlobalConfigPath();
322
- let rawConfig = {};
323
- try {
324
- if (fs.existsSync(configPath)) {
325
- const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
326
- // A non-object root holds no explicit settings, and reading a key
327
- // off `null` would crash this read-only command.
328
- if (isConfigRootObject(parsed)) {
329
- rawConfig = parsed;
330
- }
286
+ export function configPathCommand() {
287
+ console.log(getGlobalConfigPath());
288
+ }
289
+ export function configListCommand(options) {
290
+ const config = getGlobalConfig();
291
+ if (options.json) {
292
+ console.log(JSON.stringify(config, null, 2));
293
+ }
294
+ else {
295
+ // Read raw config to determine which values are explicit vs defaults
296
+ const configPath = getGlobalConfigPath();
297
+ let rawConfig = {};
298
+ try {
299
+ if (fs.existsSync(configPath)) {
300
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
301
+ // A non-object root holds no explicit settings, and reading a key
302
+ // off `null` would crash this read-only command.
303
+ if (isConfigRootObject(parsed)) {
304
+ rawConfig = parsed;
331
305
  }
332
306
  }
333
- catch {
334
- // If reading fails, treat all as defaults
335
- }
336
- console.log(formatValueYaml(config));
337
- // Annotate profile settings
338
- const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
339
- const deliverySource = rawConfig.delivery !== undefined ? '(explicit)' : '(default)';
340
- console.log(`\nProfile settings:`);
341
- console.log(` profile: ${config.profile} ${profileSource}`);
342
- console.log(` delivery: ${config.delivery} ${deliverySource}`);
343
- if (config.profile === 'core') {
344
- console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`);
345
- }
346
- else if (config.workflows && config.workflows.length > 0) {
347
- console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
348
- }
349
- else {
350
- console.log(` workflows: (none)`);
351
- }
352
307
  }
353
- });
354
- // config get
355
- configCmd
356
- .command('get <key>')
357
- .description('Get a specific value (raw, scriptable)')
358
- .action((key) => {
359
- const config = getGlobalConfig();
360
- const value = getNestedValue(config, key);
361
- if (value === undefined) {
362
- process.exitCode = 1;
363
- return;
308
+ catch {
309
+ // If reading fails, treat all as defaults
364
310
  }
365
- if (typeof value === 'object' && value !== null) {
366
- console.log(JSON.stringify(value));
311
+ console.log(formatValueYaml(config));
312
+ // Annotate profile settings
313
+ const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
314
+ const deliverySource = rawConfig.delivery !== undefined ? '(explicit)' : '(default)';
315
+ console.log(`\nProfile settings:`);
316
+ console.log(` profile: ${config.profile} ${profileSource}`);
317
+ console.log(` delivery: ${config.delivery} ${deliverySource}`);
318
+ if (config.profile === 'core') {
319
+ console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`);
320
+ }
321
+ else if (config.workflows && config.workflows.length > 0) {
322
+ console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
367
323
  }
368
324
  else {
369
- console.log(String(value));
325
+ console.log(` workflows: (none)`);
370
326
  }
371
- });
372
- // config set
373
- configCmd
374
- .command('set <key> <value>')
375
- .description('Set a value (auto-coerce types)')
376
- .option('--string', 'Force value to be stored as string')
377
- .option('--allow-unknown', 'Allow setting unknown keys')
378
- .action((key, value, options) => {
379
- const allowUnknown = Boolean(options.allowUnknown);
380
- const keyValidation = validateConfigKeyPath(key);
381
- // --allow-unknown relaxes the known-key check, but never the prototype-safety check.
382
- const unsafeKey = hasUnsafeKeySegment(key);
383
- if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
384
- const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
385
- console.error(`Error: Invalid configuration key "${key}".${reason}`);
386
- console.error('Use "openspec config list" to see available keys.');
387
- if (!allowUnknown && !unsafeKey) {
388
- console.error('Pass --allow-unknown to bypass this check.');
327
+ }
328
+ }
329
+ export function configGetCommand(key) {
330
+ const config = getGlobalConfig();
331
+ const value = getNestedValue(config, key);
332
+ if (value === undefined) {
333
+ process.exitCode = 1;
334
+ return;
335
+ }
336
+ if (typeof value === 'object' && value !== null) {
337
+ console.log(JSON.stringify(value));
338
+ }
339
+ else {
340
+ console.log(String(value));
341
+ }
342
+ }
343
+ export function configSetCommand(key, value, options) {
344
+ const allowUnknown = Boolean(options.allowUnknown);
345
+ const keyValidation = validateConfigKeyPath(key);
346
+ // --allow-unknown relaxes the known-key check, but never the prototype-safety check.
347
+ const unsafeKey = hasUnsafeKeySegment(key);
348
+ if (!keyValidation.valid && (!allowUnknown || unsafeKey)) {
349
+ const reason = keyValidation.reason ? ` ${keyValidation.reason}.` : '';
350
+ console.error(`Error: Invalid configuration key "${key}".${reason}`);
351
+ console.error('Use "openspec config list" to see available keys.');
352
+ if (!allowUnknown && !unsafeKey) {
353
+ console.error('Pass --allow-unknown to bypass this check.');
354
+ }
355
+ process.exitCode = 1;
356
+ return;
357
+ }
358
+ if (refuseUnreadableConfig()) {
359
+ return;
360
+ }
361
+ const config = getGlobalConfig();
362
+ const coercedValue = coerceValue(value, options.string || false);
363
+ // Create a copy to validate before saving
364
+ const newConfig = JSON.parse(JSON.stringify(config));
365
+ setNestedValue(newConfig, key, coercedValue);
366
+ // Validate the new config
367
+ const validation = validateConfig(newConfig);
368
+ if (!validation.success) {
369
+ console.error(`Error: Invalid configuration - ${validation.error}`);
370
+ process.exitCode = 1;
371
+ return;
372
+ }
373
+ // Apply changes and save
374
+ setNestedValue(config, key, coercedValue);
375
+ saveGlobalConfig(config);
376
+ const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
377
+ console.log(`Set ${key} = ${displayValue}`);
378
+ }
379
+ export function configUnsetCommand(key) {
380
+ if (refuseUnreadableConfig()) {
381
+ return;
382
+ }
383
+ const config = getGlobalConfig();
384
+ const existed = deleteNestedValue(config, key);
385
+ if (existed) {
386
+ saveGlobalConfig(config);
387
+ console.log(`Unset ${key} (reverted to default)`);
388
+ }
389
+ else {
390
+ console.log(`Key "${key}" was not set`);
391
+ }
392
+ }
393
+ export async function configResetCommand(options) {
394
+ if (!options.all) {
395
+ console.error('Error: --all flag is required for reset');
396
+ console.error('Usage: openspec config reset --all [-y]');
397
+ process.exitCode = 1;
398
+ return;
399
+ }
400
+ if (!options.yes) {
401
+ const { confirm } = await import('@inquirer/prompts');
402
+ let confirmed;
403
+ try {
404
+ confirmed = await confirm({
405
+ message: 'Reset all configuration to defaults?',
406
+ default: false,
407
+ });
408
+ }
409
+ catch (error) {
410
+ if (isPromptCancellationError(error)) {
411
+ console.log('Reset cancelled.');
412
+ process.exitCode = 130;
413
+ return;
389
414
  }
390
- process.exitCode = 1;
391
- return;
415
+ throw error;
392
416
  }
393
- if (refuseUnreadableConfig()) {
417
+ if (!confirmed) {
418
+ console.log('Reset cancelled.');
394
419
  return;
395
420
  }
396
- const config = getGlobalConfig();
397
- const coercedValue = coerceValue(value, options.string || false);
398
- // Create a copy to validate before saving
399
- const newConfig = JSON.parse(JSON.stringify(config));
400
- setNestedValue(newConfig, key, coercedValue);
401
- // Validate the new config
402
- const validation = validateConfig(newConfig);
421
+ }
422
+ // A reset is the one write meant to replace a file that cannot be parsed.
423
+ saveGlobalConfig({ ...DEFAULT_CONFIG }, { replaceUnreadable: true });
424
+ console.log('Configuration reset to defaults');
425
+ }
426
+ export async function configEditCommand() {
427
+ const editor = process.env.EDITOR || process.env.VISUAL;
428
+ if (!editor) {
429
+ console.error('Error: No editor configured');
430
+ console.error('Set the EDITOR or VISUAL environment variable to your preferred editor');
431
+ console.error('Example: export EDITOR=vim');
432
+ process.exitCode = 1;
433
+ return;
434
+ }
435
+ const configPath = getGlobalConfigPath();
436
+ // Ensure config file exists with defaults
437
+ if (!fs.existsSync(configPath)) {
438
+ saveGlobalConfig({ ...DEFAULT_CONFIG });
439
+ }
440
+ // Wait for the editor to close; a failure is reported, never thrown.
441
+ const outcome = await runEditor(editor, configPath);
442
+ if ('error' in outcome || outcome.code !== 0) {
443
+ reportEditorFailure(editor, outcome);
444
+ process.exitCode = 1;
445
+ return;
446
+ }
447
+ try {
448
+ const rawConfig = fs.readFileSync(configPath, 'utf-8');
449
+ const parsedConfig = JSON.parse(rawConfig);
450
+ const validation = validateConfig(parsedConfig);
403
451
  if (!validation.success) {
404
452
  console.error(`Error: Invalid configuration - ${validation.error}`);
405
453
  process.exitCode = 1;
406
- return;
407
454
  }
408
- // Apply changes and save
409
- setNestedValue(config, key, coercedValue);
410
- saveGlobalConfig(config);
411
- const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
412
- console.log(`Set ${key} = ${displayValue}`);
413
- });
414
- // config unset
415
- configCmd
416
- .command('unset <key>')
417
- .description('Remove a key (revert to default)')
418
- .action((key) => {
419
- if (refuseUnreadableConfig()) {
420
- return;
455
+ }
456
+ catch (error) {
457
+ if (error.code === 'ENOENT') {
458
+ console.error(`Error: Config file not found at ${configPath}`);
421
459
  }
422
- const config = getGlobalConfig();
423
- const existed = deleteNestedValue(config, key);
424
- if (existed) {
425
- saveGlobalConfig(config);
426
- console.log(`Unset ${key} (reverted to default)`);
460
+ else if (error instanceof SyntaxError) {
461
+ console.error(`Error: Invalid JSON in ${configPath}`);
462
+ console.error(error.message);
427
463
  }
428
464
  else {
429
- console.log(`Key "${key}" was not set`);
465
+ console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`);
430
466
  }
431
- });
432
- // config reset
433
- configCmd
434
- .command('reset')
435
- .description('Reset configuration to defaults')
436
- .option('--all', 'Reset all configuration (required)')
437
- .option('-y, --yes', 'Skip confirmation prompts')
438
- .action(async (options) => {
439
- if (!options.all) {
440
- console.error('Error: --all flag is required for reset');
441
- console.error('Usage: openspec config reset --all [-y]');
442
- process.exitCode = 1;
467
+ process.exitCode = 1;
468
+ }
469
+ }
470
+ export async function configProfileCommand(preset) {
471
+ if (refuseUnreadableConfig()) {
472
+ return;
473
+ }
474
+ // Preset shortcut: `openspec config profile core`
475
+ if (preset === 'core') {
476
+ const config = getGlobalConfig();
477
+ config.profile = 'core';
478
+ config.workflows = [...CORE_WORKFLOWS];
479
+ // Preserve delivery setting
480
+ saveGlobalConfig(config);
481
+ printConfigProfileApplyGuidance();
482
+ return;
483
+ }
484
+ if (preset) {
485
+ console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
486
+ process.exitCode = 1;
487
+ return;
488
+ }
489
+ // Non-interactive check
490
+ if (!process.stdout.isTTY) {
491
+ console.error('Interactive mode required. Use `openspec config profile core` or set config via environment/flags.');
492
+ process.exitCode = 1;
493
+ return;
494
+ }
495
+ // Interactive picker
496
+ const { select, checkbox, confirm } = await import('@inquirer/prompts');
497
+ const chalk = (await import('chalk')).default;
498
+ try {
499
+ const config = getGlobalConfig();
500
+ const currentState = resolveCurrentProfileState(config);
501
+ console.log(chalk.bold('\nCurrent profile settings'));
502
+ console.log(` Delivery: ${currentState.delivery}`);
503
+ console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
504
+ console.log(chalk.dim(' Delivery = where workflows are installed (skills, commands, or both)'));
505
+ console.log(chalk.dim(' Workflows = which actions are available (propose, explore, apply, etc.)'));
506
+ console.log();
507
+ const action = await select({
508
+ message: 'What do you want to configure?',
509
+ choices: [
510
+ {
511
+ value: 'both',
512
+ name: 'Delivery and workflows',
513
+ description: 'Update install mode and available actions together',
514
+ },
515
+ {
516
+ value: 'delivery',
517
+ name: 'Delivery only',
518
+ description: 'Change where workflows are installed',
519
+ },
520
+ {
521
+ value: 'workflows',
522
+ name: 'Workflows only',
523
+ description: 'Change which workflow actions are available',
524
+ },
525
+ {
526
+ value: 'keep',
527
+ name: 'Keep current settings (exit)',
528
+ description: 'Leave configuration unchanged and exit',
529
+ },
530
+ ],
531
+ });
532
+ if (action === 'keep') {
533
+ console.log('No config changes.');
534
+ await maybeWarnProjectConfigDrift(process.cwd(), currentState, chalk.yellow);
443
535
  return;
444
536
  }
445
- if (!options.yes) {
446
- const { confirm } = await import('@inquirer/prompts');
447
- let confirmed;
448
- try {
449
- confirmed = await confirm({
450
- message: 'Reset all configuration to defaults?',
451
- default: false,
452
- });
453
- }
454
- catch (error) {
455
- if (isPromptCancellationError(error)) {
456
- console.log('Reset cancelled.');
457
- process.exitCode = 130;
458
- return;
537
+ const nextState = {
538
+ profile: currentState.profile,
539
+ delivery: currentState.delivery,
540
+ workflows: [...currentState.workflows],
541
+ };
542
+ let workflowSelectionChanged = false;
543
+ if (action === 'both' || action === 'delivery') {
544
+ const deliveryChoices = [
545
+ {
546
+ value: 'both',
547
+ name: 'Both (skills + commands)',
548
+ description: 'Install workflows as both skills and slash commands',
549
+ },
550
+ {
551
+ value: 'skills',
552
+ name: 'Skills only',
553
+ description: 'Install workflows only as skills',
554
+ },
555
+ {
556
+ value: 'commands',
557
+ name: 'Commands only',
558
+ description: 'Install workflows only as slash commands',
559
+ },
560
+ ];
561
+ for (const choice of deliveryChoices) {
562
+ if (choice.value === currentState.delivery) {
563
+ choice.name += ' [current]';
459
564
  }
460
- throw error;
461
- }
462
- if (!confirmed) {
463
- console.log('Reset cancelled.');
464
- return;
465
565
  }
566
+ nextState.delivery = await select({
567
+ message: 'Delivery mode (how workflows are installed):',
568
+ choices: deliveryChoices,
569
+ default: currentState.delivery,
570
+ });
466
571
  }
467
- // A reset is the one write meant to replace a file that cannot be parsed.
468
- saveGlobalConfig({ ...DEFAULT_CONFIG }, { replaceUnreadable: true });
469
- console.log('Configuration reset to defaults');
470
- });
471
- // config edit
472
- configCmd
473
- .command('edit')
474
- .description('Open config in $EDITOR')
475
- .action(async () => {
476
- const editor = process.env.EDITOR || process.env.VISUAL;
477
- if (!editor) {
478
- console.error('Error: No editor configured');
479
- console.error('Set the EDITOR or VISUAL environment variable to your preferred editor');
480
- console.error('Example: export EDITOR=vim');
481
- process.exitCode = 1;
482
- return;
483
- }
484
- const configPath = getGlobalConfigPath();
485
- // Ensure config file exists with defaults
486
- if (!fs.existsSync(configPath)) {
487
- saveGlobalConfig({ ...DEFAULT_CONFIG });
488
- }
489
- // Wait for the editor to close; a failure is reported, never thrown.
490
- const outcome = await runEditor(editor, configPath);
491
- if ('error' in outcome || outcome.code !== 0) {
492
- reportEditorFailure(editor, outcome);
493
- process.exitCode = 1;
494
- return;
495
- }
496
- try {
497
- const rawConfig = fs.readFileSync(configPath, 'utf-8');
498
- const parsedConfig = JSON.parse(rawConfig);
499
- const validation = validateConfig(parsedConfig);
500
- if (!validation.success) {
501
- console.error(`Error: Invalid configuration - ${validation.error}`);
502
- process.exitCode = 1;
503
- }
504
- }
505
- catch (error) {
506
- if (error.code === 'ENOENT') {
507
- console.error(`Error: Config file not found at ${configPath}`);
508
- }
509
- else if (error instanceof SyntaxError) {
510
- console.error(`Error: Invalid JSON in ${configPath}`);
511
- console.error(error.message);
512
- }
513
- else {
514
- console.error(`Error: Unable to validate configuration - ${error instanceof Error ? error.message : String(error)}`);
515
- }
516
- process.exitCode = 1;
517
- }
518
- });
519
- // config profile [preset]
520
- configCmd
521
- .command('profile [preset]')
522
- .description('Configure workflow profile (interactive picker or preset shortcut)')
523
- .action(async (preset) => {
524
- if (refuseUnreadableConfig()) {
525
- return;
526
- }
527
- // Preset shortcut: `openspec config profile core`
528
- if (preset === 'core') {
529
- const config = getGlobalConfig();
530
- config.profile = 'core';
531
- config.workflows = [...CORE_WORKFLOWS];
532
- // Preserve delivery setting
533
- saveGlobalConfig(config);
534
- printConfigProfileApplyGuidance();
572
+ if (action === 'both' || action === 'workflows') {
573
+ const formatWorkflowChoice = (workflow) => {
574
+ const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
575
+ name: workflow,
576
+ description: `Workflow: ${workflow}`,
577
+ };
578
+ return {
579
+ value: workflow,
580
+ name: metadata.name,
581
+ description: metadata.description,
582
+ short: metadata.name,
583
+ checked: currentState.workflows.includes(workflow),
584
+ };
585
+ };
586
+ const selectedWorkflows = await checkbox({
587
+ // The `instructions` option was removed in @inquirer/checkbox v5.
588
+ // Its replacement, the built-in keys help tip, renders
589
+ // "↑↓ navigate • space select • ⏎ submit" by default — a superset of
590
+ // the hint this used to pass — so no theme override is needed here.
591
+ message: 'Select workflows to make available:',
592
+ pageSize: ALL_WORKFLOWS.length,
593
+ theme: {
594
+ icon: {
595
+ checked: '[x]',
596
+ unchecked: '[ ]',
597
+ },
598
+ },
599
+ choices: ALL_WORKFLOWS.map(formatWorkflowChoice),
600
+ });
601
+ nextState.workflows = selectedWorkflows;
602
+ workflowSelectionChanged =
603
+ selectedWorkflows.length !== currentState.workflows.length ||
604
+ selectedWorkflows.some((workflow) => !currentState.workflows.includes(workflow));
605
+ nextState.profile = workflowSelectionChanged
606
+ ? deriveProfileFromWorkflowSelection(selectedWorkflows)
607
+ : currentState.profile;
608
+ }
609
+ const diff = diffProfileState(currentState, nextState);
610
+ if (!diff.hasChanges) {
611
+ console.log('No config changes.');
612
+ await maybeWarnProjectConfigDrift(process.cwd(), nextState, chalk.yellow);
535
613
  return;
536
614
  }
537
- if (preset) {
538
- console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
539
- process.exitCode = 1;
540
- return;
615
+ console.log(chalk.bold('\nConfig changes:'));
616
+ for (const line of diff.lines) {
617
+ console.log(` ${line}`);
541
618
  }
542
- // Non-interactive check
543
- if (!process.stdout.isTTY) {
544
- console.error('Interactive mode required. Use `openspec config profile core` or set config via environment/flags.');
545
- process.exitCode = 1;
546
- return;
619
+ console.log();
620
+ config.profile = nextState.profile;
621
+ config.delivery = nextState.delivery;
622
+ if (currentState.profile !== 'custom' || workflowSelectionChanged) {
623
+ config.workflows = nextState.workflows;
547
624
  }
548
- // Interactive picker
549
- const { select, checkbox, confirm } = await import('@inquirer/prompts');
550
- const chalk = (await import('chalk')).default;
551
- try {
552
- const config = getGlobalConfig();
553
- const currentState = resolveCurrentProfileState(config);
554
- console.log(chalk.bold('\nCurrent profile settings'));
555
- console.log(` Delivery: ${currentState.delivery}`);
556
- console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
557
- console.log(chalk.dim(' Delivery = where workflows are installed (skills, commands, or both)'));
558
- console.log(chalk.dim(' Workflows = which actions are available (propose, explore, apply, etc.)'));
559
- console.log();
560
- const action = await select({
561
- message: 'What do you want to configure?',
562
- choices: [
563
- {
564
- value: 'both',
565
- name: 'Delivery and workflows',
566
- description: 'Update install mode and available actions together',
567
- },
568
- {
569
- value: 'delivery',
570
- name: 'Delivery only',
571
- description: 'Change where workflows are installed',
572
- },
573
- {
574
- value: 'workflows',
575
- name: 'Workflows only',
576
- description: 'Change which workflow actions are available',
577
- },
578
- {
579
- value: 'keep',
580
- name: 'Keep current settings (exit)',
581
- description: 'Leave configuration unchanged and exit',
582
- },
583
- ],
625
+ saveGlobalConfig(config);
626
+ // Check if inside an OpenSpec project
627
+ const projectDir = process.cwd();
628
+ const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
629
+ if (fs.existsSync(openspecDir)) {
630
+ const applyNow = await confirm({
631
+ message: 'Apply changes to this project now?',
632
+ default: true,
584
633
  });
585
- if (action === 'keep') {
586
- console.log('No config changes.');
587
- maybeWarnProjectConfigDrift(process.cwd(), currentState, chalk.yellow);
588
- return;
589
- }
590
- const nextState = {
591
- profile: currentState.profile,
592
- delivery: currentState.delivery,
593
- workflows: [...currentState.workflows],
594
- };
595
- let workflowSelectionChanged = false;
596
- if (action === 'both' || action === 'delivery') {
597
- const deliveryChoices = [
598
- {
599
- value: 'both',
600
- name: 'Both (skills + commands)',
601
- description: 'Install workflows as both skills and slash commands',
602
- },
603
- {
604
- value: 'skills',
605
- name: 'Skills only',
606
- description: 'Install workflows only as skills',
607
- },
608
- {
609
- value: 'commands',
610
- name: 'Commands only',
611
- description: 'Install workflows only as slash commands',
612
- },
613
- ];
614
- for (const choice of deliveryChoices) {
615
- if (choice.value === currentState.delivery) {
616
- choice.name += ' [current]';
617
- }
634
+ if (applyNow) {
635
+ try {
636
+ const { UpdateCommand } = await import('../core/update.js');
637
+ await new UpdateCommand().execute(projectDir);
638
+ console.log('Run `openspec update` in your other projects to apply.');
618
639
  }
619
- nextState.delivery = await select({
620
- message: 'Delivery mode (how workflows are installed):',
621
- choices: deliveryChoices,
622
- default: currentState.delivery,
623
- });
624
- }
625
- if (action === 'both' || action === 'workflows') {
626
- const formatWorkflowChoice = (workflow) => {
627
- const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
628
- name: workflow,
629
- description: `Workflow: ${workflow}`,
630
- };
631
- return {
632
- value: workflow,
633
- name: metadata.name,
634
- description: metadata.description,
635
- short: metadata.name,
636
- checked: currentState.workflows.includes(workflow),
637
- };
638
- };
639
- const selectedWorkflows = await checkbox({
640
- // The `instructions` option was removed in @inquirer/checkbox v5.
641
- // Its replacement, the built-in keys help tip, renders
642
- // "↑↓ navigate • space select • ⏎ submit" by default — a superset of
643
- // the hint this used to pass — so no theme override is needed here.
644
- message: 'Select workflows to make available:',
645
- pageSize: ALL_WORKFLOWS.length,
646
- theme: {
647
- icon: {
648
- checked: '[x]',
649
- unchecked: '[ ]',
650
- },
651
- },
652
- choices: ALL_WORKFLOWS.map(formatWorkflowChoice),
653
- });
654
- nextState.workflows = selectedWorkflows;
655
- workflowSelectionChanged =
656
- selectedWorkflows.length !== currentState.workflows.length ||
657
- selectedWorkflows.some((workflow) => !currentState.workflows.includes(workflow));
658
- nextState.profile = workflowSelectionChanged
659
- ? deriveProfileFromWorkflowSelection(selectedWorkflows)
660
- : currentState.profile;
661
- }
662
- const diff = diffProfileState(currentState, nextState);
663
- if (!diff.hasChanges) {
664
- console.log('No config changes.');
665
- maybeWarnProjectConfigDrift(process.cwd(), nextState, chalk.yellow);
666
- return;
667
- }
668
- console.log(chalk.bold('\nConfig changes:'));
669
- for (const line of diff.lines) {
670
- console.log(` ${line}`);
671
- }
672
- console.log();
673
- config.profile = nextState.profile;
674
- config.delivery = nextState.delivery;
675
- if (currentState.profile !== 'custom' || workflowSelectionChanged) {
676
- config.workflows = nextState.workflows;
677
- }
678
- saveGlobalConfig(config);
679
- // Check if inside an OpenSpec project
680
- const projectDir = process.cwd();
681
- const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
682
- if (fs.existsSync(openspecDir)) {
683
- const applyNow = await confirm({
684
- message: 'Apply changes to this project now?',
685
- default: true,
686
- });
687
- if (applyNow) {
688
- try {
689
- await new UpdateCommand().execute(projectDir);
690
- console.log('Run `openspec update` in your other projects to apply.');
691
- }
692
- catch (error) {
693
- console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
694
- console.error('Please run it manually to apply the profile changes.');
695
- process.exitCode = 1;
696
- }
697
- return;
640
+ catch (error) {
641
+ console.error(`\`openspec update\` failed: ${asErrorMessage(error)}`);
642
+ console.error('Please run it manually to apply the profile changes.');
643
+ process.exitCode = 1;
698
644
  }
699
- }
700
- printConfigProfileApplyGuidance();
701
- }
702
- catch (error) {
703
- if (isPromptCancellationError(error)) {
704
- console.log('Config profile cancelled.');
705
- process.exitCode = 130;
706
645
  return;
707
646
  }
708
- throw error;
709
647
  }
710
- });
648
+ printConfigProfileApplyGuidance();
649
+ }
650
+ catch (error) {
651
+ if (isPromptCancellationError(error)) {
652
+ console.log('Config profile cancelled.');
653
+ process.exitCode = 130;
654
+ return;
655
+ }
656
+ throw error;
657
+ }
711
658
  }
712
659
  //# sourceMappingURL=config.js.map