@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.
Files changed (68) hide show
  1. package/dist/cli/index.js +2 -0
  2. package/dist/commands/config.d.ts +28 -0
  3. package/dist/commands/config.js +359 -5
  4. package/dist/core/available-tools.d.ts +16 -0
  5. package/dist/core/available-tools.js +30 -0
  6. package/dist/core/command-generation/adapters/index.d.ts +2 -0
  7. package/dist/core/command-generation/adapters/index.js +2 -0
  8. package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
  9. package/dist/core/command-generation/adapters/kiro.js +26 -0
  10. package/dist/core/command-generation/adapters/pi.d.ts +14 -0
  11. package/dist/core/command-generation/adapters/pi.js +41 -0
  12. package/dist/core/command-generation/registry.js +4 -0
  13. package/dist/core/completions/command-registry.js +5 -0
  14. package/dist/core/config-schema.d.ts +10 -0
  15. package/dist/core/config-schema.js +14 -1
  16. package/dist/core/config.js +2 -0
  17. package/dist/core/global-config.d.ts +5 -0
  18. package/dist/core/global-config.js +12 -2
  19. package/dist/core/init.d.ts +5 -0
  20. package/dist/core/init.js +205 -44
  21. package/dist/core/legacy-cleanup.js +1 -0
  22. package/dist/core/migration.d.ts +23 -0
  23. package/dist/core/migration.js +108 -0
  24. package/dist/core/profile-sync-drift.d.ts +38 -0
  25. package/dist/core/profile-sync-drift.js +200 -0
  26. package/dist/core/profiles.d.ts +26 -0
  27. package/dist/core/profiles.js +40 -0
  28. package/dist/core/shared/index.d.ts +1 -1
  29. package/dist/core/shared/index.js +1 -1
  30. package/dist/core/shared/skill-generation.d.ts +14 -7
  31. package/dist/core/shared/skill-generation.js +36 -20
  32. package/dist/core/shared/tool-detection.d.ts +8 -3
  33. package/dist/core/shared/tool-detection.js +18 -0
  34. package/dist/core/templates/index.d.ts +1 -1
  35. package/dist/core/templates/index.js +2 -2
  36. package/dist/core/templates/skill-templates.d.ts +15 -118
  37. package/dist/core/templates/skill-templates.js +14 -3424
  38. package/dist/core/templates/types.d.ts +19 -0
  39. package/dist/core/templates/types.js +5 -0
  40. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  41. package/dist/core/templates/workflows/apply-change.js +308 -0
  42. package/dist/core/templates/workflows/archive-change.d.ts +10 -0
  43. package/dist/core/templates/workflows/archive-change.js +271 -0
  44. package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
  45. package/dist/core/templates/workflows/bulk-archive-change.js +488 -0
  46. package/dist/core/templates/workflows/continue-change.d.ts +10 -0
  47. package/dist/core/templates/workflows/continue-change.js +232 -0
  48. package/dist/core/templates/workflows/explore.d.ts +10 -0
  49. package/dist/core/templates/workflows/explore.js +461 -0
  50. package/dist/core/templates/workflows/feedback.d.ts +9 -0
  51. package/dist/core/templates/workflows/feedback.js +108 -0
  52. package/dist/core/templates/workflows/ff-change.d.ts +10 -0
  53. package/dist/core/templates/workflows/ff-change.js +198 -0
  54. package/dist/core/templates/workflows/new-change.d.ts +10 -0
  55. package/dist/core/templates/workflows/new-change.js +143 -0
  56. package/dist/core/templates/workflows/onboard.d.ts +10 -0
  57. package/dist/core/templates/workflows/onboard.js +565 -0
  58. package/dist/core/templates/workflows/propose.d.ts +10 -0
  59. package/dist/core/templates/workflows/propose.js +216 -0
  60. package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
  61. package/dist/core/templates/workflows/sync-specs.js +272 -0
  62. package/dist/core/templates/workflows/verify-change.d.ts +10 -0
  63. package/dist/core/templates/workflows/verify-change.js +332 -0
  64. package/dist/core/update.d.ts +36 -1
  65. package/dist/core/update.js +291 -65
  66. package/dist/prompts/searchable-multi-select.d.ts +3 -2
  67. package/dist/prompts/searchable-multi-select.js +22 -12
  68. 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.
@@ -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
- return {
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
@@ -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
- if (!canPrompt || validTools.length === 0) {
151
- throw new Error(`Missing required option --tools. Valid tools:\n ${validTools.join('\n ')}\n\nUse --tools all, --tools none, or --tools claude,cursor,...`);
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 with configured status and sort configured tools first
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
- preSelected: configured, // Pre-select configured tools for easy refresh
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
- // Get skill and command templates once (shared across all tools)
292
- const skillTemplates = getSkillTemplates();
293
- const commandContents = getCommandContents();
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
- // Use tool-specific skillsDir
299
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
300
- // Create skill directories and SKILL.md files
301
- for (const { template, dirName } of skillTemplates) {
302
- const skillDir = path.join(skillsDir, dirName);
303
- const skillFile = path.join(skillDir, 'SKILL.md');
304
- // Generate SKILL.md content with YAML frontmatter including generatedBy
305
- // Use hyphen-based command references for OpenCode
306
- const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
307
- const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
308
- // Write the skill file
309
- await FileSystemUtils.writeFile(skillFile, skillContent);
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 using the adapter system
312
- const adapter = CommandAdapterRegistry.get(tool.value);
313
- if (adapter) {
314
- const generatedCommands = generateCommands(commandContents, adapter);
315
- for (const cmd of generatedCommands) {
316
- const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
317
- await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
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
- else {
321
- commandsSkipped.push(tool.value);
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 { createdTools, refreshedTools, failedTools, commandsSkipped };
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 hasCommands = results.commandsSkipped.length < successfulTools.length;
381
- if (hasCommands) {
382
- console.log(`${getSkillTemplates().length} skills and ${getCommandContents().length} commands in ${toolDirs}/`);
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(`${getSkillTemplates().length} skills in ${toolDirs}/`);
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
- console.log(chalk.bold('Getting started:'));
413
- console.log(' /opsx:new Start a new change');
414
- console.log(' /opsx:continue Create the next artifact');
415
- console.log(' /opsx:apply Implement tasks');
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