@fission-ai/openspec 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +2 -0
- package/dist/commands/config.d.ts +28 -0
- package/dist/commands/config.js +359 -5
- package/dist/core/available-tools.d.ts +16 -0
- package/dist/core/available-tools.js +30 -0
- package/dist/core/command-generation/adapters/index.d.ts +2 -0
- package/dist/core/command-generation/adapters/index.js +2 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +26 -0
- package/dist/core/command-generation/adapters/opencode.js +4 -1
- package/dist/core/command-generation/adapters/pi.d.ts +14 -0
- package/dist/core/command-generation/adapters/pi.js +41 -0
- package/dist/core/command-generation/registry.js +4 -0
- package/dist/core/completions/command-registry.js +5 -0
- package/dist/core/config-schema.d.ts +10 -0
- package/dist/core/config-schema.js +14 -1
- package/dist/core/config.js +2 -0
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/global-config.js +12 -2
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +206 -42
- package/dist/core/legacy-cleanup.js +1 -0
- package/dist/core/migration.d.ts +23 -0
- package/dist/core/migration.js +108 -0
- package/dist/core/profile-sync-drift.d.ts +38 -0
- package/dist/core/profile-sync-drift.js +200 -0
- package/dist/core/profiles.d.ts +26 -0
- package/dist/core/profiles.js +40 -0
- package/dist/core/shared/index.d.ts +1 -1
- package/dist/core/shared/index.js +1 -1
- package/dist/core/shared/skill-generation.d.ts +16 -8
- package/dist/core/shared/skill-generation.js +42 -22
- package/dist/core/shared/tool-detection.d.ts +8 -3
- package/dist/core/shared/tool-detection.js +18 -0
- package/dist/core/templates/index.d.ts +1 -1
- package/dist/core/templates/index.js +2 -2
- package/dist/core/templates/skill-templates.d.ts +15 -118
- package/dist/core/templates/skill-templates.js +14 -3424
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +308 -0
- package/dist/core/templates/workflows/archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/archive-change.js +271 -0
- package/dist/core/templates/workflows/bulk-archive-change.d.ts +10 -0
- package/dist/core/templates/workflows/bulk-archive-change.js +488 -0
- package/dist/core/templates/workflows/continue-change.d.ts +10 -0
- package/dist/core/templates/workflows/continue-change.js +232 -0
- package/dist/core/templates/workflows/explore.d.ts +10 -0
- package/dist/core/templates/workflows/explore.js +461 -0
- package/dist/core/templates/workflows/feedback.d.ts +9 -0
- package/dist/core/templates/workflows/feedback.js +108 -0
- package/dist/core/templates/workflows/ff-change.d.ts +10 -0
- package/dist/core/templates/workflows/ff-change.js +198 -0
- package/dist/core/templates/workflows/new-change.d.ts +10 -0
- package/dist/core/templates/workflows/new-change.js +143 -0
- package/dist/core/templates/workflows/onboard.d.ts +10 -0
- package/dist/core/templates/workflows/onboard.js +565 -0
- package/dist/core/templates/workflows/propose.d.ts +10 -0
- package/dist/core/templates/workflows/propose.js +216 -0
- package/dist/core/templates/workflows/sync-specs.d.ts +10 -0
- package/dist/core/templates/workflows/sync-specs.js +272 -0
- package/dist/core/templates/workflows/verify-change.d.ts +10 -0
- package/dist/core/templates/workflows/verify-change.js +332 -0
- package/dist/core/update.d.ts +36 -1
- package/dist/core/update.js +292 -61
- package/dist/prompts/searchable-multi-select.d.ts +3 -2
- package/dist/prompts/searchable-multi-select.js +22 -12
- package/dist/utils/command-references.d.ts +18 -0
- package/dist/utils/command-references.js +20 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +2 -0
- package/package.json +1 -1
package/dist/core/update.js
CHANGED
|
@@ -2,20 +2,39 @@
|
|
|
2
2
|
* Update Command
|
|
3
3
|
*
|
|
4
4
|
* Refreshes OpenSpec skills and commands for configured tools.
|
|
5
|
-
* Supports
|
|
5
|
+
* Supports profile-aware updates, delivery changes, migration, and smart update detection.
|
|
6
6
|
*/
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import ora from 'ora';
|
|
10
|
+
import * as fs from 'fs';
|
|
10
11
|
import { createRequire } from 'module';
|
|
11
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
13
|
+
import { transformToHyphenCommands } from '../utils/command-references.js';
|
|
12
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
13
15
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
14
|
-
import {
|
|
16
|
+
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
|
|
15
17
|
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
|
|
16
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
|
+
import { getGlobalConfig } from './global-config.js';
|
|
20
|
+
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
21
|
+
import { getAvailableTools } from './available-tools.js';
|
|
22
|
+
import { WORKFLOW_TO_SKILL_DIR, getCommandConfiguredTools, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
23
|
+
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, } from './migration.js';
|
|
17
24
|
const require = createRequire(import.meta.url);
|
|
18
25
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
26
|
+
/**
|
|
27
|
+
* Scans installed workflow artifacts (skills and managed commands) across all configured tools.
|
|
28
|
+
* Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
|
|
29
|
+
*
|
|
30
|
+
* Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs.
|
|
31
|
+
*/
|
|
32
|
+
export function scanInstalledWorkflows(projectPath, toolIds) {
|
|
33
|
+
const tools = toolIds
|
|
34
|
+
.map((id) => AI_TOOLS.find((t) => t.value === id))
|
|
35
|
+
.filter((t) => t != null);
|
|
36
|
+
return scanInstalledWorkflowsShared(projectPath, tools);
|
|
37
|
+
}
|
|
19
38
|
export class UpdateCommand {
|
|
20
39
|
force;
|
|
21
40
|
constructor(options = {}) {
|
|
@@ -28,40 +47,75 @@ export class UpdateCommand {
|
|
|
28
47
|
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
|
29
48
|
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
|
30
49
|
}
|
|
31
|
-
// 2.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
// 2. Perform one-time migration if needed before any legacy upgrade generation.
|
|
51
|
+
// Use detected tool directories to preserve existing opsx skills/commands.
|
|
52
|
+
const detectedTools = getAvailableTools(resolvedProjectPath);
|
|
53
|
+
migrateIfNeededShared(resolvedProjectPath, detectedTools);
|
|
54
|
+
// 3. Read global config for profile/delivery
|
|
55
|
+
const globalConfig = getGlobalConfig();
|
|
56
|
+
const profile = globalConfig.profile ?? 'core';
|
|
57
|
+
const delivery = globalConfig.delivery ?? 'both';
|
|
58
|
+
const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
59
|
+
const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
60
|
+
const shouldGenerateSkills = delivery !== 'commands';
|
|
61
|
+
const shouldGenerateCommands = delivery !== 'skills';
|
|
62
|
+
// 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config
|
|
63
|
+
const newlyConfiguredTools = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
|
|
64
|
+
// 5. Find configured tools
|
|
65
|
+
const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
|
|
35
66
|
if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
|
|
36
67
|
console.log(chalk.yellow('No configured tools found.'));
|
|
37
68
|
console.log(chalk.dim('Run "openspec init" to set up tools.'));
|
|
38
69
|
return;
|
|
39
70
|
}
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
// 6. Check version status for all configured tools
|
|
72
|
+
const commandConfiguredTools = getCommandConfiguredTools(resolvedProjectPath);
|
|
73
|
+
const commandConfiguredSet = new Set(commandConfiguredTools);
|
|
74
|
+
const toolStatuses = configuredTools.map((toolId) => {
|
|
75
|
+
const status = getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION);
|
|
76
|
+
if (!status.configured && commandConfiguredSet.has(toolId)) {
|
|
77
|
+
return { ...status, configured: true };
|
|
78
|
+
}
|
|
79
|
+
return status;
|
|
80
|
+
});
|
|
81
|
+
const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
|
|
82
|
+
// 7. Smart update detection
|
|
83
|
+
const toolsNeedingVersionUpdate = toolStatuses
|
|
84
|
+
.filter((s) => s.needsUpdate)
|
|
85
|
+
.map((s) => s.toolId);
|
|
86
|
+
const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
|
|
87
|
+
const toolsToUpdateSet = new Set([
|
|
88
|
+
...toolsNeedingVersionUpdate,
|
|
89
|
+
...toolsNeedingConfigSync,
|
|
90
|
+
]);
|
|
91
|
+
const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
|
|
92
|
+
if (!this.force && toolsToUpdateSet.size === 0) {
|
|
46
93
|
// All tools are up to date
|
|
47
94
|
this.displayUpToDateMessage(toolStatuses);
|
|
95
|
+
// Still check for new tool directories and extra workflows
|
|
96
|
+
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
97
|
+
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
|
|
48
98
|
return;
|
|
49
99
|
}
|
|
50
|
-
//
|
|
100
|
+
// 8. Display update plan
|
|
51
101
|
if (this.force) {
|
|
52
102
|
console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
|
|
53
103
|
}
|
|
54
104
|
else {
|
|
55
|
-
this.displayUpdatePlan(
|
|
105
|
+
this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
|
|
56
106
|
}
|
|
57
107
|
console.log();
|
|
58
|
-
//
|
|
59
|
-
const skillTemplates = getSkillTemplates();
|
|
60
|
-
const commandContents = getCommandContents();
|
|
61
|
-
//
|
|
62
|
-
const toolsToUpdate = this.force ? configuredTools :
|
|
108
|
+
// 9. Determine what to generate based on delivery
|
|
109
|
+
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
|
|
110
|
+
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
|
|
111
|
+
// 10. Update tools (all if force, otherwise only those needing update)
|
|
112
|
+
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
|
|
63
113
|
const updatedTools = [];
|
|
64
114
|
const failedTools = [];
|
|
115
|
+
let removedCommandCount = 0;
|
|
116
|
+
let removedSkillCount = 0;
|
|
117
|
+
let removedDeselectedCommandCount = 0;
|
|
118
|
+
let removedDeselectedSkillCount = 0;
|
|
65
119
|
for (const toolId of toolsToUpdate) {
|
|
66
120
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
67
121
|
if (!tool?.skillsDir)
|
|
@@ -69,22 +123,38 @@ export class UpdateCommand {
|
|
|
69
123
|
const spinner = ora(`Updating ${tool.name}...`).start();
|
|
70
124
|
try {
|
|
71
125
|
const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
126
|
+
// Generate skill files if delivery includes skills
|
|
127
|
+
if (shouldGenerateSkills) {
|
|
128
|
+
for (const { template, dirName } of skillTemplates) {
|
|
129
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
130
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
131
|
+
// Use hyphen-based command references for OpenCode
|
|
132
|
+
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
133
|
+
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
134
|
+
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
135
|
+
}
|
|
136
|
+
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, desiredWorkflows);
|
|
137
|
+
}
|
|
138
|
+
// Delete skill directories if delivery is commands-only
|
|
139
|
+
if (!shouldGenerateSkills) {
|
|
140
|
+
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
78
141
|
}
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
142
|
+
// Generate commands if delivery includes commands
|
|
143
|
+
if (shouldGenerateCommands) {
|
|
144
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
145
|
+
if (adapter) {
|
|
146
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
147
|
+
for (const cmd of generatedCommands) {
|
|
148
|
+
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
|
|
149
|
+
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
150
|
+
}
|
|
151
|
+
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, desiredWorkflows);
|
|
86
152
|
}
|
|
87
153
|
}
|
|
154
|
+
// Delete command files if delivery is skills-only
|
|
155
|
+
if (!shouldGenerateCommands) {
|
|
156
|
+
removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
|
|
157
|
+
}
|
|
88
158
|
spinner.succeed(`Updated ${tool.name}`);
|
|
89
159
|
updatedTools.push(tool.name);
|
|
90
160
|
}
|
|
@@ -96,7 +166,7 @@ export class UpdateCommand {
|
|
|
96
166
|
});
|
|
97
167
|
}
|
|
98
168
|
}
|
|
99
|
-
//
|
|
169
|
+
// 11. Summary
|
|
100
170
|
console.log();
|
|
101
171
|
if (updatedTools.length > 0) {
|
|
102
172
|
console.log(chalk.green(`✓ Updated: ${updatedTools.join(', ')} (v${OPENSPEC_VERSION})`));
|
|
@@ -104,7 +174,19 @@ export class UpdateCommand {
|
|
|
104
174
|
if (failedTools.length > 0) {
|
|
105
175
|
console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
|
|
106
176
|
}
|
|
107
|
-
|
|
177
|
+
if (removedCommandCount > 0) {
|
|
178
|
+
console.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
|
|
179
|
+
}
|
|
180
|
+
if (removedSkillCount > 0) {
|
|
181
|
+
console.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
|
|
182
|
+
}
|
|
183
|
+
if (removedDeselectedCommandCount > 0) {
|
|
184
|
+
console.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
|
|
185
|
+
}
|
|
186
|
+
if (removedDeselectedSkillCount > 0) {
|
|
187
|
+
console.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
|
|
188
|
+
}
|
|
189
|
+
// 12. Show onboarding message for newly configured tools from legacy upgrade
|
|
108
190
|
if (newlyConfiguredTools.length > 0) {
|
|
109
191
|
console.log();
|
|
110
192
|
console.log(chalk.bold('Getting started:'));
|
|
@@ -114,6 +196,16 @@ export class UpdateCommand {
|
|
|
114
196
|
console.log();
|
|
115
197
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
116
198
|
}
|
|
199
|
+
const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
|
|
200
|
+
// 13. Detect new tool directories not currently configured
|
|
201
|
+
this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
|
|
202
|
+
// 14. Display note about extra workflows not in profile
|
|
203
|
+
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
|
|
204
|
+
// 15. List affected tools
|
|
205
|
+
if (updatedTools.length > 0) {
|
|
206
|
+
const toolDisplayNames = updatedTools;
|
|
207
|
+
console.log(chalk.dim(`Tools: ${toolDisplayNames.join(', ')}`));
|
|
208
|
+
}
|
|
117
209
|
console.log();
|
|
118
210
|
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
|
|
119
211
|
}
|
|
@@ -125,28 +217,159 @@ export class UpdateCommand {
|
|
|
125
217
|
console.log(chalk.green(`✓ All ${toolStatuses.length} tool(s) up to date (v${OPENSPEC_VERSION})`));
|
|
126
218
|
console.log(chalk.dim(` Tools: ${toolNames.join(', ')}`));
|
|
127
219
|
console.log();
|
|
128
|
-
console.log(chalk.dim('Use --force to refresh
|
|
220
|
+
console.log(chalk.dim('Use --force to refresh files anyway.'));
|
|
129
221
|
}
|
|
130
222
|
/**
|
|
131
223
|
* Display the update plan showing which tools need updating.
|
|
132
224
|
*/
|
|
133
|
-
displayUpdatePlan(
|
|
134
|
-
const updates =
|
|
135
|
-
const
|
|
136
|
-
|
|
225
|
+
displayUpdatePlan(toolsToUpdate, statusByTool, upToDate) {
|
|
226
|
+
const updates = toolsToUpdate.map((toolId) => {
|
|
227
|
+
const status = statusByTool.get(toolId);
|
|
228
|
+
if (status?.needsUpdate) {
|
|
229
|
+
const fromVersion = status.generatedByVersion ?? 'unknown';
|
|
230
|
+
return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
|
|
231
|
+
}
|
|
232
|
+
return `${toolId} (config sync)`;
|
|
137
233
|
});
|
|
138
|
-
console.log(`Updating ${
|
|
234
|
+
console.log(`Updating ${toolsToUpdate.length} tool(s): ${updates.join(', ')}`);
|
|
139
235
|
if (upToDate.length > 0) {
|
|
140
236
|
const upToDateNames = upToDate.map((s) => s.toolId);
|
|
141
237
|
console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
|
|
142
238
|
}
|
|
143
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Detects new tool directories that aren't currently configured and displays a hint.
|
|
242
|
+
*/
|
|
243
|
+
detectNewTools(projectPath, configuredTools) {
|
|
244
|
+
const availableTools = getAvailableTools(projectPath);
|
|
245
|
+
const configuredSet = new Set(configuredTools);
|
|
246
|
+
const newTools = availableTools.filter((t) => !configuredSet.has(t.value));
|
|
247
|
+
if (newTools.length > 0) {
|
|
248
|
+
const newToolNames = newTools.map((tool) => tool.name);
|
|
249
|
+
const isSingleTool = newToolNames.length === 1;
|
|
250
|
+
const toolNoun = isSingleTool ? 'tool' : 'tools';
|
|
251
|
+
const pronoun = isSingleTool ? 'it' : 'them';
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(chalk.yellow(`Detected new ${toolNoun}: ${newToolNames.join(', ')}. Run 'openspec init' to add ${pronoun}.`));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Displays a note about extra workflows installed that aren't in the current profile.
|
|
258
|
+
*/
|
|
259
|
+
displayExtraWorkflowsNote(projectPath, configuredTools, profileWorkflows) {
|
|
260
|
+
const installedWorkflows = scanInstalledWorkflows(projectPath, configuredTools);
|
|
261
|
+
const profileSet = new Set(profileWorkflows);
|
|
262
|
+
const extraWorkflows = installedWorkflows.filter((w) => !profileSet.has(w));
|
|
263
|
+
if (extraWorkflows.length > 0) {
|
|
264
|
+
console.log(chalk.dim(`Note: ${extraWorkflows.length} extra workflows not in profile (use \`openspec config profile\` to manage)`));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Removes skill directories for workflows when delivery changed to commands-only.
|
|
269
|
+
* Returns the number of directories removed.
|
|
270
|
+
*/
|
|
271
|
+
async removeSkillDirs(skillsDir) {
|
|
272
|
+
let removed = 0;
|
|
273
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
274
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
275
|
+
if (!dirName)
|
|
276
|
+
continue;
|
|
277
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
278
|
+
try {
|
|
279
|
+
if (fs.existsSync(skillDir)) {
|
|
280
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
281
|
+
removed++;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Ignore errors
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return removed;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Removes skill directories for workflows that are no longer selected in the active profile.
|
|
292
|
+
* Returns the number of directories removed.
|
|
293
|
+
*/
|
|
294
|
+
async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) {
|
|
295
|
+
const desiredSet = new Set(desiredWorkflows);
|
|
296
|
+
let removed = 0;
|
|
297
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
298
|
+
if (desiredSet.has(workflow))
|
|
299
|
+
continue;
|
|
300
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
301
|
+
if (!dirName)
|
|
302
|
+
continue;
|
|
303
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
304
|
+
try {
|
|
305
|
+
if (fs.existsSync(skillDir)) {
|
|
306
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
307
|
+
removed++;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Ignore errors
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return removed;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Removes command files for workflows when delivery changed to skills-only.
|
|
318
|
+
* Returns the number of files removed.
|
|
319
|
+
*/
|
|
320
|
+
async removeCommandFiles(projectPath, toolId) {
|
|
321
|
+
let removed = 0;
|
|
322
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
323
|
+
if (!adapter)
|
|
324
|
+
return 0;
|
|
325
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
326
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
327
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
328
|
+
try {
|
|
329
|
+
if (fs.existsSync(fullPath)) {
|
|
330
|
+
await fs.promises.unlink(fullPath);
|
|
331
|
+
removed++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Ignore errors
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return removed;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Removes command files for workflows that are no longer selected in the active profile.
|
|
342
|
+
* Returns the number of files removed.
|
|
343
|
+
*/
|
|
344
|
+
async removeUnselectedCommandFiles(projectPath, toolId, desiredWorkflows) {
|
|
345
|
+
let removed = 0;
|
|
346
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
347
|
+
if (!adapter)
|
|
348
|
+
return 0;
|
|
349
|
+
const desiredSet = new Set(desiredWorkflows);
|
|
350
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
351
|
+
if (desiredSet.has(workflow))
|
|
352
|
+
continue;
|
|
353
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
354
|
+
const fullPath = path.isAbsolute(cmdPath) ? cmdPath : path.join(projectPath, cmdPath);
|
|
355
|
+
try {
|
|
356
|
+
if (fs.existsSync(fullPath)) {
|
|
357
|
+
await fs.promises.unlink(fullPath);
|
|
358
|
+
removed++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
// Ignore errors
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return removed;
|
|
366
|
+
}
|
|
144
367
|
/**
|
|
145
368
|
* Detect and handle legacy OpenSpec artifacts.
|
|
146
369
|
* Unlike init, update warns but continues if legacy files found in non-interactive mode.
|
|
147
370
|
* Returns array of tool IDs that were newly configured during legacy upgrade.
|
|
148
371
|
*/
|
|
149
|
-
async handleLegacyCleanup(projectPath) {
|
|
372
|
+
async handleLegacyCleanup(projectPath, desiredWorkflows, delivery) {
|
|
150
373
|
// Detect legacy artifacts
|
|
151
374
|
const detection = await detectLegacyArtifacts(projectPath);
|
|
152
375
|
if (!detection.hasLegacyArtifacts) {
|
|
@@ -161,7 +384,7 @@ export class UpdateCommand {
|
|
|
161
384
|
// --force flag: proceed with cleanup automatically
|
|
162
385
|
await this.performLegacyCleanup(projectPath, detection);
|
|
163
386
|
// Then upgrade legacy tools to new skills
|
|
164
|
-
return this.upgradeLegacyTools(projectPath, detection, canPrompt);
|
|
387
|
+
return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
165
388
|
}
|
|
166
389
|
if (!canPrompt) {
|
|
167
390
|
// Non-interactive mode without --force: warn and continue
|
|
@@ -179,7 +402,7 @@ export class UpdateCommand {
|
|
|
179
402
|
if (shouldCleanup) {
|
|
180
403
|
await this.performLegacyCleanup(projectPath, detection);
|
|
181
404
|
// Then upgrade legacy tools to new skills
|
|
182
|
-
return this.upgradeLegacyTools(projectPath, detection, canPrompt);
|
|
405
|
+
return this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
183
406
|
}
|
|
184
407
|
else {
|
|
185
408
|
console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
|
|
@@ -205,14 +428,14 @@ export class UpdateCommand {
|
|
|
205
428
|
* Upgrade legacy tools to new skills system.
|
|
206
429
|
* Returns array of tool IDs that were newly configured.
|
|
207
430
|
*/
|
|
208
|
-
async upgradeLegacyTools(projectPath, detection, canPrompt) {
|
|
431
|
+
async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) {
|
|
209
432
|
// Get tools that had legacy artifacts
|
|
210
433
|
const legacyTools = getToolsFromLegacyArtifacts(detection);
|
|
211
434
|
if (legacyTools.length === 0) {
|
|
212
435
|
return [];
|
|
213
436
|
}
|
|
214
437
|
// Get currently configured tools
|
|
215
|
-
const configuredTools =
|
|
438
|
+
const configuredTools = getConfiguredToolsForProfileSync(projectPath);
|
|
216
439
|
const configuredSet = new Set(configuredTools);
|
|
217
440
|
// Filter to tools that aren't already configured
|
|
218
441
|
const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
|
|
@@ -262,10 +485,12 @@ export class UpdateCommand {
|
|
|
262
485
|
return [];
|
|
263
486
|
}
|
|
264
487
|
}
|
|
265
|
-
// Create skills for selected tools
|
|
488
|
+
// Create skills/commands for selected tools using effective profile+delivery.
|
|
266
489
|
const newlyConfigured = [];
|
|
267
|
-
const
|
|
268
|
-
const
|
|
490
|
+
const shouldGenerateSkills = delivery !== 'commands';
|
|
491
|
+
const shouldGenerateCommands = delivery !== 'skills';
|
|
492
|
+
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
|
|
493
|
+
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
|
|
269
494
|
for (const toolId of selectedTools) {
|
|
270
495
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
271
496
|
if (!tool?.skillsDir)
|
|
@@ -273,20 +498,26 @@ export class UpdateCommand {
|
|
|
273
498
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
274
499
|
try {
|
|
275
500
|
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
276
|
-
// Create skill files
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
501
|
+
// Create skill files when delivery includes skills
|
|
502
|
+
if (shouldGenerateSkills) {
|
|
503
|
+
for (const { template, dirName } of skillTemplates) {
|
|
504
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
505
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
506
|
+
// Use hyphen-based command references for OpenCode
|
|
507
|
+
const transformer = tool.value === 'opencode' ? transformToHyphenCommands : undefined;
|
|
508
|
+
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
509
|
+
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
510
|
+
}
|
|
282
511
|
}
|
|
283
|
-
// Create commands
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
512
|
+
// Create commands when delivery includes commands
|
|
513
|
+
if (shouldGenerateCommands) {
|
|
514
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
515
|
+
if (adapter) {
|
|
516
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
517
|
+
for (const cmd of generatedCommands) {
|
|
518
|
+
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(projectPath, cmd.path);
|
|
519
|
+
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
520
|
+
}
|
|
290
521
|
}
|
|
291
522
|
}
|
|
292
523
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
@@ -3,6 +3,7 @@ interface Choice {
|
|
|
3
3
|
value: string;
|
|
4
4
|
description?: string;
|
|
5
5
|
configured?: boolean;
|
|
6
|
+
detected?: boolean;
|
|
6
7
|
configuredLabel?: string;
|
|
7
8
|
preSelected?: boolean;
|
|
8
9
|
}
|
|
@@ -18,9 +19,9 @@ interface Config {
|
|
|
18
19
|
*
|
|
19
20
|
* - Type to filter choices
|
|
20
21
|
* - ↑↓ to navigate
|
|
21
|
-
* -
|
|
22
|
+
* - Space to toggle highlighted item selection
|
|
22
23
|
* - Backspace to remove last selected item (or delete search char)
|
|
23
|
-
* -
|
|
24
|
+
* - Enter to confirm selections
|
|
24
25
|
*/
|
|
25
26
|
export declare function searchableMultiSelect(config: Config): Promise<string[]>;
|
|
26
27
|
export default searchableMultiSelect;
|
|
@@ -26,8 +26,8 @@ async function createSearchableMultiSelect() {
|
|
|
26
26
|
useKeypress((key) => {
|
|
27
27
|
if (status === 'done')
|
|
28
28
|
return;
|
|
29
|
-
//
|
|
30
|
-
if (key
|
|
29
|
+
// Enter to confirm/submit
|
|
30
|
+
if (isEnterKey(key)) {
|
|
31
31
|
if (validate) {
|
|
32
32
|
const result = validate(selectedValues);
|
|
33
33
|
if (result !== true) {
|
|
@@ -39,13 +39,16 @@ async function createSearchableMultiSelect() {
|
|
|
39
39
|
done(selectedValues);
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
//
|
|
43
|
-
if (
|
|
42
|
+
// Space to toggle selection
|
|
43
|
+
if (key.name === 'space') {
|
|
44
44
|
const choice = filteredChoices[cursor];
|
|
45
|
-
if (choice
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if (choice) {
|
|
46
|
+
if (selectedSet.has(choice.value)) {
|
|
47
|
+
setSelectedValues(selectedValues.filter(v => v !== choice.value));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
setSelectedValues([...selectedValues, choice.value]);
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
53
|
return;
|
|
51
54
|
}
|
|
@@ -95,7 +98,7 @@ async function createSearchableMultiSelect() {
|
|
|
95
98
|
// Search box
|
|
96
99
|
lines.push(` Search: ${chalk.yellow('[')}${searchText || chalk.dim('type to filter')}${chalk.yellow(']')}`);
|
|
97
100
|
// Instructions
|
|
98
|
-
lines.push(` ${chalk.cyan('↑↓')} navigate • ${chalk.cyan('
|
|
101
|
+
lines.push(` ${chalk.cyan('↑↓')} navigate • ${chalk.cyan('Space')} toggle • ${chalk.cyan('Backspace')} remove • ${chalk.cyan('Enter')} confirm`);
|
|
99
102
|
// List
|
|
100
103
|
if (filteredChoices.length === 0) {
|
|
101
104
|
lines.push(chalk.yellow(' No matches'));
|
|
@@ -114,9 +117,16 @@ async function createSearchableMultiSelect() {
|
|
|
114
117
|
const arrow = isActive ? chalk.cyan('›') : ' ';
|
|
115
118
|
const name = isActive ? chalk.cyan(item.name) : item.name;
|
|
116
119
|
const isRefresh = selected && item.configured;
|
|
120
|
+
const statusLabel = !selected
|
|
121
|
+
? item.configured
|
|
122
|
+
? ' (configured)'
|
|
123
|
+
: item.detected
|
|
124
|
+
? ' (detected)'
|
|
125
|
+
: ''
|
|
126
|
+
: '';
|
|
117
127
|
const suffix = selected
|
|
118
128
|
? chalk.dim(isRefresh ? ' (refresh)' : ' (selected)')
|
|
119
|
-
:
|
|
129
|
+
: chalk.dim(statusLabel);
|
|
120
130
|
lines.push(` ${arrow} ${icon} ${name}${suffix}`);
|
|
121
131
|
}
|
|
122
132
|
// Show pagination indicator if needed
|
|
@@ -137,9 +147,9 @@ async function createSearchableMultiSelect() {
|
|
|
137
147
|
*
|
|
138
148
|
* - Type to filter choices
|
|
139
149
|
* - ↑↓ to navigate
|
|
140
|
-
* -
|
|
150
|
+
* - Space to toggle highlighted item selection
|
|
141
151
|
* - Backspace to remove last selected item (or delete search char)
|
|
142
|
-
* -
|
|
152
|
+
* - Enter to confirm selections
|
|
143
153
|
*/
|
|
144
154
|
export async function searchableMultiSelect(config) {
|
|
145
155
|
const prompt = await createSearchableMultiSelect();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Reference Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for transforming command references to tool-specific formats.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Transforms colon-based command references to hyphen-based format.
|
|
8
|
+
* Converts `/opsx:` patterns to `/opsx-` for tools that use hyphen syntax.
|
|
9
|
+
*
|
|
10
|
+
* @param text - The text containing command references
|
|
11
|
+
* @returns Text with command references transformed to hyphen format
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* transformToHyphenCommands('/opsx:new') // returns '/opsx-new'
|
|
15
|
+
* transformToHyphenCommands('Use /opsx:apply to implement') // returns 'Use /opsx-apply to implement'
|
|
16
|
+
*/
|
|
17
|
+
export declare function transformToHyphenCommands(text: string): string;
|
|
18
|
+
//# sourceMappingURL=command-references.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Reference Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for transforming command references to tool-specific formats.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Transforms colon-based command references to hyphen-based format.
|
|
8
|
+
* Converts `/opsx:` patterns to `/opsx-` for tools that use hyphen syntax.
|
|
9
|
+
*
|
|
10
|
+
* @param text - The text containing command references
|
|
11
|
+
* @returns Text with command references transformed to hyphen format
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* transformToHyphenCommands('/opsx:new') // returns '/opsx-new'
|
|
15
|
+
* transformToHyphenCommands('Use /opsx:apply to implement') // returns 'Use /opsx-apply to implement'
|
|
16
|
+
*/
|
|
17
|
+
export function transformToHyphenCommands(text) {
|
|
18
|
+
return text.replace(/\/opsx:/g, '/opsx-');
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=command-references.js.map
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { validateChangeName, createChange } from './change-utils.js';
|
|
|
2
2
|
export type { ValidationResult, CreateChangeOptions } from './change-utils.js';
|
|
3
3
|
export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
|
|
4
4
|
export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
|
|
5
|
+
export { transformToHyphenCommands } from './command-references.js';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/utils/index.js
CHANGED
|
@@ -4,4 +4,6 @@ export { validateChangeName, createChange } from './change-utils.js';
|
|
|
4
4
|
export { readChangeMetadata, writeChangeMetadata, resolveSchemaForChange, validateSchemaName, ChangeMetadataError, } from './change-metadata.js';
|
|
5
5
|
// File system utilities
|
|
6
6
|
export { FileSystemUtils, removeMarkerBlock } from './file-system.js';
|
|
7
|
+
// Command reference utilities
|
|
8
|
+
export { transformToHyphenCommands } from './command-references.js';
|
|
7
9
|
//# sourceMappingURL=index.js.map
|