@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/dist/core/init.d.ts
CHANGED
|
@@ -8,15 +8,18 @@ type InitCommandOptions = {
|
|
|
8
8
|
tools?: string;
|
|
9
9
|
force?: boolean;
|
|
10
10
|
interactive?: boolean;
|
|
11
|
+
profile?: string;
|
|
11
12
|
};
|
|
12
13
|
export declare class InitCommand {
|
|
13
14
|
private readonly toolsArg?;
|
|
14
15
|
private readonly force;
|
|
15
16
|
private readonly interactiveOption?;
|
|
17
|
+
private readonly profileOverride?;
|
|
16
18
|
constructor(options?: InitCommandOptions);
|
|
17
19
|
execute(targetPath: string): Promise<void>;
|
|
18
20
|
private validate;
|
|
19
21
|
private canPromptInteractively;
|
|
22
|
+
private resolveProfileOverride;
|
|
20
23
|
private handleLegacyCleanup;
|
|
21
24
|
private performLegacyCleanup;
|
|
22
25
|
private getSelectedTools;
|
|
@@ -27,6 +30,8 @@ export declare class InitCommand {
|
|
|
27
30
|
private createConfig;
|
|
28
31
|
private displaySuccessMessage;
|
|
29
32
|
private startSpinner;
|
|
33
|
+
private removeSkillDirs;
|
|
34
|
+
private removeCommandFiles;
|
|
30
35
|
}
|
|
31
36
|
export {};
|
|
32
37
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/core/init.js
CHANGED
|
@@ -18,6 +18,10 @@ import { serializeConfig } from './config-prompts.js';
|
|
|
18
18
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
19
19
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, } from './legacy-cleanup.js';
|
|
20
20
|
import { getToolsWithSkillsDir, getToolStates, getSkillTemplates, getCommandContents, generateSkillContent, } from './shared/index.js';
|
|
21
|
+
import { getGlobalConfig } from './global-config.js';
|
|
22
|
+
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
23
|
+
import { getAvailableTools } from './available-tools.js';
|
|
24
|
+
import { migrateIfNeeded } from './migration.js';
|
|
21
25
|
const require = createRequire(import.meta.url);
|
|
22
26
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
23
27
|
// -----------------------------------------------------------------------------
|
|
@@ -28,6 +32,19 @@ const PROGRESS_SPINNER = {
|
|
|
28
32
|
interval: 80,
|
|
29
33
|
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
|
30
34
|
};
|
|
35
|
+
const WORKFLOW_TO_SKILL_DIR = {
|
|
36
|
+
'explore': 'openspec-explore',
|
|
37
|
+
'new': 'openspec-new-change',
|
|
38
|
+
'continue': 'openspec-continue-change',
|
|
39
|
+
'apply': 'openspec-apply-change',
|
|
40
|
+
'ff': 'openspec-ff-change',
|
|
41
|
+
'sync': 'openspec-sync-specs',
|
|
42
|
+
'archive': 'openspec-archive-change',
|
|
43
|
+
'bulk-archive': 'openspec-bulk-archive-change',
|
|
44
|
+
'verify': 'openspec-verify-change',
|
|
45
|
+
'onboard': 'openspec-onboard',
|
|
46
|
+
'propose': 'openspec-propose',
|
|
47
|
+
};
|
|
31
48
|
// -----------------------------------------------------------------------------
|
|
32
49
|
// Init Command Class
|
|
33
50
|
// -----------------------------------------------------------------------------
|
|
@@ -35,10 +52,12 @@ export class InitCommand {
|
|
|
35
52
|
toolsArg;
|
|
36
53
|
force;
|
|
37
54
|
interactiveOption;
|
|
55
|
+
profileOverride;
|
|
38
56
|
constructor(options = {}) {
|
|
39
57
|
this.toolsArg = options.tools;
|
|
40
58
|
this.force = options.force ?? false;
|
|
41
59
|
this.interactiveOption = options.interactive;
|
|
60
|
+
this.profileOverride = options.profile;
|
|
42
61
|
}
|
|
43
62
|
async execute(targetPath) {
|
|
44
63
|
const projectPath = path.resolve(targetPath);
|
|
@@ -48,16 +67,25 @@ export class InitCommand {
|
|
|
48
67
|
const extendMode = await this.validate(projectPath, openspecPath);
|
|
49
68
|
// Check for legacy artifacts and handle cleanup
|
|
50
69
|
await this.handleLegacyCleanup(projectPath, extendMode);
|
|
70
|
+
// Detect available tools in the project (task 7.1)
|
|
71
|
+
const detectedTools = getAvailableTools(projectPath);
|
|
72
|
+
// Migration check: migrate existing projects to profile system (task 7.3)
|
|
73
|
+
if (extendMode) {
|
|
74
|
+
migrateIfNeeded(projectPath, detectedTools);
|
|
75
|
+
}
|
|
51
76
|
// Show animated welcome screen (interactive mode only)
|
|
52
77
|
const canPrompt = this.canPromptInteractively();
|
|
53
78
|
if (canPrompt) {
|
|
54
79
|
const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
|
|
55
80
|
await showWelcomeScreen();
|
|
56
81
|
}
|
|
82
|
+
// Validate profile override early so invalid values fail before tool setup.
|
|
83
|
+
// The resolved value is consumed later when generation reads effective config.
|
|
84
|
+
this.resolveProfileOverride();
|
|
57
85
|
// Get tool states before processing
|
|
58
86
|
const toolStates = getToolStates(projectPath);
|
|
59
|
-
// Get tool selection
|
|
60
|
-
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode);
|
|
87
|
+
// Get tool selection (pass detected tools for pre-selection)
|
|
88
|
+
const selectedToolIds = await this.getSelectedTools(toolStates, extendMode, detectedTools, projectPath);
|
|
61
89
|
// Validate selected tools
|
|
62
90
|
const validatedTools = this.validateTools(selectedToolIds, toolStates);
|
|
63
91
|
// Create directory structure and config
|
|
@@ -87,6 +115,15 @@ export class InitCommand {
|
|
|
87
115
|
return false;
|
|
88
116
|
return isInteractive({ interactive: this.interactiveOption });
|
|
89
117
|
}
|
|
118
|
+
resolveProfileOverride() {
|
|
119
|
+
if (this.profileOverride === undefined) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
if (this.profileOverride === 'core' || this.profileOverride === 'custom') {
|
|
123
|
+
return this.profileOverride;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`Invalid profile "${this.profileOverride}". Available profiles: core, custom`);
|
|
126
|
+
}
|
|
90
127
|
// ═══════════════════════════════════════════════════════════
|
|
91
128
|
// LEGACY CLEANUP
|
|
92
129
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -101,17 +138,13 @@ export class InitCommand {
|
|
|
101
138
|
console.log(formatDetectionSummary(detection));
|
|
102
139
|
console.log();
|
|
103
140
|
const canPrompt = this.canPromptInteractively();
|
|
104
|
-
if (this.force) {
|
|
105
|
-
// --force flag: proceed with cleanup automatically
|
|
141
|
+
if (this.force || !canPrompt) {
|
|
142
|
+
// --force flag or non-interactive mode: proceed with cleanup automatically.
|
|
143
|
+
// Legacy slash commands are 100% OpenSpec-managed, and config file cleanup
|
|
144
|
+
// only removes markers (never deletes files), so auto-cleanup is safe.
|
|
106
145
|
await this.performLegacyCleanup(projectPath, detection);
|
|
107
146
|
return;
|
|
108
147
|
}
|
|
109
|
-
if (!canPrompt) {
|
|
110
|
-
// Non-interactive mode without --force: abort
|
|
111
|
-
console.log(chalk.red('Legacy files detected in non-interactive mode.'));
|
|
112
|
-
console.log(chalk.dim('Run interactively to upgrade, or use --force to auto-cleanup.'));
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
148
|
// Interactive mode: prompt for confirmation
|
|
116
149
|
const { confirm } = await import('@inquirer/prompts');
|
|
117
150
|
const shouldCleanup = await confirm({
|
|
@@ -139,40 +172,73 @@ export class InitCommand {
|
|
|
139
172
|
// ═══════════════════════════════════════════════════════════
|
|
140
173
|
// TOOL SELECTION
|
|
141
174
|
// ═══════════════════════════════════════════════════════════
|
|
142
|
-
async getSelectedTools(toolStates, extendMode) {
|
|
175
|
+
async getSelectedTools(toolStates, extendMode, detectedTools, projectPath) {
|
|
143
176
|
// Check for --tools flag first
|
|
144
177
|
const nonInteractiveSelection = this.resolveToolsArg();
|
|
145
178
|
if (nonInteractiveSelection !== null) {
|
|
146
179
|
return nonInteractiveSelection;
|
|
147
180
|
}
|
|
148
181
|
const validTools = getToolsWithSkillsDir();
|
|
182
|
+
const detectedToolIds = new Set(detectedTools.map((t) => t.value));
|
|
183
|
+
const configuredToolIds = new Set([...toolStates.entries()]
|
|
184
|
+
.filter(([, status]) => status.configured)
|
|
185
|
+
.map(([toolId]) => toolId));
|
|
186
|
+
const shouldPreselectDetected = !extendMode && configuredToolIds.size === 0;
|
|
149
187
|
const canPrompt = this.canPromptInteractively();
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
// Non-interactive mode: use detected tools as fallback (task 7.8)
|
|
189
|
+
if (!canPrompt) {
|
|
190
|
+
if (detectedToolIds.size > 0) {
|
|
191
|
+
return [...detectedToolIds];
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`No tools detected and no --tools flag provided. Valid tools:\n ${validTools.join('\n ')}\n\nUse --tools all, --tools none, or --tools claude,cursor,...`);
|
|
194
|
+
}
|
|
195
|
+
if (validTools.length === 0) {
|
|
196
|
+
throw new Error(`No tools available for skill generation.`);
|
|
152
197
|
}
|
|
153
198
|
// Interactive mode: show searchable multi-select
|
|
154
199
|
const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
|
|
155
|
-
// Build choices
|
|
200
|
+
// Build choices: pre-select configured tools; keep detected tools visible but unselected.
|
|
156
201
|
const sortedChoices = validTools
|
|
157
202
|
.map((toolId) => {
|
|
158
203
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
159
204
|
const status = toolStates.get(toolId);
|
|
160
205
|
const configured = status?.configured ?? false;
|
|
206
|
+
const detected = detectedToolIds.has(toolId);
|
|
161
207
|
return {
|
|
162
208
|
name: tool?.name || toolId,
|
|
163
209
|
value: toolId,
|
|
164
210
|
configured,
|
|
165
|
-
|
|
211
|
+
detected: detected && !configured,
|
|
212
|
+
preSelected: configured || (shouldPreselectDetected && detected && !configured),
|
|
166
213
|
};
|
|
167
214
|
})
|
|
168
215
|
.sort((a, b) => {
|
|
169
|
-
// Configured tools first
|
|
216
|
+
// Configured tools first, then detected (not configured), then everything else.
|
|
170
217
|
if (a.configured && !b.configured)
|
|
171
218
|
return -1;
|
|
172
219
|
if (!a.configured && b.configured)
|
|
173
220
|
return 1;
|
|
221
|
+
if (a.detected && !b.detected)
|
|
222
|
+
return -1;
|
|
223
|
+
if (!a.detected && b.detected)
|
|
224
|
+
return 1;
|
|
174
225
|
return 0;
|
|
175
226
|
});
|
|
227
|
+
const configuredNames = validTools
|
|
228
|
+
.filter((toolId) => configuredToolIds.has(toolId))
|
|
229
|
+
.map((toolId) => AI_TOOLS.find((t) => t.value === toolId)?.name || toolId);
|
|
230
|
+
if (configuredNames.length > 0) {
|
|
231
|
+
console.log(`OpenSpec configured: ${configuredNames.join(', ')} (pre-selected)`);
|
|
232
|
+
}
|
|
233
|
+
const detectedOnlyNames = detectedTools
|
|
234
|
+
.filter((tool) => !configuredToolIds.has(tool.value))
|
|
235
|
+
.map((tool) => tool.name);
|
|
236
|
+
if (detectedOnlyNames.length > 0) {
|
|
237
|
+
const detectionLabel = shouldPreselectDetected
|
|
238
|
+
? 'pre-selected for first-time setup'
|
|
239
|
+
: 'not pre-selected';
|
|
240
|
+
console.log(`Detected tool directories: ${detectedOnlyNames.join(', ')} (${detectionLabel})`);
|
|
241
|
+
}
|
|
176
242
|
const selectedTools = await searchableMultiSelect({
|
|
177
243
|
message: `Select tools to set up (${validTools.length} available)`,
|
|
178
244
|
pageSize: 15,
|
|
@@ -288,37 +354,58 @@ export class InitCommand {
|
|
|
288
354
|
const refreshedTools = [];
|
|
289
355
|
const failedTools = [];
|
|
290
356
|
const commandsSkipped = [];
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
357
|
+
let removedCommandCount = 0;
|
|
358
|
+
let removedSkillCount = 0;
|
|
359
|
+
// Read global config for profile and delivery settings (use --profile override if set)
|
|
360
|
+
const globalConfig = getGlobalConfig();
|
|
361
|
+
const profile = this.resolveProfileOverride() ?? globalConfig.profile ?? 'core';
|
|
362
|
+
const delivery = globalConfig.delivery ?? 'both';
|
|
363
|
+
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
364
|
+
// Get skill and command templates filtered by profile workflows
|
|
365
|
+
const shouldGenerateSkills = delivery !== 'commands';
|
|
366
|
+
const shouldGenerateCommands = delivery !== 'skills';
|
|
367
|
+
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(workflows) : [];
|
|
368
|
+
const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : [];
|
|
294
369
|
// Process each tool
|
|
295
370
|
for (const tool of tools) {
|
|
296
371
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
297
372
|
try {
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
373
|
+
// Generate skill files if delivery includes skills
|
|
374
|
+
if (shouldGenerateSkills) {
|
|
375
|
+
// Use tool-specific skillsDir
|
|
376
|
+
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
377
|
+
// Create skill directories and SKILL.md files
|
|
378
|
+
for (const { template, dirName } of skillTemplates) {
|
|
379
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
380
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
381
|
+
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
382
|
+
// Use hyphen-based command references for tools where filename = command name
|
|
383
|
+
const transformer = (tool.value === 'opencode' || tool.value === 'pi') ? transformToHyphenCommands : undefined;
|
|
384
|
+
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
385
|
+
// Write the skill file
|
|
386
|
+
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
387
|
+
}
|
|
310
388
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
389
|
+
if (!shouldGenerateSkills) {
|
|
390
|
+
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
391
|
+
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
392
|
+
}
|
|
393
|
+
// Generate commands if delivery includes commands
|
|
394
|
+
if (shouldGenerateCommands) {
|
|
395
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
396
|
+
if (adapter) {
|
|
397
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
398
|
+
for (const cmd of generatedCommands) {
|
|
399
|
+
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
|
|
400
|
+
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
commandsSkipped.push(tool.value);
|
|
318
405
|
}
|
|
319
406
|
}
|
|
320
|
-
|
|
321
|
-
|
|
407
|
+
if (!shouldGenerateCommands) {
|
|
408
|
+
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
|
|
322
409
|
}
|
|
323
410
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
324
411
|
if (tool.wasConfigured) {
|
|
@@ -333,7 +420,14 @@ export class InitCommand {
|
|
|
333
420
|
failedTools.push({ name: tool.name, error: error });
|
|
334
421
|
}
|
|
335
422
|
}
|
|
336
|
-
return {
|
|
423
|
+
return {
|
|
424
|
+
createdTools,
|
|
425
|
+
refreshedTools,
|
|
426
|
+
failedTools,
|
|
427
|
+
commandsSkipped,
|
|
428
|
+
removedCommandCount,
|
|
429
|
+
removedSkillCount,
|
|
430
|
+
};
|
|
337
431
|
}
|
|
338
432
|
// ═══════════════════════════════════════════════════════════
|
|
339
433
|
// CONFIG FILE
|
|
@@ -373,16 +467,24 @@ export class InitCommand {
|
|
|
373
467
|
if (results.refreshedTools.length > 0) {
|
|
374
468
|
console.log(`Refreshed: ${results.refreshedTools.map((t) => t.name).join(', ')}`);
|
|
375
469
|
}
|
|
376
|
-
// Show counts
|
|
470
|
+
// Show counts (respecting profile filter)
|
|
377
471
|
const successfulTools = [...results.createdTools, ...results.refreshedTools];
|
|
378
472
|
if (successfulTools.length > 0) {
|
|
473
|
+
const globalConfig = getGlobalConfig();
|
|
474
|
+
const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
|
|
475
|
+
const delivery = globalConfig.delivery ?? 'both';
|
|
476
|
+
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
379
477
|
const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', ');
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
478
|
+
const skillCount = delivery !== 'commands' ? getSkillTemplates(workflows).length : 0;
|
|
479
|
+
const commandCount = delivery !== 'skills' ? getCommandContents(workflows).length : 0;
|
|
480
|
+
if (skillCount > 0 && commandCount > 0) {
|
|
481
|
+
console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
|
|
482
|
+
}
|
|
483
|
+
else if (skillCount > 0) {
|
|
484
|
+
console.log(`${skillCount} skills in ${toolDirs}/`);
|
|
383
485
|
}
|
|
384
|
-
else {
|
|
385
|
-
console.log(`${
|
|
486
|
+
else if (commandCount > 0) {
|
|
487
|
+
console.log(`${commandCount} commands in ${toolDirs}/`);
|
|
386
488
|
}
|
|
387
489
|
}
|
|
388
490
|
// Show failures
|
|
@@ -393,6 +495,12 @@ export class InitCommand {
|
|
|
393
495
|
if (results.commandsSkipped.length > 0) {
|
|
394
496
|
console.log(chalk.dim(`Commands skipped for: ${results.commandsSkipped.join(', ')} (no adapter)`));
|
|
395
497
|
}
|
|
498
|
+
if (results.removedCommandCount > 0) {
|
|
499
|
+
console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`));
|
|
500
|
+
}
|
|
501
|
+
if (results.removedSkillCount > 0) {
|
|
502
|
+
console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
|
|
503
|
+
}
|
|
396
504
|
// Config status
|
|
397
505
|
if (configStatus === 'created') {
|
|
398
506
|
console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
|
|
@@ -407,12 +515,22 @@ export class InitCommand {
|
|
|
407
515
|
else {
|
|
408
516
|
console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
|
|
409
517
|
}
|
|
410
|
-
// Getting started
|
|
518
|
+
// Getting started (task 7.6: show propose if in profile)
|
|
519
|
+
const globalCfg = getGlobalConfig();
|
|
520
|
+
const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
|
|
521
|
+
const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
|
|
411
522
|
console.log();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
523
|
+
if (activeWorkflows.includes('propose')) {
|
|
524
|
+
console.log(chalk.bold('Getting started:'));
|
|
525
|
+
console.log(' Start your first change: /opsx:propose "your idea"');
|
|
526
|
+
}
|
|
527
|
+
else if (activeWorkflows.includes('new')) {
|
|
528
|
+
console.log(chalk.bold('Getting started:'));
|
|
529
|
+
console.log(' Start your first change: /opsx:new "your idea"');
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.log("Done. Run 'openspec config profile' to configure your workflows.");
|
|
533
|
+
}
|
|
416
534
|
// Links
|
|
417
535
|
console.log();
|
|
418
536
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
@@ -432,5 +550,44 @@ export class InitCommand {
|
|
|
432
550
|
spinner: PROGRESS_SPINNER,
|
|
433
551
|
}).start();
|
|
434
552
|
}
|
|
553
|
+
async removeSkillDirs(skillsDir) {
|
|
554
|
+
let removed = 0;
|
|
555
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
556
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
557
|
+
if (!dirName)
|
|
558
|
+
continue;
|
|
559
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
560
|
+
try {
|
|
561
|
+
if (fs.existsSync(skillDir)) {
|
|
562
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
563
|
+
removed++;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
// Ignore errors
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return removed;
|
|
571
|
+
}
|
|
572
|
+
async removeCommandFiles(projectPath, toolId) {
|
|
573
|
+
let removed = 0;
|
|
574
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
575
|
+
if (!adapter)
|
|
576
|
+
return 0;
|
|
577
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
578
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
579
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
580
|
+
try {
|
|
581
|
+
if (fs.existsSync(fullPath)) {
|
|
582
|
+
await fs.promises.unlink(fullPath);
|
|
583
|
+
removed++;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Ignore errors
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return removed;
|
|
591
|
+
}
|
|
435
592
|
}
|
|
436
593
|
//# sourceMappingURL=init.js.map
|
|
@@ -19,7 +19,7 @@ export declare const LEGACY_SLASH_COMMAND_PATHS: Record<string, LegacySlashComma
|
|
|
19
19
|
export interface LegacySlashCommandPattern {
|
|
20
20
|
type: 'directory' | 'files';
|
|
21
21
|
path?: string;
|
|
22
|
-
pattern?: string;
|
|
22
|
+
pattern?: string | string[];
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Result of legacy artifact detection
|
|
@@ -30,6 +30,7 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
30
30
|
'claude': { type: 'directory', path: '.claude/commands/openspec' },
|
|
31
31
|
'codebuddy': { type: 'directory', path: '.codebuddy/commands/openspec' },
|
|
32
32
|
'qoder': { type: 'directory', path: '.qoder/commands/openspec' },
|
|
33
|
+
'lingma': { type: 'directory', path: '.lingma/commands/openspec' },
|
|
33
34
|
'crush': { type: 'directory', path: '.crush/commands/openspec' },
|
|
34
35
|
'gemini': { type: 'directory', path: '.gemini/commands/openspec' },
|
|
35
36
|
'costrict': { type: 'directory', path: '.cospec/openspec/commands' },
|
|
@@ -37,16 +38,18 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
37
38
|
'cursor': { type: 'files', pattern: '.cursor/commands/openspec-*.md' },
|
|
38
39
|
'windsurf': { type: 'files', pattern: '.windsurf/workflows/openspec-*.md' },
|
|
39
40
|
'kilocode': { type: 'files', pattern: '.kilocode/workflows/openspec-*.md' },
|
|
41
|
+
'kiro': { type: 'files', pattern: '.kiro/prompts/openspec-*.prompt.md' },
|
|
40
42
|
'github-copilot': { type: 'files', pattern: '.github/prompts/openspec-*.prompt.md' },
|
|
41
43
|
'amazon-q': { type: 'files', pattern: '.amazonq/prompts/openspec-*.md' },
|
|
42
44
|
'cline': { type: 'files', pattern: '.clinerules/workflows/openspec-*.md' },
|
|
43
45
|
'roocode': { type: 'files', pattern: '.roo/commands/openspec-*.md' },
|
|
44
46
|
'auggie': { type: 'files', pattern: '.augment/commands/openspec-*.md' },
|
|
45
47
|
'factory': { type: 'files', pattern: '.factory/commands/openspec-*.md' },
|
|
46
|
-
'opencode': { type: 'files', pattern: '.opencode/command/openspec-*.md' },
|
|
48
|
+
'opencode': { type: 'files', pattern: ['.opencode/command/opsx-*.md', '.opencode/command/openspec-*.md'] },
|
|
47
49
|
'continue': { type: 'files', pattern: '.continue/prompts/openspec-*.prompt' },
|
|
48
50
|
'antigravity': { type: 'files', pattern: '.agent/workflows/openspec-*.md' },
|
|
49
51
|
'iflow': { type: 'files', pattern: '.iflow/commands/openspec-*.md' },
|
|
52
|
+
'junie': { type: 'files', pattern: ['.junie/commands/opsx-*.md', '.junie/commands/openspec-*.md'] },
|
|
50
53
|
'qwen': { type: 'files', pattern: '.qwen/commands/openspec-*.toml' },
|
|
51
54
|
'codex': { type: 'files', pattern: '.codex/prompts/openspec-*.md' },
|
|
52
55
|
};
|
|
@@ -131,8 +134,11 @@ export async function detectLegacySlashCommands(projectPath) {
|
|
|
131
134
|
}
|
|
132
135
|
else if (pattern.type === 'files' && pattern.pattern) {
|
|
133
136
|
// For file-based patterns, check for individual files
|
|
134
|
-
const
|
|
135
|
-
|
|
137
|
+
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
138
|
+
for (const p of patterns) {
|
|
139
|
+
const foundFiles = await findLegacySlashCommandFiles(projectPath, p);
|
|
140
|
+
files.push(...foundFiles);
|
|
141
|
+
}
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
return { directories, files };
|
|
@@ -465,14 +471,21 @@ export function getToolsFromLegacyArtifacts(detection) {
|
|
|
465
471
|
if (pattern.type === 'files' && pattern.pattern) {
|
|
466
472
|
// Convert glob pattern to regex for matching
|
|
467
473
|
// e.g., '.cursor/commands/openspec-*.md' -> /^\.cursor\/commands\/openspec-.*\.md$/
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
474
|
+
const patterns = Array.isArray(pattern.pattern) ? pattern.pattern : [pattern.pattern];
|
|
475
|
+
let matched = false;
|
|
476
|
+
for (const p of patterns) {
|
|
477
|
+
const regexPattern = p
|
|
478
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
479
|
+
.replace(/\*/g, '.*'); // Replace * with .*
|
|
480
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
481
|
+
if (regex.test(normalizedFile)) {
|
|
482
|
+
tools.add(toolId);
|
|
483
|
+
matched = true;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
475
486
|
}
|
|
487
|
+
if (matched)
|
|
488
|
+
break;
|
|
476
489
|
}
|
|
477
490
|
}
|
|
478
491
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Utilities
|
|
3
|
+
*
|
|
4
|
+
* One-time migration logic for existing projects when profile system is introduced.
|
|
5
|
+
* Called by both init and update commands before profile resolution.
|
|
6
|
+
*/
|
|
7
|
+
import type { AIToolOption } from './config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Scans installed workflow files across all detected tools and returns
|
|
10
|
+
* the union of installed workflow IDs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function scanInstalledWorkflows(projectPath: string, tools: AIToolOption[]): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Performs one-time migration if the global config does not yet have a profile field.
|
|
15
|
+
* Called by both init and update before profile resolution.
|
|
16
|
+
*
|
|
17
|
+
* - If no profile field exists and workflows are installed: sets profile to 'custom'
|
|
18
|
+
* with the detected workflows, preserving the user's existing setup.
|
|
19
|
+
* - If no profile field exists and no workflows are installed: no-op (defaults apply).
|
|
20
|
+
* - If profile field already exists: no-op.
|
|
21
|
+
*/
|
|
22
|
+
export declare function migrateIfNeeded(projectPath: string, tools: AIToolOption[]): void;
|
|
23
|
+
//# sourceMappingURL=migration.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Utilities
|
|
3
|
+
*
|
|
4
|
+
* One-time migration logic for existing projects when profile system is introduced.
|
|
5
|
+
* Called by both init and update commands before profile resolution.
|
|
6
|
+
*/
|
|
7
|
+
import { getGlobalConfig, getGlobalConfigPath, saveGlobalConfig } from './global-config.js';
|
|
8
|
+
import { CommandAdapterRegistry } from './command-generation/index.js';
|
|
9
|
+
import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
|
|
10
|
+
import { ALL_WORKFLOWS } from './profiles.js';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
function scanInstalledWorkflowArtifacts(projectPath, tools) {
|
|
14
|
+
const installed = new Set();
|
|
15
|
+
let hasSkills = false;
|
|
16
|
+
let hasCommands = false;
|
|
17
|
+
for (const tool of tools) {
|
|
18
|
+
if (!tool.skillsDir)
|
|
19
|
+
continue;
|
|
20
|
+
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
21
|
+
for (const workflowId of ALL_WORKFLOWS) {
|
|
22
|
+
const skillDirName = WORKFLOW_TO_SKILL_DIR[workflowId];
|
|
23
|
+
const skillFile = path.join(skillsDir, skillDirName, 'SKILL.md');
|
|
24
|
+
if (fs.existsSync(skillFile)) {
|
|
25
|
+
installed.add(workflowId);
|
|
26
|
+
hasSkills = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
30
|
+
if (!adapter)
|
|
31
|
+
continue;
|
|
32
|
+
for (const workflowId of ALL_WORKFLOWS) {
|
|
33
|
+
const commandPath = adapter.getFilePath(workflowId);
|
|
34
|
+
const fullPath = path.isAbsolute(commandPath)
|
|
35
|
+
? commandPath
|
|
36
|
+
: path.join(projectPath, commandPath);
|
|
37
|
+
if (fs.existsSync(fullPath)) {
|
|
38
|
+
installed.add(workflowId);
|
|
39
|
+
hasCommands = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
workflows: ALL_WORKFLOWS.filter((workflowId) => installed.has(workflowId)),
|
|
45
|
+
hasSkills,
|
|
46
|
+
hasCommands,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Scans installed workflow files across all detected tools and returns
|
|
51
|
+
* the union of installed workflow IDs.
|
|
52
|
+
*/
|
|
53
|
+
export function scanInstalledWorkflows(projectPath, tools) {
|
|
54
|
+
return scanInstalledWorkflowArtifacts(projectPath, tools).workflows;
|
|
55
|
+
}
|
|
56
|
+
function inferDelivery(artifacts) {
|
|
57
|
+
if (artifacts.hasSkills && artifacts.hasCommands) {
|
|
58
|
+
return 'both';
|
|
59
|
+
}
|
|
60
|
+
if (artifacts.hasCommands) {
|
|
61
|
+
return 'commands';
|
|
62
|
+
}
|
|
63
|
+
return 'skills';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Performs one-time migration if the global config does not yet have a profile field.
|
|
67
|
+
* Called by both init and update before profile resolution.
|
|
68
|
+
*
|
|
69
|
+
* - If no profile field exists and workflows are installed: sets profile to 'custom'
|
|
70
|
+
* with the detected workflows, preserving the user's existing setup.
|
|
71
|
+
* - If no profile field exists and no workflows are installed: no-op (defaults apply).
|
|
72
|
+
* - If profile field already exists: no-op.
|
|
73
|
+
*/
|
|
74
|
+
export function migrateIfNeeded(projectPath, tools) {
|
|
75
|
+
const config = getGlobalConfig();
|
|
76
|
+
// Check raw config file for profile field presence
|
|
77
|
+
const configPath = getGlobalConfigPath();
|
|
78
|
+
let rawConfig = {};
|
|
79
|
+
try {
|
|
80
|
+
if (fs.existsSync(configPath)) {
|
|
81
|
+
rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return; // Can't read config, skip migration
|
|
86
|
+
}
|
|
87
|
+
// If profile is already explicitly set, no migration needed
|
|
88
|
+
if (rawConfig.profile !== undefined) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Scan for installed workflows
|
|
92
|
+
const artifacts = scanInstalledWorkflowArtifacts(projectPath, tools);
|
|
93
|
+
const installedWorkflows = artifacts.workflows;
|
|
94
|
+
if (installedWorkflows.length === 0) {
|
|
95
|
+
// No workflows installed, new user — defaults will apply
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Migrate: set profile to custom with detected workflows
|
|
99
|
+
config.profile = 'custom';
|
|
100
|
+
config.workflows = installedWorkflows;
|
|
101
|
+
if (rawConfig.delivery === undefined) {
|
|
102
|
+
config.delivery = inferDelivery(artifacts);
|
|
103
|
+
}
|
|
104
|
+
saveGlobalConfig(config);
|
|
105
|
+
console.log(`Migrated: custom profile with ${installedWorkflows.length} workflows`);
|
|
106
|
+
console.log("New in this version: /opsx:propose. Try 'openspec config profile core' for the streamlined experience.");
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=migration.js.map
|