@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.
- package/README.md +8 -9
- package/dist/cli/index.js +2 -0
- package/dist/commands/config.d.ts +28 -0
- package/dist/commands/config.js +359 -5
- package/dist/commands/workflow/shared.d.ts +5 -0
- package/dist/commands/workflow/shared.js +21 -16
- package/dist/commands/workflow/status.js +18 -1
- package/dist/core/available-tools.d.ts +17 -0
- package/dist/core/available-tools.js +43 -0
- package/dist/core/command-generation/adapters/bob.d.ts +14 -0
- package/dist/core/command-generation/adapters/bob.js +45 -0
- package/dist/core/command-generation/adapters/index.d.ts +5 -0
- package/dist/core/command-generation/adapters/index.js +5 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +26 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +26 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +1 -1
- package/dist/core/command-generation/adapters/opencode.js +2 -2
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +55 -0
- package/dist/core/command-generation/registry.js +10 -0
- package/dist/core/completions/command-registry.js +5 -0
- package/dist/core/completions/installers/powershell-installer.d.ts +14 -0
- package/dist/core/completions/installers/powershell-installer.js +69 -9
- package/dist/core/config-schema.d.ts +10 -0
- package/dist/core/config-schema.js +14 -1
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.js +7 -1
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/global-config.js +12 -2
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +209 -52
- package/dist/core/legacy-cleanup.d.ts +1 -1
- package/dist/core/legacy-cleanup.js +23 -10
- package/dist/core/migration.d.ts +23 -0
- package/dist/core/migration.js +108 -0
- package/dist/core/profile-sync-drift.d.ts +38 -0
- package/dist/core/profile-sync-drift.js +200 -0
- package/dist/core/profiles.d.ts +26 -0
- package/dist/core/profiles.js +40 -0
- package/dist/core/shared/index.d.ts +1 -1
- package/dist/core/shared/index.js +1 -1
- package/dist/core/shared/skill-generation.d.ts +14 -7
- package/dist/core/shared/skill-generation.js +36 -20
- package/dist/core/shared/tool-detection.d.ts +8 -3
- package/dist/core/shared/tool-detection.js +18 -0
- package/dist/core/templates/index.d.ts +1 -1
- package/dist/core/templates/index.js +2 -2
- package/dist/core/templates/skill-templates.d.ts +15 -118
- package/dist/core/templates/skill-templates.js +14 -3424
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +308 -0
- package/dist/core/templates/workflows/archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/archive-change.js +271 -0
- package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/bulk-archive-change.js +488 -0
- package/dist/core/templates/workflows/continue-change.d.ts +10 -0
- package/dist/core/templates/workflows/continue-change.js +232 -0
- package/dist/core/templates/workflows/explore.d.ts +10 -0
- package/dist/core/templates/workflows/explore.js +461 -0
- package/dist/core/templates/workflows/feedback.d.ts +9 -0
- package/dist/core/templates/workflows/feedback.js +108 -0
- package/dist/core/templates/workflows/ff-change.d.ts +10 -0
- package/dist/core/templates/workflows/ff-change.js +198 -0
- package/dist/core/templates/workflows/new-change.d.ts +10 -0
- package/dist/core/templates/workflows/new-change.js +143 -0
- package/dist/core/templates/workflows/onboard.d.ts +10 -0
- package/dist/core/templates/workflows/onboard.js +565 -0
- package/dist/core/templates/workflows/propose.d.ts +10 -0
- package/dist/core/templates/workflows/propose.js +216 -0
- package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
- package/dist/core/templates/workflows/sync-specs.js +272 -0
- package/dist/core/templates/workflows/verify-change.d.ts +10 -0
- package/dist/core/templates/workflows/verify-change.js +332 -0
- package/dist/core/update.d.ts +36 -1
- package/dist/core/update.js +291 -65
- package/dist/prompts/searchable-multi-select.d.ts +3 -2
- package/dist/prompts/searchable-multi-select.js +22 -12
- package/package.json +1 -1
- 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:
|
|
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:
|
|
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:
|
|
54
|
+
You: /opsx:propose add-dark-mode
|
|
55
55
|
AI: Created openspec/changes/add-dark-mode/
|
|
56
|
-
|
|
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:
|
|
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
|
|
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
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
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
|