@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.
Files changed (73) 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/opencode.js +4 -1
  11. package/dist/core/command-generation/adapters/pi.d.ts +14 -0
  12. package/dist/core/command-generation/adapters/pi.js +41 -0
  13. package/dist/core/command-generation/registry.js +4 -0
  14. package/dist/core/completions/command-registry.js +5 -0
  15. package/dist/core/config-schema.d.ts +10 -0
  16. package/dist/core/config-schema.js +14 -1
  17. package/dist/core/config.js +2 -0
  18. package/dist/core/global-config.d.ts +5 -0
  19. package/dist/core/global-config.js +12 -2
  20. package/dist/core/init.d.ts +5 -0
  21. package/dist/core/init.js +206 -42
  22. package/dist/core/legacy-cleanup.js +1 -0
  23. package/dist/core/migration.d.ts +23 -0
  24. package/dist/core/migration.js +108 -0
  25. package/dist/core/profile-sync-drift.d.ts +38 -0
  26. package/dist/core/profile-sync-drift.js +200 -0
  27. package/dist/core/profiles.d.ts +26 -0
  28. package/dist/core/profiles.js +40 -0
  29. package/dist/core/shared/index.d.ts +1 -1
  30. package/dist/core/shared/index.js +1 -1
  31. package/dist/core/shared/skill-generation.d.ts +16 -8
  32. package/dist/core/shared/skill-generation.js +42 -22
  33. package/dist/core/shared/tool-detection.d.ts +8 -3
  34. package/dist/core/shared/tool-detection.js +18 -0
  35. package/dist/core/templates/index.d.ts +1 -1
  36. package/dist/core/templates/index.js +2 -2
  37. package/dist/core/templates/skill-templates.d.ts +15 -118
  38. package/dist/core/templates/skill-templates.js +14 -3424
  39. package/dist/core/templates/types.d.ts +19 -0
  40. package/dist/core/templates/types.js +5 -0
  41. package/dist/core/templates/workflows/apply-change.d.ts +10 -0
  42. package/dist/core/templates/workflows/apply-change.js +308 -0
  43. package/dist/core/templates/workflows/archive-change.d.ts +10 -0
  44. package/dist/core/templates/workflows/archive-change.js +271 -0
  45. package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
  46. package/dist/core/templates/workflows/bulk-archive-change.js +488 -0
  47. package/dist/core/templates/workflows/continue-change.d.ts +10 -0
  48. package/dist/core/templates/workflows/continue-change.js +232 -0
  49. package/dist/core/templates/workflows/explore.d.ts +10 -0
  50. package/dist/core/templates/workflows/explore.js +461 -0
  51. package/dist/core/templates/workflows/feedback.d.ts +9 -0
  52. package/dist/core/templates/workflows/feedback.js +108 -0
  53. package/dist/core/templates/workflows/ff-change.d.ts +10 -0
  54. package/dist/core/templates/workflows/ff-change.js +198 -0
  55. package/dist/core/templates/workflows/new-change.d.ts +10 -0
  56. package/dist/core/templates/workflows/new-change.js +143 -0
  57. package/dist/core/templates/workflows/onboard.d.ts +10 -0
  58. package/dist/core/templates/workflows/onboard.js +565 -0
  59. package/dist/core/templates/workflows/propose.d.ts +10 -0
  60. package/dist/core/templates/workflows/propose.js +216 -0
  61. package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
  62. package/dist/core/templates/workflows/sync-specs.js +272 -0
  63. package/dist/core/templates/workflows/verify-change.d.ts +10 -0
  64. package/dist/core/templates/workflows/verify-change.js +332 -0
  65. package/dist/core/update.d.ts +36 -1
  66. package/dist/core/update.js +292 -61
  67. package/dist/prompts/searchable-multi-select.d.ts +3 -2
  68. package/dist/prompts/searchable-multi-select.js +22 -12
  69. package/dist/utils/command-references.d.ts +18 -0
  70. package/dist/utils/command-references.js +20 -0
  71. package/dist/utils/index.d.ts +1 -0
  72. package/dist/utils/index.js +2 -0
  73. 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.
@@ -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
@@ -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
- if (!canPrompt || validTools.length === 0) {
150
- 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.`);
151
201
  }
152
202
  // Interactive mode: show searchable multi-select
153
203
  const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
154
- // Build choices with configured status and sort configured tools first
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
- preSelected: configured, // Pre-select configured tools for easy refresh
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
- // Get skill and command templates once (shared across all tools)
291
- const skillTemplates = getSkillTemplates();
292
- 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) : [];
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
- // Use tool-specific skillsDir
298
- const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
299
- // Create skill directories and SKILL.md files
300
- for (const { template, dirName } of skillTemplates) {
301
- const skillDir = path.join(skillsDir, dirName);
302
- const skillFile = path.join(skillDir, 'SKILL.md');
303
- // Generate SKILL.md content with YAML frontmatter including generatedBy
304
- const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
305
- // Write the skill file
306
- 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);
307
396
  }
308
- // Generate commands using the adapter system
309
- const adapter = CommandAdapterRegistry.get(tool.value);
310
- if (adapter) {
311
- const generatedCommands = generateCommands(commandContents, adapter);
312
- for (const cmd of generatedCommands) {
313
- const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
314
- 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);
315
409
  }
316
410
  }
317
- else {
318
- commandsSkipped.push(tool.value);
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 { createdTools, refreshedTools, failedTools, commandsSkipped };
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 hasCommands = results.commandsSkipped.length < successfulTools.length;
378
- if (hasCommands) {
379
- 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}/`);
380
486
  }
381
- else {
382
- 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}/`);
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
- console.log(chalk.bold('Getting started:'));
410
- console.log(' /opsx:new Start a new change');
411
- console.log(' /opsx:continue Create the next artifact');
412
- 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
+ }
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