@fission-ai/openspec 1.1.0 → 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/opencode.js +4 -1
- 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 +206 -42
- 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 +16 -8
- package/dist/core/shared/skill-generation.js +42 -22
- 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 +292 -61
- package/dist/prompts/searchable-multi-select.d.ts +3 -2
- package/dist/prompts/searchable-multi-select.js +22 -12
- package/dist/utils/command-references.d.ts +18 -0
- package/dist/utils/command-references.js +20 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/package.json +1 -1
|
@@ -5,6 +5,16 @@ import { z } from 'zod';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const GlobalConfigSchema: z.ZodObject<{
|
|
7
7
|
featureFlags: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
|
|
8
|
+
profile: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
core: "core";
|
|
10
|
+
custom: "custom";
|
|
11
|
+
}>>>;
|
|
12
|
+
delivery: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
13
|
+
commands: "commands";
|
|
14
|
+
skills: "skills";
|
|
15
|
+
both: "both";
|
|
16
|
+
}>>>;
|
|
17
|
+
workflows: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
8
18
|
}, z.core.$loose>;
|
|
9
19
|
export type GlobalConfigType = z.infer<typeof GlobalConfigSchema>;
|
|
10
20
|
/**
|
|
@@ -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
|
@@ -10,6 +10,7 @@ import ora from 'ora';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
|
+
import { transformToHyphenCommands } from '../utils/command-references.js';
|
|
13
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME, } from './config.js';
|
|
14
15
|
import { PALETTE } from './styles/palette.js';
|
|
15
16
|
import { isInteractive } from '../utils/interactive.js';
|
|
@@ -17,6 +18,10 @@ import { serializeConfig } from './config-prompts.js';
|
|
|
17
18
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
18
19
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, } from './legacy-cleanup.js';
|
|
19
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';
|
|
20
25
|
const require = createRequire(import.meta.url);
|
|
21
26
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
22
27
|
// -----------------------------------------------------------------------------
|
|
@@ -27,6 +32,19 @@ const PROGRESS_SPINNER = {
|
|
|
27
32
|
interval: 80,
|
|
28
33
|
frames: ['░░░', '▒░░', '▒▒░', '▒▒▒', '▓▒▒', '▓▓▒', '▓▓▓', '▒▓▓', '░▒▓'],
|
|
29
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
|
+
};
|
|
30
48
|
// -----------------------------------------------------------------------------
|
|
31
49
|
// Init Command Class
|
|
32
50
|
// -----------------------------------------------------------------------------
|
|
@@ -34,10 +52,12 @@ export class InitCommand {
|
|
|
34
52
|
toolsArg;
|
|
35
53
|
force;
|
|
36
54
|
interactiveOption;
|
|
55
|
+
profileOverride;
|
|
37
56
|
constructor(options = {}) {
|
|
38
57
|
this.toolsArg = options.tools;
|
|
39
58
|
this.force = options.force ?? false;
|
|
40
59
|
this.interactiveOption = options.interactive;
|
|
60
|
+
this.profileOverride = options.profile;
|
|
41
61
|
}
|
|
42
62
|
async execute(targetPath) {
|
|
43
63
|
const projectPath = path.resolve(targetPath);
|
|
@@ -47,16 +67,25 @@ export class InitCommand {
|
|
|
47
67
|
const extendMode = await this.validate(projectPath, openspecPath);
|
|
48
68
|
// Check for legacy artifacts and handle cleanup
|
|
49
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
|
+
}
|
|
50
76
|
// Show animated welcome screen (interactive mode only)
|
|
51
77
|
const canPrompt = this.canPromptInteractively();
|
|
52
78
|
if (canPrompt) {
|
|
53
79
|
const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
|
|
54
80
|
await showWelcomeScreen();
|
|
55
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();
|
|
56
85
|
// Get tool states before processing
|
|
57
86
|
const toolStates = getToolStates(projectPath);
|
|
58
|
-
// Get tool selection
|
|
59
|
-
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);
|
|
60
89
|
// Validate selected tools
|
|
61
90
|
const validatedTools = this.validateTools(selectedToolIds, toolStates);
|
|
62
91
|
// Create directory structure and config
|
|
@@ -86,6 +115,15 @@ export class InitCommand {
|
|
|
86
115
|
return false;
|
|
87
116
|
return isInteractive({ interactive: this.interactiveOption });
|
|
88
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
|
+
}
|
|
89
127
|
// ═══════════════════════════════════════════════════════════
|
|
90
128
|
// LEGACY CLEANUP
|
|
91
129
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -138,40 +176,73 @@ export class InitCommand {
|
|
|
138
176
|
// ═══════════════════════════════════════════════════════════
|
|
139
177
|
// TOOL SELECTION
|
|
140
178
|
// ═══════════════════════════════════════════════════════════
|
|
141
|
-
async getSelectedTools(toolStates, extendMode) {
|
|
179
|
+
async getSelectedTools(toolStates, extendMode, detectedTools, projectPath) {
|
|
142
180
|
// Check for --tools flag first
|
|
143
181
|
const nonInteractiveSelection = this.resolveToolsArg();
|
|
144
182
|
if (nonInteractiveSelection !== null) {
|
|
145
183
|
return nonInteractiveSelection;
|
|
146
184
|
}
|
|
147
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;
|
|
148
191
|
const canPrompt = this.canPromptInteractively();
|
|
149
|
-
|
|
150
|
-
|
|
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.`);
|
|
151
201
|
}
|
|
152
202
|
// Interactive mode: show searchable multi-select
|
|
153
203
|
const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
|
|
154
|
-
// Build choices
|
|
204
|
+
// Build choices: pre-select configured tools; keep detected tools visible but unselected.
|
|
155
205
|
const sortedChoices = validTools
|
|
156
206
|
.map((toolId) => {
|
|
157
207
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
158
208
|
const status = toolStates.get(toolId);
|
|
159
209
|
const configured = status?.configured ?? false;
|
|
210
|
+
const detected = detectedToolIds.has(toolId);
|
|
160
211
|
return {
|
|
161
212
|
name: tool?.name || toolId,
|
|
162
213
|
value: toolId,
|
|
163
214
|
configured,
|
|
164
|
-
|
|
215
|
+
detected: detected && !configured,
|
|
216
|
+
preSelected: configured || (shouldPreselectDetected && detected && !configured),
|
|
165
217
|
};
|
|
166
218
|
})
|
|
167
219
|
.sort((a, b) => {
|
|
168
|
-
// Configured tools first
|
|
220
|
+
// Configured tools first, then detected (not configured), then everything else.
|
|
169
221
|
if (a.configured && !b.configured)
|
|
170
222
|
return -1;
|
|
171
223
|
if (!a.configured && b.configured)
|
|
172
224
|
return 1;
|
|
225
|
+
if (a.detected && !b.detected)
|
|
226
|
+
return -1;
|
|
227
|
+
if (!a.detected && b.detected)
|
|
228
|
+
return 1;
|
|
173
229
|
return 0;
|
|
174
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
|
+
}
|
|
175
246
|
const selectedTools = await searchableMultiSelect({
|
|
176
247
|
message: `Select tools to set up (${validTools.length} available)`,
|
|
177
248
|
pageSize: 15,
|
|
@@ -287,35 +358,58 @@ export class InitCommand {
|
|
|
287
358
|
const refreshedTools = [];
|
|
288
359
|
const failedTools = [];
|
|
289
360
|
const commandsSkipped = [];
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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) : [];
|
|
293
373
|
// Process each tool
|
|
294
374
|
for (const tool of tools) {
|
|
295
375
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
296
376
|
try {
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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);
|
|
307
396
|
}
|
|
308
|
-
// Generate commands
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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);
|
|
315
409
|
}
|
|
316
410
|
}
|
|
317
|
-
|
|
318
|
-
|
|
411
|
+
if (!shouldGenerateCommands) {
|
|
412
|
+
removedCommandCount += await this.removeCommandFiles(projectPath, tool.value);
|
|
319
413
|
}
|
|
320
414
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
321
415
|
if (tool.wasConfigured) {
|
|
@@ -330,7 +424,14 @@ export class InitCommand {
|
|
|
330
424
|
failedTools.push({ name: tool.name, error: error });
|
|
331
425
|
}
|
|
332
426
|
}
|
|
333
|
-
return {
|
|
427
|
+
return {
|
|
428
|
+
createdTools,
|
|
429
|
+
refreshedTools,
|
|
430
|
+
failedTools,
|
|
431
|
+
commandsSkipped,
|
|
432
|
+
removedCommandCount,
|
|
433
|
+
removedSkillCount,
|
|
434
|
+
};
|
|
334
435
|
}
|
|
335
436
|
// ═══════════════════════════════════════════════════════════
|
|
336
437
|
// CONFIG FILE
|
|
@@ -370,16 +471,24 @@ export class InitCommand {
|
|
|
370
471
|
if (results.refreshedTools.length > 0) {
|
|
371
472
|
console.log(`Refreshed: ${results.refreshedTools.map((t) => t.name).join(', ')}`);
|
|
372
473
|
}
|
|
373
|
-
// Show counts
|
|
474
|
+
// Show counts (respecting profile filter)
|
|
374
475
|
const successfulTools = [...results.createdTools, ...results.refreshedTools];
|
|
375
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);
|
|
376
481
|
const toolDirs = [...new Set(successfulTools.map((t) => t.skillsDir))].join(', ');
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
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}/`);
|
|
380
486
|
}
|
|
381
|
-
else {
|
|
382
|
-
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}/`);
|
|
383
492
|
}
|
|
384
493
|
}
|
|
385
494
|
// Show failures
|
|
@@ -390,6 +499,12 @@ export class InitCommand {
|
|
|
390
499
|
if (results.commandsSkipped.length > 0) {
|
|
391
500
|
console.log(chalk.dim(`Commands skipped for: ${results.commandsSkipped.join(', ')} (no adapter)`));
|
|
392
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
|
+
}
|
|
393
508
|
// Config status
|
|
394
509
|
if (configStatus === 'created') {
|
|
395
510
|
console.log(`Config: openspec/config.yaml (schema: ${DEFAULT_SCHEMA})`);
|
|
@@ -404,12 +519,22 @@ export class InitCommand {
|
|
|
404
519
|
else {
|
|
405
520
|
console.log(chalk.dim(`Config: skipped (non-interactive mode)`));
|
|
406
521
|
}
|
|
407
|
-
// 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)];
|
|
408
526
|
console.log();
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
+
}
|
|
413
538
|
// Links
|
|
414
539
|
console.log();
|
|
415
540
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
@@ -429,5 +554,44 @@ export class InitCommand {
|
|
|
429
554
|
spinner: PROGRESS_SPINNER,
|
|
430
555
|
}).start();
|
|
431
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
|
+
}
|
|
432
596
|
}
|
|
433
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
|