@fission-ai/openspec 0.23.0 → 1.0.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 (183) hide show
  1. package/README.md +111 -382
  2. package/dist/cli/index.js +120 -6
  3. package/dist/commands/workflow/index.d.ts +17 -0
  4. package/dist/commands/workflow/index.js +12 -0
  5. package/dist/commands/workflow/instructions.d.ts +29 -0
  6. package/dist/commands/workflow/instructions.js +381 -0
  7. package/dist/commands/workflow/new-change.d.ts +11 -0
  8. package/dist/commands/workflow/new-change.js +44 -0
  9. package/dist/commands/workflow/schemas.d.ts +10 -0
  10. package/dist/commands/workflow/schemas.js +34 -0
  11. package/dist/commands/workflow/shared.d.ts +52 -0
  12. package/dist/commands/workflow/shared.js +111 -0
  13. package/dist/commands/workflow/status.d.ts +14 -0
  14. package/dist/commands/workflow/status.js +58 -0
  15. package/dist/commands/workflow/templates.d.ts +16 -0
  16. package/dist/commands/workflow/templates.js +68 -0
  17. package/dist/core/artifact-graph/instruction-loader.d.ts +5 -1
  18. package/dist/core/artifact-graph/instruction-loader.js +8 -19
  19. package/dist/core/command-generation/adapters/amazon-q.d.ts +13 -0
  20. package/dist/core/command-generation/adapters/amazon-q.js +26 -0
  21. package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
  22. package/dist/core/command-generation/adapters/antigravity.js +26 -0
  23. package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
  24. package/dist/core/command-generation/adapters/auggie.js +27 -0
  25. package/dist/core/command-generation/adapters/claude.d.ts +13 -0
  26. package/dist/core/command-generation/adapters/claude.js +50 -0
  27. package/dist/core/command-generation/adapters/cline.d.ts +14 -0
  28. package/dist/core/command-generation/adapters/cline.js +27 -0
  29. package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
  30. package/dist/core/command-generation/adapters/codebuddy.js +28 -0
  31. package/dist/core/command-generation/adapters/codex.d.ts +13 -0
  32. package/dist/core/command-generation/adapters/codex.js +27 -0
  33. package/dist/core/command-generation/adapters/continue.d.ts +13 -0
  34. package/dist/core/command-generation/adapters/continue.js +28 -0
  35. package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
  36. package/dist/core/command-generation/adapters/costrict.js +27 -0
  37. package/dist/core/command-generation/adapters/crush.d.ts +13 -0
  38. package/dist/core/command-generation/adapters/crush.js +30 -0
  39. package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
  40. package/dist/core/command-generation/adapters/cursor.js +44 -0
  41. package/dist/core/command-generation/adapters/factory.d.ts +13 -0
  42. package/dist/core/command-generation/adapters/factory.js +27 -0
  43. package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
  44. package/dist/core/command-generation/adapters/gemini.js +26 -0
  45. package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
  46. package/dist/core/command-generation/adapters/github-copilot.js +26 -0
  47. package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
  48. package/dist/core/command-generation/adapters/iflow.js +29 -0
  49. package/dist/core/command-generation/adapters/index.d.ts +27 -0
  50. package/dist/core/command-generation/adapters/index.js +27 -0
  51. package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
  52. package/dist/core/command-generation/adapters/kilocode.js +23 -0
  53. package/dist/core/command-generation/adapters/opencode.d.ts +13 -0
  54. package/dist/core/command-generation/adapters/opencode.js +26 -0
  55. package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
  56. package/dist/core/command-generation/adapters/qoder.js +30 -0
  57. package/dist/core/command-generation/adapters/qwen.d.ts +13 -0
  58. package/dist/core/command-generation/adapters/qwen.js +26 -0
  59. package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
  60. package/dist/core/command-generation/adapters/roocode.js +27 -0
  61. package/dist/core/command-generation/adapters/windsurf.d.ts +14 -0
  62. package/dist/core/command-generation/adapters/windsurf.js +51 -0
  63. package/dist/core/command-generation/generator.d.ts +21 -0
  64. package/dist/core/command-generation/generator.js +27 -0
  65. package/dist/core/command-generation/index.d.ts +22 -0
  66. package/dist/core/command-generation/index.js +24 -0
  67. package/dist/core/command-generation/registry.d.ts +36 -0
  68. package/dist/core/command-generation/registry.js +88 -0
  69. package/dist/core/command-generation/types.d.ts +55 -0
  70. package/dist/core/command-generation/types.js +8 -0
  71. package/dist/core/config.d.ts +1 -0
  72. package/dist/core/config.js +21 -21
  73. package/dist/core/init.d.ts +16 -36
  74. package/dist/core/init.js +323 -534
  75. package/dist/core/legacy-cleanup.d.ts +162 -0
  76. package/dist/core/legacy-cleanup.js +501 -0
  77. package/dist/core/shared/index.d.ts +8 -0
  78. package/dist/core/shared/index.js +8 -0
  79. package/dist/core/shared/skill-generation.d.ts +41 -0
  80. package/dist/core/shared/skill-generation.js +76 -0
  81. package/dist/core/shared/tool-detection.d.ts +66 -0
  82. package/dist/core/shared/tool-detection.js +140 -0
  83. package/dist/core/templates/index.d.ts +7 -16
  84. package/dist/core/templates/index.js +8 -36
  85. package/dist/core/templates/skill-templates.d.ts +13 -0
  86. package/dist/core/templates/skill-templates.js +627 -21
  87. package/dist/core/update.d.ts +38 -0
  88. package/dist/core/update.js +280 -62
  89. package/dist/prompts/searchable-multi-select.d.ts +27 -0
  90. package/dist/prompts/searchable-multi-select.js +149 -0
  91. package/dist/ui/ascii-patterns.d.ts +16 -0
  92. package/dist/ui/ascii-patterns.js +133 -0
  93. package/dist/ui/welcome-screen.d.ts +10 -0
  94. package/dist/ui/welcome-screen.js +146 -0
  95. package/dist/utils/file-system.d.ts +11 -0
  96. package/dist/utils/file-system.js +65 -2
  97. package/dist/utils/index.d.ts +1 -0
  98. package/dist/utils/index.js +2 -0
  99. package/package.json +1 -1
  100. package/dist/commands/artifact-workflow.d.ts +0 -17
  101. package/dist/commands/artifact-workflow.js +0 -915
  102. package/dist/core/configurators/agents.d.ts +0 -8
  103. package/dist/core/configurators/agents.js +0 -15
  104. package/dist/core/configurators/base.d.ts +0 -7
  105. package/dist/core/configurators/base.js +0 -2
  106. package/dist/core/configurators/claude.d.ts +0 -8
  107. package/dist/core/configurators/claude.js +0 -15
  108. package/dist/core/configurators/cline.d.ts +0 -8
  109. package/dist/core/configurators/cline.js +0 -15
  110. package/dist/core/configurators/codebuddy.d.ts +0 -8
  111. package/dist/core/configurators/codebuddy.js +0 -15
  112. package/dist/core/configurators/costrict.d.ts +0 -8
  113. package/dist/core/configurators/costrict.js +0 -15
  114. package/dist/core/configurators/iflow.d.ts +0 -8
  115. package/dist/core/configurators/iflow.js +0 -15
  116. package/dist/core/configurators/qoder.d.ts +0 -30
  117. package/dist/core/configurators/qoder.js +0 -42
  118. package/dist/core/configurators/qwen.d.ts +0 -24
  119. package/dist/core/configurators/qwen.js +0 -37
  120. package/dist/core/configurators/registry.d.ts +0 -9
  121. package/dist/core/configurators/registry.js +0 -43
  122. package/dist/core/configurators/slash/amazon-q.d.ts +0 -9
  123. package/dist/core/configurators/slash/amazon-q.js +0 -46
  124. package/dist/core/configurators/slash/antigravity.d.ts +0 -9
  125. package/dist/core/configurators/slash/antigravity.js +0 -23
  126. package/dist/core/configurators/slash/auggie.d.ts +0 -9
  127. package/dist/core/configurators/slash/auggie.js +0 -31
  128. package/dist/core/configurators/slash/base.d.ts +0 -19
  129. package/dist/core/configurators/slash/base.js +0 -69
  130. package/dist/core/configurators/slash/claude.d.ts +0 -9
  131. package/dist/core/configurators/slash/claude.js +0 -37
  132. package/dist/core/configurators/slash/cline.d.ts +0 -9
  133. package/dist/core/configurators/slash/cline.js +0 -23
  134. package/dist/core/configurators/slash/codebuddy.d.ts +0 -9
  135. package/dist/core/configurators/slash/codebuddy.js +0 -34
  136. package/dist/core/configurators/slash/codex.d.ts +0 -14
  137. package/dist/core/configurators/slash/codex.js +0 -109
  138. package/dist/core/configurators/slash/continue.d.ts +0 -9
  139. package/dist/core/configurators/slash/continue.js +0 -46
  140. package/dist/core/configurators/slash/costrict.d.ts +0 -9
  141. package/dist/core/configurators/slash/costrict.js +0 -31
  142. package/dist/core/configurators/slash/crush.d.ts +0 -9
  143. package/dist/core/configurators/slash/crush.js +0 -37
  144. package/dist/core/configurators/slash/cursor.d.ts +0 -9
  145. package/dist/core/configurators/slash/cursor.js +0 -37
  146. package/dist/core/configurators/slash/factory.d.ts +0 -10
  147. package/dist/core/configurators/slash/factory.js +0 -35
  148. package/dist/core/configurators/slash/gemini.d.ts +0 -9
  149. package/dist/core/configurators/slash/gemini.js +0 -22
  150. package/dist/core/configurators/slash/github-copilot.d.ts +0 -9
  151. package/dist/core/configurators/slash/github-copilot.js +0 -34
  152. package/dist/core/configurators/slash/iflow.d.ts +0 -9
  153. package/dist/core/configurators/slash/iflow.js +0 -37
  154. package/dist/core/configurators/slash/kilocode.d.ts +0 -9
  155. package/dist/core/configurators/slash/kilocode.js +0 -17
  156. package/dist/core/configurators/slash/opencode.d.ts +0 -12
  157. package/dist/core/configurators/slash/opencode.js +0 -72
  158. package/dist/core/configurators/slash/qoder.d.ts +0 -35
  159. package/dist/core/configurators/slash/qoder.js +0 -76
  160. package/dist/core/configurators/slash/qwen.d.ts +0 -32
  161. package/dist/core/configurators/slash/qwen.js +0 -49
  162. package/dist/core/configurators/slash/registry.d.ts +0 -8
  163. package/dist/core/configurators/slash/registry.js +0 -78
  164. package/dist/core/configurators/slash/roocode.d.ts +0 -9
  165. package/dist/core/configurators/slash/roocode.js +0 -23
  166. package/dist/core/configurators/slash/toml-base.d.ts +0 -10
  167. package/dist/core/configurators/slash/toml-base.js +0 -53
  168. package/dist/core/configurators/slash/windsurf.d.ts +0 -9
  169. package/dist/core/configurators/slash/windsurf.js +0 -23
  170. package/dist/core/templates/agents-root-stub.d.ts +0 -2
  171. package/dist/core/templates/agents-root-stub.js +0 -17
  172. package/dist/core/templates/agents-template.d.ts +0 -2
  173. package/dist/core/templates/agents-template.js +0 -458
  174. package/dist/core/templates/claude-template.d.ts +0 -2
  175. package/dist/core/templates/claude-template.js +0 -2
  176. package/dist/core/templates/cline-template.d.ts +0 -2
  177. package/dist/core/templates/cline-template.js +0 -2
  178. package/dist/core/templates/costrict-template.d.ts +0 -2
  179. package/dist/core/templates/costrict-template.js +0 -2
  180. package/dist/core/templates/project-template.d.ts +0 -8
  181. package/dist/core/templates/project-template.js +0 -32
  182. package/dist/core/templates/slash-command-templates.d.ts +0 -4
  183. package/dist/core/templates/slash-command-templates.js +0 -49
@@ -1,4 +1,42 @@
1
+ /**
2
+ * Update Command
3
+ *
4
+ * Refreshes OpenSpec skills and commands for configured tools.
5
+ * Supports smart update detection to skip updates when already current.
6
+ */
7
+ /**
8
+ * Options for the update command.
9
+ */
10
+ export interface UpdateCommandOptions {
11
+ /** Force update even when tools are up to date */
12
+ force?: boolean;
13
+ }
1
14
  export declare class UpdateCommand {
15
+ private readonly force;
16
+ constructor(options?: UpdateCommandOptions);
2
17
  execute(projectPath: string): Promise<void>;
18
+ /**
19
+ * Display message when all tools are up to date.
20
+ */
21
+ private displayUpToDateMessage;
22
+ /**
23
+ * Display the update plan showing which tools need updating.
24
+ */
25
+ private displayUpdatePlan;
26
+ /**
27
+ * Detect and handle legacy OpenSpec artifacts.
28
+ * Unlike init, update warns but continues if legacy files found in non-interactive mode.
29
+ * Returns array of tool IDs that were newly configured during legacy upgrade.
30
+ */
31
+ private handleLegacyCleanup;
32
+ /**
33
+ * Perform cleanup of legacy artifacts.
34
+ */
35
+ private performLegacyCleanup;
36
+ /**
37
+ * Upgrade legacy tools to new skills system.
38
+ * Returns array of tool IDs that were newly configured.
39
+ */
40
+ private upgradeLegacyTools;
3
41
  }
4
42
  //# sourceMappingURL=update.d.ts.map
@@ -1,88 +1,306 @@
1
+ /**
2
+ * Update Command
3
+ *
4
+ * Refreshes OpenSpec skills and commands for configured tools.
5
+ * Supports smart update detection to skip updates when already current.
6
+ */
1
7
  import path from 'path';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { createRequire } from 'module';
2
11
  import { FileSystemUtils } from '../utils/file-system.js';
3
- import { OPENSPEC_DIR_NAME } from './config.js';
4
- import { ToolRegistry } from './configurators/registry.js';
5
- import { SlashCommandRegistry } from './configurators/slash/registry.js';
6
- import { agentsTemplate } from './templates/agents-template.js';
12
+ import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
13
+ import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
14
+ import { getConfiguredTools, getAllToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
15
+ import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
16
+ import { isInteractive } from '../utils/interactive.js';
17
+ const require = createRequire(import.meta.url);
18
+ const { version: OPENSPEC_VERSION } = require('../../package.json');
7
19
  export class UpdateCommand {
20
+ force;
21
+ constructor(options = {}) {
22
+ this.force = options.force ?? false;
23
+ }
8
24
  async execute(projectPath) {
9
25
  const resolvedProjectPath = path.resolve(projectPath);
10
- const openspecDirName = OPENSPEC_DIR_NAME;
11
- const openspecPath = path.join(resolvedProjectPath, openspecDirName);
26
+ const openspecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
12
27
  // 1. Check openspec directory exists
13
28
  if (!await FileSystemUtils.directoryExists(openspecPath)) {
14
29
  throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
15
30
  }
16
- // 2. Update AGENTS.md (full replacement)
17
- const agentsPath = path.join(openspecPath, 'AGENTS.md');
18
- await FileSystemUtils.writeFile(agentsPath, agentsTemplate);
19
- // 3. Update existing AI tool configuration files only
20
- const configurators = ToolRegistry.getAll();
21
- const slashConfigurators = SlashCommandRegistry.getAll();
22
- const updatedFiles = [];
23
- const createdFiles = [];
24
- const failedFiles = [];
25
- const updatedSlashFiles = [];
26
- const failedSlashTools = [];
27
- for (const configurator of configurators) {
28
- const configFilePath = path.join(resolvedProjectPath, configurator.configFileName);
29
- const fileExists = await FileSystemUtils.fileExists(configFilePath);
30
- const shouldConfigure = fileExists || configurator.configFileName === 'AGENTS.md';
31
- if (!shouldConfigure) {
31
+ // 2. Detect and handle legacy artifacts + upgrade legacy tools to new skills
32
+ const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath);
33
+ // 3. Find configured tools
34
+ const configuredTools = getConfiguredTools(resolvedProjectPath);
35
+ if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
36
+ console.log(chalk.yellow('No configured tools found.'));
37
+ console.log(chalk.dim('Run "openspec init" to set up tools.'));
38
+ return;
39
+ }
40
+ // 4. Check version status for all configured tools
41
+ const toolStatuses = getAllToolVersionStatus(resolvedProjectPath, OPENSPEC_VERSION);
42
+ // 5. Smart update detection
43
+ const toolsNeedingUpdate = toolStatuses.filter((s) => s.needsUpdate);
44
+ const toolsUpToDate = toolStatuses.filter((s) => !s.needsUpdate);
45
+ if (!this.force && toolsNeedingUpdate.length === 0) {
46
+ // All tools are up to date
47
+ this.displayUpToDateMessage(toolStatuses);
48
+ return;
49
+ }
50
+ // 6. Display update plan
51
+ if (this.force) {
52
+ console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
53
+ }
54
+ else {
55
+ this.displayUpdatePlan(toolsNeedingUpdate, toolsUpToDate);
56
+ }
57
+ console.log();
58
+ // 7. Prepare templates
59
+ const skillTemplates = getSkillTemplates();
60
+ const commandContents = getCommandContents();
61
+ // 8. Update tools (all if force, otherwise only those needing update)
62
+ const toolsToUpdate = this.force ? configuredTools : toolsNeedingUpdate.map((s) => s.toolId);
63
+ const updatedTools = [];
64
+ const failedTools = [];
65
+ for (const toolId of toolsToUpdate) {
66
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
67
+ if (!tool?.skillsDir)
32
68
  continue;
33
- }
69
+ const spinner = ora(`Updating ${tool.name}...`).start();
34
70
  try {
35
- if (fileExists && !await FileSystemUtils.canWriteFile(configFilePath)) {
36
- throw new Error(`Insufficient permissions to modify ${configurator.configFileName}`);
71
+ const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
72
+ // Update skill files
73
+ for (const { template, dirName } of skillTemplates) {
74
+ const skillDir = path.join(skillsDir, dirName);
75
+ const skillFile = path.join(skillDir, 'SKILL.md');
76
+ const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
77
+ await FileSystemUtils.writeFile(skillFile, skillContent);
37
78
  }
38
- await configurator.configure(resolvedProjectPath, openspecPath);
39
- updatedFiles.push(configurator.configFileName);
40
- if (!fileExists) {
41
- createdFiles.push(configurator.configFileName);
79
+ // Update commands
80
+ const adapter = CommandAdapterRegistry.get(tool.value);
81
+ if (adapter) {
82
+ const generatedCommands = generateCommands(commandContents, adapter);
83
+ for (const cmd of generatedCommands) {
84
+ const commandFile = path.join(resolvedProjectPath, cmd.path);
85
+ await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
86
+ }
42
87
  }
88
+ spinner.succeed(`Updated ${tool.name}`);
89
+ updatedTools.push(tool.name);
43
90
  }
44
91
  catch (error) {
45
- failedFiles.push(configurator.configFileName);
46
- console.error(`Failed to update ${configurator.configFileName}: ${error instanceof Error ? error.message : String(error)}`);
92
+ spinner.fail(`Failed to update ${tool.name}`);
93
+ failedTools.push({
94
+ name: tool.name,
95
+ error: error instanceof Error ? error.message : String(error)
96
+ });
47
97
  }
48
98
  }
49
- for (const slashConfigurator of slashConfigurators) {
50
- if (!slashConfigurator.isAvailable) {
51
- continue;
99
+ // 9. Summary
100
+ console.log();
101
+ if (updatedTools.length > 0) {
102
+ console.log(chalk.green(`✓ Updated: ${updatedTools.join(', ')} (v${OPENSPEC_VERSION})`));
103
+ }
104
+ if (failedTools.length > 0) {
105
+ console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
106
+ }
107
+ // 10. Show onboarding message for newly configured tools from legacy upgrade
108
+ if (newlyConfiguredTools.length > 0) {
109
+ console.log();
110
+ console.log(chalk.bold('Getting started:'));
111
+ console.log(' /opsx:new Start a new change');
112
+ console.log(' /opsx:continue Create the next artifact');
113
+ console.log(' /opsx:apply Implement tasks');
114
+ console.log();
115
+ console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
116
+ }
117
+ console.log();
118
+ console.log(chalk.dim('Restart your IDE for changes to take effect.'));
119
+ }
120
+ /**
121
+ * Display message when all tools are up to date.
122
+ */
123
+ displayUpToDateMessage(toolStatuses) {
124
+ const toolNames = toolStatuses.map((s) => s.toolId);
125
+ console.log(chalk.green(`✓ All ${toolStatuses.length} tool(s) up to date (v${OPENSPEC_VERSION})`));
126
+ console.log(chalk.dim(` Tools: ${toolNames.join(', ')}`));
127
+ console.log();
128
+ console.log(chalk.dim('Use --force to refresh skills anyway.'));
129
+ }
130
+ /**
131
+ * Display the update plan showing which tools need updating.
132
+ */
133
+ displayUpdatePlan(needingUpdate, upToDate) {
134
+ const updates = needingUpdate.map((s) => {
135
+ const fromVersion = s.generatedByVersion ?? 'unknown';
136
+ return `${s.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
137
+ });
138
+ console.log(`Updating ${needingUpdate.length} tool(s): ${updates.join(', ')}`);
139
+ if (upToDate.length > 0) {
140
+ const upToDateNames = upToDate.map((s) => s.toolId);
141
+ console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
142
+ }
143
+ }
144
+ /**
145
+ * Detect and handle legacy OpenSpec artifacts.
146
+ * Unlike init, update warns but continues if legacy files found in non-interactive mode.
147
+ * Returns array of tool IDs that were newly configured during legacy upgrade.
148
+ */
149
+ async handleLegacyCleanup(projectPath) {
150
+ // Detect legacy artifacts
151
+ const detection = await detectLegacyArtifacts(projectPath);
152
+ if (!detection.hasLegacyArtifacts) {
153
+ return []; // No legacy artifacts found
154
+ }
155
+ // Show what was detected
156
+ console.log();
157
+ console.log(formatDetectionSummary(detection));
158
+ console.log();
159
+ const canPrompt = isInteractive();
160
+ if (this.force) {
161
+ // --force flag: proceed with cleanup automatically
162
+ await this.performLegacyCleanup(projectPath, detection);
163
+ // Then upgrade legacy tools to new skills
164
+ return this.upgradeLegacyTools(projectPath, detection, canPrompt);
165
+ }
166
+ if (!canPrompt) {
167
+ // Non-interactive mode without --force: warn and continue
168
+ // (Unlike init, update doesn't abort - user may just want to update skills)
169
+ console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
170
+ console.log();
171
+ return [];
172
+ }
173
+ // Interactive mode: prompt for confirmation
174
+ const { confirm } = await import('@inquirer/prompts');
175
+ const shouldCleanup = await confirm({
176
+ message: 'Upgrade and clean up legacy files?',
177
+ default: true,
178
+ });
179
+ if (shouldCleanup) {
180
+ await this.performLegacyCleanup(projectPath, detection);
181
+ // Then upgrade legacy tools to new skills
182
+ return this.upgradeLegacyTools(projectPath, detection, canPrompt);
183
+ }
184
+ else {
185
+ console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
186
+ console.log();
187
+ return [];
188
+ }
189
+ }
190
+ /**
191
+ * Perform cleanup of legacy artifacts.
192
+ */
193
+ async performLegacyCleanup(projectPath, detection) {
194
+ const spinner = ora('Cleaning up legacy files...').start();
195
+ const result = await cleanupLegacyArtifacts(projectPath, detection);
196
+ spinner.succeed('Legacy files cleaned up');
197
+ const summary = formatCleanupSummary(result);
198
+ if (summary) {
199
+ console.log();
200
+ console.log(summary);
201
+ }
202
+ console.log();
203
+ }
204
+ /**
205
+ * Upgrade legacy tools to new skills system.
206
+ * Returns array of tool IDs that were newly configured.
207
+ */
208
+ async upgradeLegacyTools(projectPath, detection, canPrompt) {
209
+ // Get tools that had legacy artifacts
210
+ const legacyTools = getToolsFromLegacyArtifacts(detection);
211
+ if (legacyTools.length === 0) {
212
+ return [];
213
+ }
214
+ // Get currently configured tools
215
+ const configuredTools = getConfiguredTools(projectPath);
216
+ const configuredSet = new Set(configuredTools);
217
+ // Filter to tools that aren't already configured
218
+ const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
219
+ if (unconfiguredLegacyTools.length === 0) {
220
+ return [];
221
+ }
222
+ // Get valid tools (those with skillsDir)
223
+ const validToolIds = new Set(getToolsWithSkillsDir());
224
+ const validUnconfiguredTools = unconfiguredLegacyTools.filter((t) => validToolIds.has(t));
225
+ if (validUnconfiguredTools.length === 0) {
226
+ return [];
227
+ }
228
+ // Show what tools were detected from legacy artifacts
229
+ console.log(chalk.bold('Tools detected from legacy artifacts:'));
230
+ for (const toolId of validUnconfiguredTools) {
231
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
232
+ console.log(` • ${tool?.name || toolId}`);
233
+ }
234
+ console.log();
235
+ let selectedTools;
236
+ if (this.force || !canPrompt) {
237
+ // Non-interactive with --force: auto-select detected tools
238
+ selectedTools = validUnconfiguredTools;
239
+ console.log(`Setting up skills for: ${selectedTools.join(', ')}`);
240
+ }
241
+ else {
242
+ // Interactive mode: prompt for tool selection with detected tools pre-selected
243
+ const { searchableMultiSelect } = await import('../prompts/searchable-multi-select.js');
244
+ const sortedChoices = validUnconfiguredTools.map((toolId) => {
245
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
246
+ return {
247
+ name: tool?.name || toolId,
248
+ value: toolId,
249
+ configured: false,
250
+ preSelected: true, // Pre-select all detected legacy tools
251
+ };
252
+ });
253
+ selectedTools = await searchableMultiSelect({
254
+ message: 'Select tools to set up with the new skill system:',
255
+ pageSize: 15,
256
+ choices: sortedChoices,
257
+ validate: (_selected) => true, // Allow empty selection (user can skip)
258
+ });
259
+ if (selectedTools.length === 0) {
260
+ console.log(chalk.dim('Skipping tool setup.'));
261
+ console.log();
262
+ return [];
52
263
  }
264
+ }
265
+ // Create skills for selected tools
266
+ const newlyConfigured = [];
267
+ const skillTemplates = getSkillTemplates();
268
+ const commandContents = getCommandContents();
269
+ for (const toolId of selectedTools) {
270
+ const tool = AI_TOOLS.find((t) => t.value === toolId);
271
+ if (!tool?.skillsDir)
272
+ continue;
273
+ const spinner = ora(`Setting up ${tool.name}...`).start();
53
274
  try {
54
- const updated = await slashConfigurator.updateExisting(resolvedProjectPath, openspecPath);
55
- updatedSlashFiles.push(...updated);
275
+ const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
276
+ // Create skill files
277
+ for (const { template, dirName } of skillTemplates) {
278
+ const skillDir = path.join(skillsDir, dirName);
279
+ const skillFile = path.join(skillDir, 'SKILL.md');
280
+ const skillContent = generateSkillContent(template, OPENSPEC_VERSION);
281
+ await FileSystemUtils.writeFile(skillFile, skillContent);
282
+ }
283
+ // Create commands
284
+ const adapter = CommandAdapterRegistry.get(tool.value);
285
+ if (adapter) {
286
+ const generatedCommands = generateCommands(commandContents, adapter);
287
+ for (const cmd of generatedCommands) {
288
+ const commandFile = path.join(projectPath, cmd.path);
289
+ await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
290
+ }
291
+ }
292
+ spinner.succeed(`Setup complete for ${tool.name}`);
293
+ newlyConfigured.push(toolId);
56
294
  }
57
295
  catch (error) {
58
- failedSlashTools.push(slashConfigurator.toolId);
59
- console.error(`Failed to update slash commands for ${slashConfigurator.toolId}: ${error instanceof Error ? error.message : String(error)}`);
296
+ spinner.fail(`Failed to set up ${tool.name}`);
297
+ console.log(chalk.red(` ${error instanceof Error ? error.message : String(error)}`));
60
298
  }
61
299
  }
62
- const summaryParts = [];
63
- const instructionFiles = ['openspec/AGENTS.md'];
64
- if (updatedFiles.includes('AGENTS.md')) {
65
- instructionFiles.push(createdFiles.includes('AGENTS.md') ? 'AGENTS.md (created)' : 'AGENTS.md');
66
- }
67
- summaryParts.push(`Updated OpenSpec instructions (${instructionFiles.join(', ')})`);
68
- const aiToolFiles = updatedFiles.filter((file) => file !== 'AGENTS.md');
69
- if (aiToolFiles.length > 0) {
70
- summaryParts.push(`Updated AI tool files: ${aiToolFiles.join(', ')}`);
71
- }
72
- if (updatedSlashFiles.length > 0) {
73
- // Normalize to forward slashes for cross-platform log consistency
74
- const normalized = updatedSlashFiles.map((p) => FileSystemUtils.toPosixPath(p));
75
- summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
76
- }
77
- const failedItems = [
78
- ...failedFiles,
79
- ...failedSlashTools.map((toolId) => `slash command refresh (${toolId})`),
80
- ];
81
- if (failedItems.length > 0) {
82
- summaryParts.push(`Failed to update: ${failedItems.join(', ')}`);
83
- }
84
- console.log(summaryParts.join(' | '));
85
- // No additional notes
300
+ if (newlyConfigured.length > 0) {
301
+ console.log();
302
+ }
303
+ return newlyConfigured;
86
304
  }
87
305
  }
88
306
  //# sourceMappingURL=update.js.map
@@ -0,0 +1,27 @@
1
+ interface Choice {
2
+ name: string;
3
+ value: string;
4
+ description?: string;
5
+ configured?: boolean;
6
+ configuredLabel?: string;
7
+ preSelected?: boolean;
8
+ }
9
+ interface Config {
10
+ message: string;
11
+ choices: Choice[];
12
+ pageSize?: number;
13
+ validate?: (selected: string[]) => boolean | string;
14
+ }
15
+ /**
16
+ * A searchable multi-select prompt with visible search box,
17
+ * selected items display, and intuitive keyboard navigation.
18
+ *
19
+ * - Type to filter choices
20
+ * - ↑↓ to navigate
21
+ * - Enter to add highlighted item
22
+ * - Backspace to remove last selected item (or delete search char)
23
+ * - Tab to confirm selections
24
+ */
25
+ export declare function searchableMultiSelect(config: Config): Promise<string[]>;
26
+ export default searchableMultiSelect;
27
+ //# sourceMappingURL=searchable-multi-select.d.ts.map
@@ -0,0 +1,149 @@
1
+ import chalk from 'chalk';
2
+ /**
3
+ * Create the searchable multi-select prompt.
4
+ * Uses dynamic import to prevent pre-commit hook hangs (see #367).
5
+ */
6
+ async function createSearchableMultiSelect() {
7
+ const { createPrompt, useState, useKeypress, useMemo, usePrefix, isEnterKey, isBackspaceKey, isUpKey, isDownKey, } = await import('@inquirer/core');
8
+ return createPrompt((config, done) => {
9
+ const { message, choices, pageSize = 15, validate } = config;
10
+ const [searchText, setSearchText] = useState('');
11
+ const [selectedValues, setSelectedValues] = useState(() => choices.filter(c => c.preSelected).map(c => c.value));
12
+ const [cursor, setCursor] = useState(0);
13
+ const [status, setStatus] = useState('idle');
14
+ const [error, setError] = useState(null);
15
+ const prefix = usePrefix({ status });
16
+ // Filter choices by search
17
+ const filteredChoices = useMemo(() => {
18
+ if (!searchText.trim())
19
+ return choices;
20
+ const term = searchText.toLowerCase();
21
+ return choices.filter((c) => c.name.toLowerCase().includes(term) ||
22
+ c.value.toLowerCase().includes(term));
23
+ }, [searchText, choices]);
24
+ const selectedSet = useMemo(() => new Set(selectedValues), [selectedValues]);
25
+ const choiceMap = useMemo(() => new Map(choices.map((c) => [c.value, c])), [choices]);
26
+ useKeypress((key) => {
27
+ if (status === 'done')
28
+ return;
29
+ // Tab to confirm
30
+ if (key.name === 'tab') {
31
+ if (validate) {
32
+ const result = validate(selectedValues);
33
+ if (result !== true) {
34
+ setError(typeof result === 'string' ? result : 'Invalid');
35
+ return;
36
+ }
37
+ }
38
+ setStatus('done');
39
+ done(selectedValues);
40
+ return;
41
+ }
42
+ // Enter to add item
43
+ if (isEnterKey(key)) {
44
+ const choice = filteredChoices[cursor];
45
+ if (choice && !selectedSet.has(choice.value)) {
46
+ setSelectedValues([...selectedValues, choice.value]);
47
+ setSearchText('');
48
+ setCursor(0);
49
+ }
50
+ return;
51
+ }
52
+ // Backspace to remove or delete search char
53
+ if (isBackspaceKey(key)) {
54
+ if (searchText === '' && selectedValues.length > 0) {
55
+ setSelectedValues(selectedValues.slice(0, -1));
56
+ }
57
+ else {
58
+ setSearchText(searchText.slice(0, -1));
59
+ setCursor(0);
60
+ }
61
+ return;
62
+ }
63
+ // Navigation
64
+ if (isUpKey(key)) {
65
+ setCursor(Math.max(0, cursor - 1));
66
+ return;
67
+ }
68
+ if (isDownKey(key)) {
69
+ setCursor(Math.min(filteredChoices.length - 1, cursor + 1));
70
+ return;
71
+ }
72
+ // Character input - handle printable characters
73
+ if (key.name && key.name.length === 1 && !key.ctrl) {
74
+ setSearchText(searchText + key.name);
75
+ setCursor(0);
76
+ }
77
+ });
78
+ // Render done state
79
+ if (status === 'done') {
80
+ const names = selectedValues
81
+ .map((v) => choiceMap.get(v)?.name ?? v)
82
+ .join(', ');
83
+ return `${prefix} ${chalk.bold(message)} ${chalk.cyan(names || '(none)')}`;
84
+ }
85
+ // Render active state
86
+ const lines = [];
87
+ lines.push(`${prefix} ${chalk.bold(message)}`);
88
+ // Selected chips
89
+ const chips = selectedValues.length > 0
90
+ ? selectedValues
91
+ .map((v) => chalk.bgCyan.black(` ${choiceMap.get(v)?.name} `))
92
+ .join(' ')
93
+ : chalk.dim('(none selected)');
94
+ lines.push(` Selected: ${chips}`);
95
+ // Search box
96
+ lines.push(` Search: ${chalk.yellow('[')}${searchText || chalk.dim('type to filter')}${chalk.yellow(']')}`);
97
+ // Instructions
98
+ lines.push(` ${chalk.cyan('↑↓')} navigate • ${chalk.cyan('Enter')} add • ${chalk.cyan('Backspace')} remove • ${chalk.cyan('Tab')} confirm`);
99
+ // List
100
+ if (filteredChoices.length === 0) {
101
+ lines.push(chalk.yellow(' No matches'));
102
+ }
103
+ else {
104
+ // Calculate pagination
105
+ const startIndex = Math.max(0, Math.min(cursor - Math.floor(pageSize / 2), filteredChoices.length - pageSize));
106
+ const endIndex = Math.min(startIndex + pageSize, filteredChoices.length);
107
+ const visibleChoices = filteredChoices.slice(startIndex, endIndex);
108
+ for (let i = 0; i < visibleChoices.length; i++) {
109
+ const item = visibleChoices[i];
110
+ const actualIndex = startIndex + i;
111
+ const isActive = actualIndex === cursor;
112
+ const selected = selectedSet.has(item.value);
113
+ const icon = selected ? chalk.green('◉') : chalk.dim('○');
114
+ const arrow = isActive ? chalk.cyan('›') : ' ';
115
+ const name = isActive ? chalk.cyan(item.name) : item.name;
116
+ const isRefresh = selected && item.configured;
117
+ const suffix = selected
118
+ ? chalk.dim(isRefresh ? ' (refresh)' : ' (selected)')
119
+ : '';
120
+ lines.push(` ${arrow} ${icon} ${name}${suffix}`);
121
+ }
122
+ // Show pagination indicator if needed
123
+ if (filteredChoices.length > pageSize) {
124
+ const currentPage = Math.floor(cursor / pageSize) + 1;
125
+ const totalPages = Math.ceil(filteredChoices.length / pageSize);
126
+ lines.push(chalk.dim(` (${currentPage}/${totalPages})`));
127
+ }
128
+ }
129
+ if (error)
130
+ lines.push(chalk.red(` ${error}`));
131
+ return lines.join('\n');
132
+ });
133
+ }
134
+ /**
135
+ * A searchable multi-select prompt with visible search box,
136
+ * selected items display, and intuitive keyboard navigation.
137
+ *
138
+ * - Type to filter choices
139
+ * - ↑↓ to navigate
140
+ * - Enter to add highlighted item
141
+ * - Backspace to remove last selected item (or delete search char)
142
+ * - Tab to confirm selections
143
+ */
144
+ export async function searchableMultiSelect(config) {
145
+ const prompt = await createSearchableMultiSelect();
146
+ return prompt(config);
147
+ }
148
+ export default searchableMultiSelect;
149
+ //# sourceMappingURL=searchable-multi-select.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ASCII art animation patterns for the welcome screen.
3
+ * OpenSpec logo animation - diamond/rhombus shape with hollow center "O".
4
+ */
5
+ /**
6
+ * Welcome animation frames - OpenSpec logo building from center
7
+ * 7 rows × 6 columns diamond with hollow center "O"
8
+ * Center bar is 2 cols × 3 rows (rows 3,4,5 cols 3,4)
9
+ * Each frame is an array of strings (lines of ASCII art)
10
+ * Grid: 6 cols × 2 chars = 12 chars wide
11
+ */
12
+ export declare const WELCOME_ANIMATION: {
13
+ interval: number;
14
+ frames: string[][];
15
+ };
16
+ //# sourceMappingURL=ascii-patterns.d.ts.map