@fission-ai/openspec 1.1.1 → 1.3.0

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 (85) hide show
  1. package/README.md +8 -9
  2. package/dist/cli/index.js +2 -0
  3. package/dist/commands/config.d.ts +28 -0
  4. package/dist/commands/config.js +359 -5
  5. package/dist/commands/workflow/shared.d.ts +5 -0
  6. package/dist/commands/workflow/shared.js +21 -16
  7. package/dist/commands/workflow/status.js +18 -1
  8. package/dist/core/available-tools.d.ts +17 -0
  9. package/dist/core/available-tools.js +43 -0
  10. package/dist/core/command-generation/adapters/bob.d.ts +14 -0
  11. package/dist/core/command-generation/adapters/bob.js +45 -0
  12. package/dist/core/command-generation/adapters/index.d.ts +5 -0
  13. package/dist/core/command-generation/adapters/index.js +5 -0
  14. package/dist/core/command-generation/adapters/junie.d.ts +13 -0
  15. package/dist/core/command-generation/adapters/junie.js +26 -0
  16. package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
  17. package/dist/core/command-generation/adapters/kiro.js +26 -0
  18. package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
  19. package/dist/core/command-generation/adapters/lingma.js +30 -0
  20. package/dist/core/command-generation/adapters/opencode.d.ts +1 -1
  21. package/dist/core/command-generation/adapters/opencode.js +2 -2
  22. package/dist/core/command-generation/adapters/pi.d.ts +18 -0
  23. package/dist/core/command-generation/adapters/pi.js +55 -0
  24. package/dist/core/command-generation/registry.js +10 -0
  25. package/dist/core/completions/command-registry.js +5 -0
  26. package/dist/core/completions/installers/powershell-installer.d.ts +14 -0
  27. package/dist/core/completions/installers/powershell-installer.js +69 -9
  28. package/dist/core/config-schema.d.ts +10 -0
  29. package/dist/core/config-schema.js +14 -1
  30. package/dist/core/config.d.ts +1 -0
  31. package/dist/core/config.js +7 -1
  32. package/dist/core/global-config.d.ts +5 -0
  33. package/dist/core/global-config.js +12 -2
  34. package/dist/core/init.d.ts +5 -0
  35. package/dist/core/init.js +209 -52
  36. package/dist/core/legacy-cleanup.d.ts +1 -1
  37. package/dist/core/legacy-cleanup.js +23 -10
  38. package/dist/core/migration.d.ts +23 -0
  39. package/dist/core/migration.js +108 -0
  40. package/dist/core/profile-sync-drift.d.ts +38 -0
  41. package/dist/core/profile-sync-drift.js +200 -0
  42. package/dist/core/profiles.d.ts +26 -0
  43. package/dist/core/profiles.js +40 -0
  44. package/dist/core/shared/index.d.ts +1 -1
  45. package/dist/core/shared/index.js +1 -1
  46. package/dist/core/shared/skill-generation.d.ts +14 -7
  47. package/dist/core/shared/skill-generation.js +36 -20
  48. package/dist/core/shared/tool-detection.d.ts +8 -3
  49. package/dist/core/shared/tool-detection.js +18 -0
  50. package/dist/core/templates/index.d.ts +1 -1
  51. package/dist/core/templates/index.js +2 -2
  52. package/dist/core/templates/skill-templates.d.ts +15 -118
  53. package/dist/core/templates/skill-templates.js +14 -3424
  54. package/dist/core/templates/types.d.ts +19 -0
  55. package/dist/core/templates/types.js +5 -0
  56. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  57. package/dist/core/templates/workflows/apply-change.js +308 -0
  58. package/dist/core/templates/workflows/archive-change.d.ts +10 -0
  59. package/dist/core/templates/workflows/archive-change.js +271 -0
  60. package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
  61. package/dist/core/templates/workflows/bulk-archive-change.js +488 -0
  62. package/dist/core/templates/workflows/continue-change.d.ts +10 -0
  63. package/dist/core/templates/workflows/continue-change.js +232 -0
  64. package/dist/core/templates/workflows/explore.d.ts +10 -0
  65. package/dist/core/templates/workflows/explore.js +461 -0
  66. package/dist/core/templates/workflows/feedback.d.ts +9 -0
  67. package/dist/core/templates/workflows/feedback.js +108 -0
  68. package/dist/core/templates/workflows/ff-change.d.ts +10 -0
  69. package/dist/core/templates/workflows/ff-change.js +198 -0
  70. package/dist/core/templates/workflows/new-change.d.ts +10 -0
  71. package/dist/core/templates/workflows/new-change.js +143 -0
  72. package/dist/core/templates/workflows/onboard.d.ts +10 -0
  73. package/dist/core/templates/workflows/onboard.js +565 -0
  74. package/dist/core/templates/workflows/propose.d.ts +10 -0
  75. package/dist/core/templates/workflows/propose.js +216 -0
  76. package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
  77. package/dist/core/templates/workflows/sync-specs.js +272 -0
  78. package/dist/core/templates/workflows/verify-change.d.ts +10 -0
  79. package/dist/core/templates/workflows/verify-change.js +332 -0
  80. package/dist/core/update.d.ts +36 -1
  81. package/dist/core/update.js +291 -65
  82. package/dist/prompts/searchable-multi-select.d.ts +3 -2
  83. package/dist/prompts/searchable-multi-select.js +22 -12
  84. package/package.json +1 -1
  85. package/scripts/postinstall.js +8 -72
package/README.md CHANGED
@@ -36,7 +36,7 @@ Our philosophy:
36
36
  > [!TIP]
37
37
  > **New workflow now available!** We've rebuilt OpenSpec with a new artifact-guided workflow.
38
38
  >
39
- > Run `/opsx:onboard` to get started. → [Learn more here](docs/opsx.md)
39
+ > Run `/opsx:propose "your idea"` to get started. → [Learn more here](docs/opsx.md)
40
40
 
41
41
  <p align="center">
42
42
  Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
@@ -46,17 +46,14 @@ Our philosophy:
46
46
 
47
47
  Using OpenSpec in a team? [Email here](mailto:teams@openspec.dev) for access to our Slack channel.
48
48
 
49
- <!-- TODO: Add GIF demo of /opsx:new → /opsx:archive workflow -->
49
+ <!-- TODO: Add GIF demo of /opsx:propose → /opsx:archive workflow -->
50
50
 
51
51
  ## See it in action
52
52
 
53
53
  ```text
54
- You: /opsx:new add-dark-mode
54
+ You: /opsx:propose add-dark-mode
55
55
  AI: Created openspec/changes/add-dark-mode/
56
- Ready to create: proposal
57
-
58
- You: /opsx:ff # "fast-forward" - generate all planning docs
59
- AI: ✓ proposal.md — why we're doing this, what's changing
56
+ proposal.md why we're doing this, what's changing
60
57
  ✓ specs/ — requirements and scenarios
61
58
  ✓ design.md — technical approach
62
59
  ✓ tasks.md — implementation checklist
@@ -101,10 +98,12 @@ cd your-project
101
98
  openspec init
102
99
  ```
103
100
 
104
- Now tell your AI: `/opsx:new <what-you-want-to-build>`
101
+ Now tell your AI: `/opsx:propose <what-you-want-to-build>`
102
+
103
+ If you want the expanded workflow (`/opsx:new`, `/opsx:continue`, `/opsx:ff`, `/opsx:verify`, `/opsx:sync`, `/opsx:bulk-archive`, `/opsx:onboard`), select it with `openspec config profile` and apply with `openspec update`.
105
104
 
106
105
  > [!NOTE]
107
- > Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 20+ tools and growing.
106
+ > Not sure if your tool is supported? [View the full list](docs/supported-tools.md) – we support 25+ tools and growing.
108
107
  >
109
108
  > Also works with pnpm, yarn, bun, and nix. [See installation options](docs/installation.md).
110
109
 
package/dist/cli/index.js CHANGED
@@ -70,6 +70,7 @@ program
70
70
  .description('Initialize OpenSpec in your project')
71
71
  .option('--tools <tools>', toolsOptionDescription)
72
72
  .option('--force', 'Auto-cleanup legacy files without prompting')
73
+ .option('--profile <profile>', 'Override global config profile (core or custom)')
73
74
  .action(async (targetPath = '.', options) => {
74
75
  try {
75
76
  // Validate that the path is a valid directory
@@ -96,6 +97,7 @@ program
96
97
  const initCommand = new InitCommand({
97
98
  tools: options?.tools,
98
99
  force: options?.force,
100
+ profile: options?.profile,
99
101
  });
100
102
  await initCommand.execute(targetPath);
101
103
  }
@@ -1,8 +1,36 @@
1
1
  import { Command } from 'commander';
2
+ import { GlobalConfig } from '../core/global-config.js';
3
+ import type { Profile, Delivery } from '../core/global-config.js';
4
+ interface ProfileState {
5
+ profile: Profile;
6
+ delivery: Delivery;
7
+ workflows: string[];
8
+ }
9
+ interface ProfileStateDiff {
10
+ hasChanges: boolean;
11
+ lines: string[];
12
+ }
13
+ /**
14
+ * Resolve the effective current profile state from global config defaults.
15
+ */
16
+ export declare function resolveCurrentProfileState(config: GlobalConfig): ProfileState;
17
+ /**
18
+ * Derive profile type from selected workflows.
19
+ */
20
+ export declare function deriveProfileFromWorkflowSelection(selectedWorkflows: string[]): Profile;
21
+ /**
22
+ * Format a compact workflow summary for the profile header.
23
+ */
24
+ export declare function formatWorkflowSummary(workflows: readonly string[], profile: Profile): string;
25
+ /**
26
+ * Build a user-facing diff summary between two profile states.
27
+ */
28
+ export declare function diffProfileState(before: ProfileState, after: ProfileState): ProfileStateDiff;
2
29
  /**
3
30
  * Register the config command and all its subcommands.
4
31
  *
5
32
  * @param program - The Commander program instance
6
33
  */
7
34
  export declare function registerConfigCommand(program: Command): void;
35
+ export {};
8
36
  //# sourceMappingURL=config.d.ts.map
@@ -1,7 +1,147 @@
1
- import { spawn } from 'node:child_process';
1
+ import { spawn, execSync } from 'node:child_process';
2
2
  import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
3
4
  import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
4
5
  import { getNestedValue, setNestedValue, deleteNestedValue, coerceValue, formatValueYaml, validateConfigKeyPath, validateConfig, DEFAULT_CONFIG, } from '../core/config-schema.js';
6
+ import { CORE_WORKFLOWS, ALL_WORKFLOWS, getProfileWorkflows } from '../core/profiles.js';
7
+ import { OPENSPEC_DIR_NAME } from '../core/config.js';
8
+ import { hasProjectConfigDrift } from '../core/profile-sync-drift.js';
9
+ const WORKFLOW_PROMPT_META = {
10
+ propose: {
11
+ name: 'Propose change',
12
+ description: 'Create proposal, design, and tasks from a request',
13
+ },
14
+ explore: {
15
+ name: 'Explore ideas',
16
+ description: 'Investigate a problem before implementation',
17
+ },
18
+ new: {
19
+ name: 'New change',
20
+ description: 'Create a new change scaffold quickly',
21
+ },
22
+ continue: {
23
+ name: 'Continue change',
24
+ description: 'Resume work on an existing change',
25
+ },
26
+ apply: {
27
+ name: 'Apply tasks',
28
+ description: 'Implement tasks from the current change',
29
+ },
30
+ ff: {
31
+ name: 'Fast-forward',
32
+ description: 'Run a faster implementation workflow',
33
+ },
34
+ sync: {
35
+ name: 'Sync specs',
36
+ description: 'Sync change artifacts with specs',
37
+ },
38
+ archive: {
39
+ name: 'Archive change',
40
+ description: 'Finalize and archive a completed change',
41
+ },
42
+ 'bulk-archive': {
43
+ name: 'Bulk archive',
44
+ description: 'Archive multiple completed changes together',
45
+ },
46
+ verify: {
47
+ name: 'Verify change',
48
+ description: 'Run verification checks against a change',
49
+ },
50
+ onboard: {
51
+ name: 'Onboard',
52
+ description: 'Guided onboarding flow for OpenSpec',
53
+ },
54
+ };
55
+ function isPromptCancellationError(error) {
56
+ return (error instanceof Error &&
57
+ (error.name === 'ExitPromptError' || error.message.includes('force closed the prompt with SIGINT')));
58
+ }
59
+ /**
60
+ * Resolve the effective current profile state from global config defaults.
61
+ */
62
+ export function resolveCurrentProfileState(config) {
63
+ const profile = config.profile || 'core';
64
+ const delivery = config.delivery || 'both';
65
+ const workflows = [
66
+ ...getProfileWorkflows(profile, config.workflows ? [...config.workflows] : undefined),
67
+ ];
68
+ return { profile, delivery, workflows };
69
+ }
70
+ /**
71
+ * Derive profile type from selected workflows.
72
+ */
73
+ export function deriveProfileFromWorkflowSelection(selectedWorkflows) {
74
+ const isCoreMatch = selectedWorkflows.length === CORE_WORKFLOWS.length &&
75
+ CORE_WORKFLOWS.every((w) => selectedWorkflows.includes(w));
76
+ return isCoreMatch ? 'core' : 'custom';
77
+ }
78
+ /**
79
+ * Format a compact workflow summary for the profile header.
80
+ */
81
+ export function formatWorkflowSummary(workflows, profile) {
82
+ return `${workflows.length} selected (${profile})`;
83
+ }
84
+ function stableWorkflowOrder(workflows) {
85
+ const seen = new Set();
86
+ const ordered = [];
87
+ for (const workflow of ALL_WORKFLOWS) {
88
+ if (workflows.includes(workflow) && !seen.has(workflow)) {
89
+ ordered.push(workflow);
90
+ seen.add(workflow);
91
+ }
92
+ }
93
+ const extras = workflows.filter((w) => !ALL_WORKFLOWS.includes(w));
94
+ extras.sort();
95
+ for (const extra of extras) {
96
+ if (!seen.has(extra)) {
97
+ ordered.push(extra);
98
+ seen.add(extra);
99
+ }
100
+ }
101
+ return ordered;
102
+ }
103
+ /**
104
+ * Build a user-facing diff summary between two profile states.
105
+ */
106
+ export function diffProfileState(before, after) {
107
+ const lines = [];
108
+ if (before.delivery !== after.delivery) {
109
+ lines.push(`delivery: ${before.delivery} -> ${after.delivery}`);
110
+ }
111
+ if (before.profile !== after.profile) {
112
+ lines.push(`profile: ${before.profile} -> ${after.profile}`);
113
+ }
114
+ const beforeOrdered = stableWorkflowOrder(before.workflows);
115
+ const afterOrdered = stableWorkflowOrder(after.workflows);
116
+ const beforeSet = new Set(beforeOrdered);
117
+ const afterSet = new Set(afterOrdered);
118
+ const added = afterOrdered.filter((w) => !beforeSet.has(w));
119
+ const removed = beforeOrdered.filter((w) => !afterSet.has(w));
120
+ if (added.length > 0 || removed.length > 0) {
121
+ const tokens = [];
122
+ if (added.length > 0) {
123
+ tokens.push(`added ${added.join(', ')}`);
124
+ }
125
+ if (removed.length > 0) {
126
+ tokens.push(`removed ${removed.join(', ')}`);
127
+ }
128
+ lines.push(`workflows: ${tokens.join('; ')}`);
129
+ }
130
+ return {
131
+ hasChanges: lines.length > 0,
132
+ lines,
133
+ };
134
+ }
135
+ function maybeWarnConfigDrift(projectDir, state, colorize) {
136
+ const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
137
+ if (!fs.existsSync(openspecDir)) {
138
+ return;
139
+ }
140
+ if (!hasProjectConfigDrift(projectDir, state.workflows, state.delivery)) {
141
+ return;
142
+ }
143
+ console.log(colorize('Warning: Global config is not applied to this project. Run `openspec update` to sync.'));
144
+ }
5
145
  /**
6
146
  * Register the config command and all its subcommands.
7
147
  *
@@ -37,7 +177,33 @@ export function registerConfigCommand(program) {
37
177
  console.log(JSON.stringify(config, null, 2));
38
178
  }
39
179
  else {
180
+ // Read raw config to determine which values are explicit vs defaults
181
+ const configPath = getGlobalConfigPath();
182
+ let rawConfig = {};
183
+ try {
184
+ if (fs.existsSync(configPath)) {
185
+ rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
186
+ }
187
+ }
188
+ catch {
189
+ // If reading fails, treat all as defaults
190
+ }
40
191
  console.log(formatValueYaml(config));
192
+ // Annotate profile settings
193
+ const profileSource = rawConfig.profile !== undefined ? '(explicit)' : '(default)';
194
+ const deliverySource = rawConfig.delivery !== undefined ? '(explicit)' : '(default)';
195
+ console.log(`\nProfile settings:`);
196
+ console.log(` profile: ${config.profile} ${profileSource}`);
197
+ console.log(` delivery: ${config.delivery} ${deliverySource}`);
198
+ if (config.profile === 'core') {
199
+ console.log(` workflows: ${CORE_WORKFLOWS.join(', ')} (from core profile)`);
200
+ }
201
+ else if (config.workflows && config.workflows.length > 0) {
202
+ console.log(` workflows: ${config.workflows.join(', ')} (explicit)`);
203
+ }
204
+ else {
205
+ console.log(` workflows: (none)`);
206
+ }
41
207
  }
42
208
  });
43
209
  // config get
@@ -123,10 +289,21 @@ export function registerConfigCommand(program) {
123
289
  }
124
290
  if (!options.yes) {
125
291
  const { confirm } = await import('@inquirer/prompts');
126
- const confirmed = await confirm({
127
- message: 'Reset all configuration to defaults?',
128
- default: false,
129
- });
292
+ let confirmed;
293
+ try {
294
+ confirmed = await confirm({
295
+ message: 'Reset all configuration to defaults?',
296
+ default: false,
297
+ });
298
+ }
299
+ catch (error) {
300
+ if (isPromptCancellationError(error)) {
301
+ console.log('Reset cancelled.');
302
+ process.exitCode = 130;
303
+ return;
304
+ }
305
+ throw error;
306
+ }
130
307
  if (!confirmed) {
131
308
  console.log('Reset cancelled.');
132
309
  return;
@@ -194,5 +371,182 @@ export function registerConfigCommand(program) {
194
371
  process.exitCode = 1;
195
372
  }
196
373
  });
374
+ // config profile [preset]
375
+ configCmd
376
+ .command('profile [preset]')
377
+ .description('Configure workflow profile (interactive picker or preset shortcut)')
378
+ .action(async (preset) => {
379
+ // Preset shortcut: `openspec config profile core`
380
+ if (preset === 'core') {
381
+ const config = getGlobalConfig();
382
+ config.profile = 'core';
383
+ config.workflows = [...CORE_WORKFLOWS];
384
+ // Preserve delivery setting
385
+ saveGlobalConfig(config);
386
+ console.log('Config updated. Run `openspec update` in your projects to apply.');
387
+ return;
388
+ }
389
+ if (preset) {
390
+ console.error(`Error: Unknown profile preset "${preset}". Available presets: core`);
391
+ process.exitCode = 1;
392
+ return;
393
+ }
394
+ // Non-interactive check
395
+ if (!process.stdout.isTTY) {
396
+ console.error('Interactive mode required. Use `openspec config profile core` or set config via environment/flags.');
397
+ process.exitCode = 1;
398
+ return;
399
+ }
400
+ // Interactive picker
401
+ const { select, checkbox, confirm } = await import('@inquirer/prompts');
402
+ const chalk = (await import('chalk')).default;
403
+ try {
404
+ const config = getGlobalConfig();
405
+ const currentState = resolveCurrentProfileState(config);
406
+ console.log(chalk.bold('\nCurrent profile settings'));
407
+ console.log(` Delivery: ${currentState.delivery}`);
408
+ console.log(` Workflows: ${formatWorkflowSummary(currentState.workflows, currentState.profile)}`);
409
+ console.log(chalk.dim(' Delivery = where workflows are installed (skills, commands, or both)'));
410
+ console.log(chalk.dim(' Workflows = which actions are available (propose, explore, apply, etc.)'));
411
+ console.log();
412
+ const action = await select({
413
+ message: 'What do you want to configure?',
414
+ choices: [
415
+ {
416
+ value: 'both',
417
+ name: 'Delivery and workflows',
418
+ description: 'Update install mode and available actions together',
419
+ },
420
+ {
421
+ value: 'delivery',
422
+ name: 'Delivery only',
423
+ description: 'Change where workflows are installed',
424
+ },
425
+ {
426
+ value: 'workflows',
427
+ name: 'Workflows only',
428
+ description: 'Change which workflow actions are available',
429
+ },
430
+ {
431
+ value: 'keep',
432
+ name: 'Keep current settings (exit)',
433
+ description: 'Leave configuration unchanged and exit',
434
+ },
435
+ ],
436
+ });
437
+ if (action === 'keep') {
438
+ console.log('No config changes.');
439
+ maybeWarnConfigDrift(process.cwd(), currentState, chalk.yellow);
440
+ return;
441
+ }
442
+ const nextState = {
443
+ profile: currentState.profile,
444
+ delivery: currentState.delivery,
445
+ workflows: [...currentState.workflows],
446
+ };
447
+ if (action === 'both' || action === 'delivery') {
448
+ const deliveryChoices = [
449
+ {
450
+ value: 'both',
451
+ name: 'Both (skills + commands)',
452
+ description: 'Install workflows as both skills and slash commands',
453
+ },
454
+ {
455
+ value: 'skills',
456
+ name: 'Skills only',
457
+ description: 'Install workflows only as skills',
458
+ },
459
+ {
460
+ value: 'commands',
461
+ name: 'Commands only',
462
+ description: 'Install workflows only as slash commands',
463
+ },
464
+ ];
465
+ for (const choice of deliveryChoices) {
466
+ if (choice.value === currentState.delivery) {
467
+ choice.name += ' [current]';
468
+ }
469
+ }
470
+ nextState.delivery = await select({
471
+ message: 'Delivery mode (how workflows are installed):',
472
+ choices: deliveryChoices,
473
+ default: currentState.delivery,
474
+ });
475
+ }
476
+ if (action === 'both' || action === 'workflows') {
477
+ const formatWorkflowChoice = (workflow) => {
478
+ const metadata = WORKFLOW_PROMPT_META[workflow] ?? {
479
+ name: workflow,
480
+ description: `Workflow: ${workflow}`,
481
+ };
482
+ return {
483
+ value: workflow,
484
+ name: metadata.name,
485
+ description: metadata.description,
486
+ short: metadata.name,
487
+ checked: currentState.workflows.includes(workflow),
488
+ };
489
+ };
490
+ const selectedWorkflows = await checkbox({
491
+ message: 'Select workflows to make available:',
492
+ instructions: 'Space to toggle, Enter to confirm',
493
+ pageSize: ALL_WORKFLOWS.length,
494
+ theme: {
495
+ icon: {
496
+ checked: '[x]',
497
+ unchecked: '[ ]',
498
+ },
499
+ },
500
+ choices: ALL_WORKFLOWS.map(formatWorkflowChoice),
501
+ });
502
+ nextState.workflows = selectedWorkflows;
503
+ nextState.profile = deriveProfileFromWorkflowSelection(selectedWorkflows);
504
+ }
505
+ const diff = diffProfileState(currentState, nextState);
506
+ if (!diff.hasChanges) {
507
+ console.log('No config changes.');
508
+ maybeWarnConfigDrift(process.cwd(), nextState, chalk.yellow);
509
+ return;
510
+ }
511
+ console.log(chalk.bold('\nConfig changes:'));
512
+ for (const line of diff.lines) {
513
+ console.log(` ${line}`);
514
+ }
515
+ console.log();
516
+ config.profile = nextState.profile;
517
+ config.delivery = nextState.delivery;
518
+ config.workflows = nextState.workflows;
519
+ saveGlobalConfig(config);
520
+ // Check if inside an OpenSpec project
521
+ const projectDir = process.cwd();
522
+ const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
523
+ if (fs.existsSync(openspecDir)) {
524
+ const applyNow = await confirm({
525
+ message: 'Apply changes to this project now?',
526
+ default: true,
527
+ });
528
+ if (applyNow) {
529
+ try {
530
+ execSync('npx openspec update', { stdio: 'inherit', cwd: projectDir });
531
+ console.log('Run `openspec update` in your other projects to apply.');
532
+ }
533
+ catch {
534
+ console.error('`openspec update` failed. Please run it manually to apply the profile changes.');
535
+ process.exitCode = 1;
536
+ }
537
+ return;
538
+ }
539
+ }
540
+ console.log('Config updated. Run `openspec update` in your projects to apply.');
541
+ }
542
+ catch (error) {
543
+ if (isPromptCancellationError(error)) {
544
+ console.log('Config profile cancelled.');
545
+ process.exitCode = 130;
546
+ return;
547
+ }
548
+ throw error;
549
+ }
550
+ });
197
551
  }
198
552
  //# sourceMappingURL=config.js.map
@@ -37,6 +37,11 @@ export declare function getStatusColor(status: 'done' | 'ready' | 'blocked'): (t
37
37
  * Gets the status indicator for an artifact.
38
38
  */
39
39
  export declare function getStatusIndicator(status: 'done' | 'ready' | 'blocked'): string;
40
+ /**
41
+ * Returns the list of available change directory names under openspec/changes/.
42
+ * Excludes the archive directory and hidden directories.
43
+ */
44
+ export declare function getAvailableChanges(projectRoot: string): Promise<string[]>;
40
45
  /**
41
46
  * Validates that a change exists and returns available changes if not.
42
47
  * Checks directory existence directly to support scaffolded changes (without proposal.md).
@@ -52,26 +52,31 @@ export function getStatusIndicator(status) {
52
52
  return color('[-]');
53
53
  }
54
54
  }
55
+ /**
56
+ * Returns the list of available change directory names under openspec/changes/.
57
+ * Excludes the archive directory and hidden directories.
58
+ */
59
+ export async function getAvailableChanges(projectRoot) {
60
+ const changesPath = path.join(projectRoot, 'openspec', 'changes');
61
+ try {
62
+ const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
63
+ return entries
64
+ .filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
65
+ .map((e) => e.name);
66
+ }
67
+ catch (error) {
68
+ if (error.code === 'ENOENT')
69
+ return [];
70
+ throw error;
71
+ }
72
+ }
55
73
  /**
56
74
  * Validates that a change exists and returns available changes if not.
57
75
  * Checks directory existence directly to support scaffolded changes (without proposal.md).
58
76
  */
59
77
  export async function validateChangeExists(changeName, projectRoot) {
60
- const changesPath = path.join(projectRoot, 'openspec', 'changes');
61
- // Get all change directories (not just those with proposal.md)
62
- const getAvailableChanges = async () => {
63
- try {
64
- const entries = await fs.promises.readdir(changesPath, { withFileTypes: true });
65
- return entries
66
- .filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
67
- .map((e) => e.name);
68
- }
69
- catch {
70
- return [];
71
- }
72
- };
73
78
  if (!changeName) {
74
- const available = await getAvailableChanges();
79
+ const available = await getAvailableChanges(projectRoot);
75
80
  if (available.length === 0) {
76
81
  throw new Error('No changes found. Create one with: openspec new change <name>');
77
82
  }
@@ -83,10 +88,10 @@ export async function validateChangeExists(changeName, projectRoot) {
83
88
  throw new Error(`Invalid change name '${changeName}': ${nameValidation.error}`);
84
89
  }
85
90
  // Check directory existence directly
86
- const changePath = path.join(changesPath, changeName);
91
+ const changePath = path.join(projectRoot, 'openspec', 'changes', changeName);
87
92
  const exists = fs.existsSync(changePath) && fs.statSync(changePath).isDirectory();
88
93
  if (!exists) {
89
- const available = await getAvailableChanges();
94
+ const available = await getAvailableChanges(projectRoot);
90
95
  if (available.length === 0) {
91
96
  throw new Error(`Change '${changeName}' not found. No changes exist. Create one with: openspec new change <name>`);
92
97
  }
@@ -6,7 +6,7 @@
6
6
  import ora from 'ora';
7
7
  import chalk from 'chalk';
8
8
  import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
9
- import { validateChangeExists, validateSchemaExists, getStatusIndicator, getStatusColor, } from './shared.js';
9
+ import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
10
10
  // -----------------------------------------------------------------------------
11
11
  // Command Implementation
12
12
  // -----------------------------------------------------------------------------
@@ -14,6 +14,23 @@ export async function statusCommand(options) {
14
14
  const spinner = ora('Loading change status...').start();
15
15
  try {
16
16
  const projectRoot = process.cwd();
17
+ // Handle no-changes case gracefully — status is informational,
18
+ // so "no changes" is a valid state, not an error.
19
+ if (!options.change) {
20
+ const available = await getAvailableChanges(projectRoot);
21
+ if (available.length === 0) {
22
+ spinner.stop();
23
+ if (options.json) {
24
+ console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
25
+ return;
26
+ }
27
+ console.log('No active changes. Create one with: openspec new change <name>');
28
+ return;
29
+ }
30
+ // Changes exist but --change not provided
31
+ spinner.stop();
32
+ throw new Error(`Missing required option --change. Available changes:\n ${available.join('\n ')}`);
33
+ }
17
34
  const changeName = await validateChangeExists(options.change, projectRoot);
18
35
  // Validate schema if explicitly provided
19
36
  if (options.schema) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Available Tools Detection
3
+ *
4
+ * Detects which AI tools are available in a project by scanning
5
+ * for their configuration directories.
6
+ */
7
+ import { type AIToolOption } from './config.js';
8
+ /**
9
+ * Scans the project path for AI tool configuration directories and returns
10
+ * the tools that are present.
11
+ *
12
+ * For tools with `detectionPaths`, checks those specific paths (files or
13
+ * directories). Otherwise checks for the tool's `skillsDir` directory at
14
+ * the project root. Only tools with a `skillsDir` property are considered.
15
+ */
16
+ export declare function getAvailableTools(projectPath: string): AIToolOption[];
17
+ //# sourceMappingURL=available-tools.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Available Tools Detection
3
+ *
4
+ * Detects which AI tools are available in a project by scanning
5
+ * for their configuration directories.
6
+ */
7
+ import path from 'path';
8
+ import * as fs from 'fs';
9
+ import { AI_TOOLS } from './config.js';
10
+ /**
11
+ * Scans the project path for AI tool configuration directories and returns
12
+ * the tools that are present.
13
+ *
14
+ * For tools with `detectionPaths`, checks those specific paths (files or
15
+ * directories). Otherwise checks for the tool's `skillsDir` directory at
16
+ * the project root. Only tools with a `skillsDir` property are considered.
17
+ */
18
+ export function getAvailableTools(projectPath) {
19
+ return AI_TOOLS.filter((tool) => {
20
+ if (!tool.skillsDir)
21
+ return false;
22
+ if (tool.detectionPaths && tool.detectionPaths.length > 0) {
23
+ // statSync without .isDirectory() — detection paths can be files or directories
24
+ return tool.detectionPaths.some((p) => {
25
+ try {
26
+ fs.statSync(path.join(projectPath, p));
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ });
33
+ }
34
+ const dirPath = path.join(projectPath, tool.skillsDir);
35
+ try {
36
+ return fs.statSync(dirPath).isDirectory();
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ });
42
+ }
43
+ //# sourceMappingURL=available-tools.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Bob Shell Command Adapter
3
+ *
4
+ * Formats commands for Bob Shell following its markdown specification.
5
+ * Commands are stored in .bob/commands/ directory.
6
+ */
7
+ import type { ToolCommandAdapter } from '../types.js';
8
+ /**
9
+ * Bob Shell adapter for command generation.
10
+ * File path: .bob/commands/opsx-<id>.md
11
+ * Frontmatter: description, argument-hint
12
+ */
13
+ export declare const bobAdapter: ToolCommandAdapter;
14
+ //# sourceMappingURL=bob.d.ts.map