@fission-ai/openspec 1.1.1 → 1.2.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/dist/cli/index.js +2 -0
- package/dist/commands/config.d.ts +28 -0
- package/dist/commands/config.js +359 -5
- package/dist/core/available-tools.d.ts +16 -0
- package/dist/core/available-tools.js +30 -0
- package/dist/core/command-generation/adapters/index.d.ts +2 -0
- package/dist/core/command-generation/adapters/index.js +2 -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/pi.d.ts +14 -0
- package/dist/core/command-generation/adapters/pi.js +41 -0
- package/dist/core/command-generation/registry.js +4 -0
- package/dist/core/completions/command-registry.js +5 -0
- package/dist/core/config-schema.d.ts +10 -0
- package/dist/core/config-schema.js +14 -1
- package/dist/core/config.js +2 -0
- 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 +205 -44
- package/dist/core/legacy-cleanup.js +1 -0
- 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
|
@@ -9,6 +9,17 @@ export const GlobalConfigSchema = z
|
|
|
9
9
|
.record(z.string(), z.boolean())
|
|
10
10
|
.optional()
|
|
11
11
|
.default({}),
|
|
12
|
+
profile: z
|
|
13
|
+
.enum(['core', 'custom'])
|
|
14
|
+
.optional()
|
|
15
|
+
.default('core'),
|
|
16
|
+
delivery: z
|
|
17
|
+
.enum(['both', 'skills', 'commands'])
|
|
18
|
+
.optional()
|
|
19
|
+
.default('both'),
|
|
20
|
+
workflows: z
|
|
21
|
+
.array(z.string())
|
|
22
|
+
.optional(),
|
|
12
23
|
})
|
|
13
24
|
.passthrough();
|
|
14
25
|
/**
|
|
@@ -16,8 +27,10 @@ export const GlobalConfigSchema = z
|
|
|
16
27
|
*/
|
|
17
28
|
export const DEFAULT_CONFIG = {
|
|
18
29
|
featureFlags: {},
|
|
30
|
+
profile: 'core',
|
|
31
|
+
delivery: 'both',
|
|
19
32
|
};
|
|
20
|
-
const KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(DEFAULT_CONFIG));
|
|
33
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set([...Object.keys(DEFAULT_CONFIG), 'workflows']);
|
|
21
34
|
/**
|
|
22
35
|
* Validate a config key path for CLI set operations.
|
|
23
36
|
* Unknown top-level keys are rejected unless explicitly allowed by the caller.
|
package/dist/core/config.js
CHANGED
|
@@ -20,7 +20,9 @@ export const AI_TOOLS = [
|
|
|
20
20
|
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github' },
|
|
21
21
|
{ name: 'iFlow', value: 'iflow', available: true, successLabel: 'iFlow', skillsDir: '.iflow' },
|
|
22
22
|
{ name: 'Kilo Code', value: 'kilocode', available: true, successLabel: 'Kilo Code', skillsDir: '.kilocode' },
|
|
23
|
+
{ name: 'Kiro', value: 'kiro', available: true, successLabel: 'Kiro', skillsDir: '.kiro' },
|
|
23
24
|
{ name: 'OpenCode', value: 'opencode', available: true, successLabel: 'OpenCode', skillsDir: '.opencode' },
|
|
25
|
+
{ name: 'Pi', value: 'pi', available: true, successLabel: 'Pi', skillsDir: '.pi' },
|
|
24
26
|
{ name: 'Qoder', value: 'qoder', available: true, successLabel: 'Qoder', skillsDir: '.qoder' },
|
|
25
27
|
{ name: 'Qwen Code', value: 'qwen', available: true, successLabel: 'Qwen Code', skillsDir: '.qwen' },
|
|
26
28
|
{ name: 'RooCode', value: 'roocode', available: true, successLabel: 'RooCode', skillsDir: '.roo' },
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export declare const GLOBAL_CONFIG_DIR_NAME = "openspec";
|
|
2
2
|
export declare const GLOBAL_CONFIG_FILE_NAME = "config.json";
|
|
3
3
|
export declare const GLOBAL_DATA_DIR_NAME = "openspec";
|
|
4
|
+
export type Profile = 'core' | 'custom';
|
|
5
|
+
export type Delivery = 'both' | 'skills' | 'commands';
|
|
4
6
|
export interface GlobalConfig {
|
|
5
7
|
featureFlags?: Record<string, boolean>;
|
|
8
|
+
profile?: Profile;
|
|
9
|
+
delivery?: Delivery;
|
|
10
|
+
workflows?: string[];
|
|
6
11
|
}
|
|
7
12
|
/**
|
|
8
13
|
* Gets the global configuration directory path following XDG Base Directory Specification.
|
|
@@ -6,7 +6,9 @@ export const GLOBAL_CONFIG_DIR_NAME = 'openspec';
|
|
|
6
6
|
export const GLOBAL_CONFIG_FILE_NAME = 'config.json';
|
|
7
7
|
export const GLOBAL_DATA_DIR_NAME = 'openspec';
|
|
8
8
|
const DEFAULT_CONFIG = {
|
|
9
|
-
featureFlags: {}
|
|
9
|
+
featureFlags: {},
|
|
10
|
+
profile: 'core',
|
|
11
|
+
delivery: 'both',
|
|
10
12
|
};
|
|
11
13
|
/**
|
|
12
14
|
* Gets the global configuration directory path following XDG Base Directory Specification.
|
|
@@ -81,7 +83,7 @@ export function getGlobalConfig() {
|
|
|
81
83
|
const content = fs.readFileSync(configPath, 'utf-8');
|
|
82
84
|
const parsed = JSON.parse(content);
|
|
83
85
|
// Merge with defaults (loaded values take precedence)
|
|
84
|
-
|
|
86
|
+
const merged = {
|
|
85
87
|
...DEFAULT_CONFIG,
|
|
86
88
|
...parsed,
|
|
87
89
|
// Deep merge featureFlags
|
|
@@ -90,6 +92,14 @@ export function getGlobalConfig() {
|
|
|
90
92
|
...(parsed.featureFlags || {})
|
|
91
93
|
}
|
|
92
94
|
};
|
|
95
|
+
// Schema evolution: apply defaults for new fields if not present in loaded config
|
|
96
|
+
if (parsed.profile === undefined) {
|
|
97
|
+
merged.profile = DEFAULT_CONFIG.profile;
|
|
98
|
+
}
|
|
99
|
+
if (parsed.delivery === undefined) {
|
|
100
|
+
merged.delivery = DEFAULT_CONFIG.delivery;
|
|
101
|
+
}
|
|
102
|
+
return merged;
|
|
93
103
|
}
|
|
94
104
|
catch (error) {
|
|
95
105
|
// Log warning for parse errors, but not for missing files
|
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
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -139,40 +176,73 @@ export class InitCommand {
|
|
|
139
176
|
// ═══════════════════════════════════════════════════════════
|
|
140
177
|
// TOOL SELECTION
|
|
141
178
|
// ═══════════════════════════════════════════════════════════
|
|
142
|
-
async getSelectedTools(toolStates, extendMode) {
|
|
179
|
+
async getSelectedTools(toolStates, extendMode, detectedTools, projectPath) {
|
|
143
180
|
// Check for --tools flag first
|
|
144
181
|
const nonInteractiveSelection = this.resolveToolsArg();
|
|
145
182
|
if (nonInteractiveSelection !== null) {
|
|
146
183
|
return nonInteractiveSelection;
|
|
147
184
|
}
|
|
148
185
|
const validTools = getToolsWithSkillsDir();
|
|
186
|
+
const detectedToolIds = new Set(detectedTools.map((t) => t.value));
|
|
187
|
+
const configuredToolIds = new Set([...toolStates.entries()]
|
|
188
|
+
.filter(([, status]) => status.configured)
|
|
189
|
+
.map(([toolId]) => toolId));
|
|
190
|
+
const shouldPreselectDetected = !extendMode && configuredToolIds.size === 0;
|
|
149
191
|
const canPrompt = this.canPromptInteractively();
|
|
150
|
-
|
|
151
|
-
|
|
192
|
+
// Non-interactive mode: use detected tools as fallback (task 7.8)
|
|
193
|
+
if (!canPrompt) {
|
|
194
|
+
if (detectedToolIds.size > 0) {
|
|
195
|
+
return [...detectedToolIds];
|
|
196
|
+
}
|
|
197
|
+
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,...`);
|
|
198
|
+
}
|
|
199
|
+
if (validTools.length === 0) {
|
|
200
|
+
throw new Error(`No tools available for skill generation.`);
|
|
152
201
|
}
|
|
153
202
|
// Interactive mode: show searchable multi-select
|
|
154
203
|
const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
|
|
155
|
-
// Build choices
|
|
204
|
+
// Build choices: pre-select configured tools; keep detected tools visible but unselected.
|
|
156
205
|
const sortedChoices = validTools
|
|
157
206
|
.map((toolId) => {
|
|
158
207
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
159
208
|
const status = toolStates.get(toolId);
|
|
160
209
|
const configured = status?.configured ?? false;
|
|
210
|
+
const detected = detectedToolIds.has(toolId);
|
|
161
211
|
return {
|
|
162
212
|
name: tool?.name || toolId,
|
|
163
213
|
value: toolId,
|
|
164
214
|
configured,
|
|
165
|
-
|
|
215
|
+
detected: detected && !configured,
|
|
216
|
+
preSelected: configured || (shouldPreselectDetected && detected && !configured),
|
|
166
217
|
};
|
|
167
218
|
})
|
|
168
219
|
.sort((a, b) => {
|
|
169
|
-
// Configured tools first
|
|
220
|
+
// Configured tools first, then detected (not configured), then everything else.
|
|
170
221
|
if (a.configured && !b.configured)
|
|
171
222
|
return -1;
|
|
172
223
|
if (!a.configured && b.configured)
|
|
173
224
|
return 1;
|
|
225
|
+
if (a.detected && !b.detected)
|
|
226
|
+
return -1;
|
|
227
|
+
if (!a.detected && b.detected)
|
|
228
|
+
return 1;
|
|
174
229
|
return 0;
|
|
175
230
|
});
|
|
231
|
+
const configuredNames = validTools
|
|
232
|
+
.filter((toolId) => configuredToolIds.has(toolId))
|
|
233
|
+
.map((toolId) => AI_TOOLS.find((t) => t.value === toolId)?.name || toolId);
|
|
234
|
+
if (configuredNames.length > 0) {
|
|
235
|
+
console.log(`OpenSpec configured: ${configuredNames.join(', ')} (pre-selected)`);
|
|
236
|
+
}
|
|
237
|
+
const detectedOnlyNames = detectedTools
|
|
238
|
+
.filter((tool) => !configuredToolIds.has(tool.value))
|
|
239
|
+
.map((tool) => tool.name);
|
|
240
|
+
if (detectedOnlyNames.length > 0) {
|
|
241
|
+
const detectionLabel = shouldPreselectDetected
|
|
242
|
+
? 'pre-selected for first-time setup'
|
|
243
|
+
: 'not pre-selected';
|
|
244
|
+
console.log(`Detected tool directories: ${detectedOnlyNames.join(', ')} (${detectionLabel})`);
|
|
245
|
+
}
|
|
176
246
|
const selectedTools = await searchableMultiSelect({
|
|
177
247
|
message: `Select tools to set up (${validTools.length} available)`,
|
|
178
248
|
pageSize: 15,
|
|
@@ -288,37 +358,58 @@ export class InitCommand {
|
|
|
288
358
|
const refreshedTools = [];
|
|
289
359
|
const failedTools = [];
|
|
290
360
|
const commandsSkipped = [];
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
361
|
+
let removedCommandCount = 0;
|
|
362
|
+
let removedSkillCount = 0;
|
|
363
|
+
// Read global config for profile and delivery settings (use --profile override if set)
|
|
364
|
+
const globalConfig = getGlobalConfig();
|
|
365
|
+
const profile = this.resolveProfileOverride() ?? globalConfig.profile ?? 'core';
|
|
366
|
+
const delivery = globalConfig.delivery ?? 'both';
|
|
367
|
+
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
368
|
+
// Get skill and command templates filtered by profile workflows
|
|
369
|
+
const shouldGenerateSkills = delivery !== 'commands';
|
|
370
|
+
const shouldGenerateCommands = delivery !== 'skills';
|
|
371
|
+
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(workflows) : [];
|
|
372
|
+
const commandContents = shouldGenerateCommands ? getCommandContents(workflows) : [];
|
|
294
373
|
// Process each tool
|
|
295
374
|
for (const tool of tools) {
|
|
296
375
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
297
376
|
try {
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
377
|
+
// Generate skill files if delivery includes skills
|
|
378
|
+
if (shouldGenerateSkills) {
|
|
379
|
+
// Use tool-specific skillsDir
|
|
380
|
+
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
381
|
+
// Create skill directories and SKILL.md files
|
|
382
|
+
for (const { template, dirName } of skillTemplates) {
|
|
383
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
384
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
385
|
+
// Generate SKILL.md content with YAML frontmatter including generatedBy
|
|
386
|
+
// Use hyphen-based command references for OpenCode
|
|
387
|
+
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
388
|
+
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
389
|
+
// Write the skill file
|
|
390
|
+
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!shouldGenerateSkills) {
|
|
394
|
+
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
395
|
+
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
310
396
|
}
|
|
311
|
-
// Generate commands
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
397
|
+
// Generate commands if delivery includes commands
|
|
398
|
+
if (shouldGenerateCommands) {
|
|
399
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
400
|
+
if (adapter) {
|
|
401
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
402
|
+
for (const cmd of generatedCommands) {
|
|
403
|
+
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
|
|
404
|
+
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
commandsSkipped.push(tool.value);
|
|
318
409
|
}
|
|
319
410
|
}
|
|
320
|
-
|
|
321
|
-
|
|
411
|
+
if (!shouldGenerateCommands) {
|
|
412
|
+
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
|
|
322
413
|
}
|
|
323
414
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
324
415
|
if (tool.wasConfigured) {
|
|
@@ -333,7 +424,14 @@ export class InitCommand {
|
|
|
333
424
|
failedTools.push({ name: tool.name, error: error });
|
|
334
425
|
}
|
|
335
426
|
}
|
|
336
|
-
return {
|
|
427
|
+
return {
|
|
428
|
+
createdTools,
|
|
429
|
+
refreshedTools,
|
|
430
|
+
failedTools,
|
|
431
|
+
commandsSkipped,
|
|
432
|
+
removedCommandCount,
|
|
433
|
+
removedSkillCount,
|
|
434
|
+
};
|
|
337
435
|
}
|
|
338
436
|
// ═══════════════════════════════════════════════════════════
|
|
339
437
|
// CONFIG FILE
|
|
@@ -373,16 +471,24 @@ export class InitCommand {
|
|
|
373
471
|
if (results.refreshedTools.length > 0) {
|
|
374
472
|
console.log(`Refreshed: ${results.refreshedTools.map((t) => t.name).join(', ')}`);
|
|
375
473
|
}
|
|
376
|
-
// Show counts
|
|
474
|
+
// Show counts (respecting profile filter)
|
|
377
475
|
const successfulTools = [...results.createdTools, ...results.refreshedTools];
|
|
378
476
|
if (successfulTools.length > 0) {
|
|
477
|
+
const globalConfig = getGlobalConfig();
|
|
478
|
+
const profile = this.profileOverride ?? globalConfig.profile ?? 'core';
|
|
479
|
+
const delivery = globalConfig.delivery ?? 'both';
|
|
480
|
+
const workflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
379
481
|
const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', ');
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
482
|
+
const skillCount = delivery !== 'commands' ? getSkillTemplates(workflows).length : 0;
|
|
483
|
+
const commandCount = delivery !== 'skills' ? getCommandContents(workflows).length : 0;
|
|
484
|
+
if (skillCount > 0 && commandCount > 0) {
|
|
485
|
+
console.log(`${skillCount} skills and ${commandCount} commands in ${toolDirs}/`);
|
|
383
486
|
}
|
|
384
|
-
else {
|
|
385
|
-
console.log(`${
|
|
487
|
+
else if (skillCount > 0) {
|
|
488
|
+
console.log(`${skillCount} skills in ${toolDirs}/`);
|
|
489
|
+
}
|
|
490
|
+
else if (commandCount > 0) {
|
|
491
|
+
console.log(`${commandCount} commands in ${toolDirs}/`);
|
|
386
492
|
}
|
|
387
493
|
}
|
|
388
494
|
// Show failures
|
|
@@ -393,6 +499,12 @@ export class InitCommand {
|
|
|
393
499
|
if (results.commandsSkipped.length > 0) {
|
|
394
500
|
console.log(chalk.dim(`Commands skipped for: ${results.commandsSkipped.join(', ')} (no adapter)`));
|
|
395
501
|
}
|
|
502
|
+
if (results.removedCommandCount > 0) {
|
|
503
|
+
console.log(chalk.dim(`Removed: ${results.removedCommandCount} command files (delivery: skills)`));
|
|
504
|
+
}
|
|
505
|
+
if (results.removedSkillCount > 0) {
|
|
506
|
+
console.log(chalk.dim(`Removed: ${results.removedSkillCount} skill directories (delivery: commands)`));
|
|
507
|
+
}
|
|
396
508
|
// Config status
|
|
397
509
|
if (configStatus === 'created') {
|
|
398
510
|
console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
|
|
@@ -407,12 +519,22 @@ export class InitCommand {
|
|
|
407
519
|
else {
|
|
408
520
|
console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
|
|
409
521
|
}
|
|
410
|
-
// Getting started
|
|
522
|
+
// Getting started (task 7.6: show propose if in profile)
|
|
523
|
+
const globalCfg = getGlobalConfig();
|
|
524
|
+
const activeProfile = this.profileOverride ?? globalCfg.profile ?? 'core';
|
|
525
|
+
const activeWorkflows = [...getProfileWorkflows(activeProfile, globalCfg.workflows)];
|
|
411
526
|
console.log();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
527
|
+
if (activeWorkflows.includes('propose')) {
|
|
528
|
+
console.log(chalk.bold('Getting started:'));
|
|
529
|
+
console.log(' Start your first change: /opsx:propose "your idea"');
|
|
530
|
+
}
|
|
531
|
+
else if (activeWorkflows.includes('new')) {
|
|
532
|
+
console.log(chalk.bold('Getting started:'));
|
|
533
|
+
console.log(' Start your first change: /opsx:new "your idea"');
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
console.log("Done. Run 'openspec config profile' to configure your workflows.");
|
|
537
|
+
}
|
|
416
538
|
// Links
|
|
417
539
|
console.log();
|
|
418
540
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
@@ -432,5 +554,44 @@ export class InitCommand {
|
|
|
432
554
|
spinner: PROGRESS_SPINNER,
|
|
433
555
|
}).start();
|
|
434
556
|
}
|
|
557
|
+
async removeSkillDirs(skillsDir) {
|
|
558
|
+
let removed = 0;
|
|
559
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
560
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
561
|
+
if (!dirName)
|
|
562
|
+
continue;
|
|
563
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
564
|
+
try {
|
|
565
|
+
if (fs.existsSync(skillDir)) {
|
|
566
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
567
|
+
removed++;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// Ignore errors
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return removed;
|
|
575
|
+
}
|
|
576
|
+
async removeCommandFiles(projectPath, toolId) {
|
|
577
|
+
let removed = 0;
|
|
578
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
579
|
+
if (!adapter)
|
|
580
|
+
return 0;
|
|
581
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
582
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
583
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
584
|
+
try {
|
|
585
|
+
if (fs.existsSync(fullPath)) {
|
|
586
|
+
await fs.promises.unlink(fullPath);
|
|
587
|
+
removed++;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// Ignore errors
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return removed;
|
|
595
|
+
}
|
|
435
596
|
}
|
|
436
597
|
//# sourceMappingURL=init.js.map
|
|
@@ -37,6 +37,7 @@ export const LEGACY_SLASH_COMMAND_PATHS = {
|
|
|
37
37
|
'cursor': { type: 'files', pattern: '.cursor/commands/openspec-*.md' },
|
|
38
38
|
'windsurf': { type: 'files', pattern: '.windsurf/workflows/openspec-*.md' },
|
|
39
39
|
'kilocode': { type: 'files', pattern: '.kilocode/workflows/openspec-*.md' },
|
|
40
|
+
'kiro': { type: 'files', pattern: '.kiro/prompts/openspec-*.prompt.md' },
|
|
40
41
|
'github-copilot': { type: 'files', pattern: '.github/prompts/openspec-*.prompt.md' },
|
|
41
42
|
'amazon-q': { type: 'files', pattern: '.amazonq/prompts/openspec-*.md' },
|
|
42
43
|
'cline': { type: 'files', pattern: '.clinerules/workflows/openspec-*.md' },
|
|
@@ -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
|