@fission-ai/openspec 1.6.0-beta.1 → 1.7.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/README.md +28 -1
- package/dist/cli/index.js +73 -7
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +90 -50
- package/dist/commands/config.js +14 -8
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/feedback.js +74 -23
- package/dist/commands/schema.js +21 -11
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +20 -24
- package/dist/commands/validate.js +18 -3
- package/dist/commands/workflow/index.d.ts +1 -1
- package/dist/commands/workflow/index.js +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.js +89 -6
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +7 -2
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +96 -33
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/instruction-loader.d.ts +27 -2
- package/dist/core/artifact-graph/instruction-loader.js +55 -7
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +34 -6
- package/dist/core/change-metadata/schema.d.ts +1 -0
- package/dist/core/change-metadata/schema.js +6 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/command-generation/adapters/amazon-q.d.ts +4 -0
- package/dist/core/command-generation/adapters/amazon-q.js +7 -1
- package/dist/core/command-generation/adapters/antigravity.js +2 -1
- package/dist/core/command-generation/adapters/auggie.js +2 -1
- package/dist/core/command-generation/adapters/bob.d.ts +5 -1
- package/dist/core/command-generation/adapters/bob.js +6 -5
- package/dist/core/command-generation/adapters/claude.js +1 -8
- package/dist/core/command-generation/adapters/codebuddy.js +3 -2
- package/dist/core/command-generation/adapters/continue.js +3 -2
- package/dist/core/command-generation/adapters/costrict.js +2 -1
- package/dist/core/command-generation/adapters/crush.js +5 -5
- package/dist/core/command-generation/adapters/cursor.js +2 -2
- package/dist/core/command-generation/adapters/devin.d.ts +19 -0
- package/dist/core/command-generation/adapters/devin.js +36 -0
- package/dist/core/command-generation/adapters/factory.js +2 -1
- package/dist/core/command-generation/adapters/gemini.js +37 -2
- package/dist/core/command-generation/adapters/github-copilot.js +2 -1
- package/dist/core/command-generation/adapters/iflow.js +5 -4
- package/dist/core/command-generation/adapters/index.d.ts +2 -2
- package/dist/core/command-generation/adapters/index.js +2 -2
- package/dist/core/command-generation/adapters/junie.js +2 -1
- package/dist/core/command-generation/adapters/kiro.js +2 -1
- package/dist/core/command-generation/adapters/lingma.js +5 -5
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/oh-my-pi.js +3 -5
- package/dist/core/command-generation/adapters/opencode.js +3 -5
- package/dist/core/command-generation/adapters/pi.d.ts +2 -2
- package/dist/core/command-generation/adapters/pi.js +3 -6
- package/dist/core/command-generation/adapters/qoder.js +5 -5
- package/dist/core/command-generation/adapters/qwen.d.ts +7 -3
- package/dist/core/command-generation/adapters/qwen.js +12 -7
- package/dist/core/command-generation/adapters/roocode.d.ts +4 -4
- package/dist/core/command-generation/adapters/roocode.js +4 -4
- package/dist/core/command-generation/adapters/trae.js +1 -21
- package/dist/core/command-generation/adapters/zcode.d.ts +16 -0
- package/dist/core/command-generation/adapters/zcode.js +33 -0
- package/dist/core/command-generation/generator.d.ts +8 -0
- package/dist/core/command-generation/generator.js +15 -1
- package/dist/core/command-generation/index.d.ts +1 -1
- package/dist/core/command-generation/index.js +1 -1
- package/dist/core/command-generation/invocation.d.ts +73 -0
- package/dist/core/command-generation/invocation.js +77 -0
- package/dist/core/command-generation/registry.js +4 -4
- package/dist/core/command-generation/types.d.ts +9 -2
- package/dist/core/command-generation/yaml.d.ts +20 -6
- package/dist/core/command-generation/yaml.js +31 -21
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/completions/command-registry.js +8 -2
- package/dist/core/completions/generators/powershell-generator.js +15 -8
- package/dist/core/completions/installers/zsh-installer.d.ts +9 -0
- package/dist/core/completions/installers/zsh-installer.js +20 -4
- package/dist/core/config-prompts.d.ts +1 -1
- package/dist/core/config-prompts.js +14 -1
- package/dist/core/config-schema.d.ts +7 -1
- package/dist/core/config-schema.js +40 -1
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +21 -3
- package/dist/core/global-config.d.ts +5 -0
- package/dist/core/init.d.ts +35 -0
- package/dist/core/init.js +225 -39
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +249 -22
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +71 -1
- package/dist/core/migration.js +294 -1
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/parsers/change-parser.d.ts +17 -2
- package/dist/core/parsers/change-parser.js +33 -20
- package/dist/core/parsers/code-fence.d.ts +15 -0
- package/dist/core/parsers/code-fence.js +49 -0
- package/dist/core/parsers/markdown-parser.js +2 -1
- package/dist/core/parsers/requirement-blocks.d.ts +8 -0
- package/dist/core/parsers/requirement-blocks.js +60 -35
- package/dist/core/parsers/requirement-text.d.ts +1 -8
- package/dist/core/parsers/requirement-text.js +4 -42
- package/dist/core/parsers/spec-structure.js +3 -29
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +9 -45
- package/dist/core/project-config.d.ts +25 -6
- package/dist/core/project-config.js +77 -7
- package/dist/core/references.js +42 -4
- package/dist/core/relationship-health.d.ts +8 -0
- package/dist/core/relationship-health.js +13 -0
- package/dist/core/root-selection.d.ts +9 -6
- package/dist/core/root-selection.js +42 -5
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +105 -6
- package/dist/core/specs-apply.d.ts +8 -33
- package/dist/core/specs-apply.js +252 -157
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/workflows/apply-change.js +54 -4
- package/dist/core/templates/workflows/archive-change.js +166 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +240 -63
- package/dist/core/templates/workflows/continue-change.js +32 -40
- package/dist/core/templates/workflows/explore.js +12 -0
- package/dist/core/templates/workflows/ff-change.js +46 -30
- package/dist/core/templates/workflows/new-change.js +2 -2
- package/dist/core/templates/workflows/onboard.js +4 -4
- package/dist/core/templates/workflows/propose.js +50 -32
- package/dist/core/templates/workflows/store-selection.d.ts +1 -1
- package/dist/core/templates/workflows/store-selection.js +1 -1
- package/dist/core/templates/workflows/sync-specs.js +172 -22
- package/dist/core/templates/workflows/update-change.js +20 -12
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +39 -5
- package/dist/core/update.js +288 -71
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +1 -7
- package/dist/core/validation/validator.js +92 -36
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +736 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +55 -33
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +30 -0
- package/dist/utils/change-metadata.js +101 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/command-references.d.ts +80 -6
- package/dist/utils/command-references.js +138 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/item-discovery.d.ts +19 -0
- package/dist/utils/item-discovery.js +30 -48
- package/dist/utils/shell-detection.d.ts +1 -1
- package/dist/utils/shell-detection.js +64 -11
- package/dist/utils/spec-discovery.d.ts +40 -0
- package/dist/utils/spec-discovery.js +102 -0
- package/package.json +29 -30
- package/schemas/spec-driven/schema.yaml +60 -5
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +5 -1
- package/schemas/spec-driven/templates/spec.md +3 -0
- package/dist/core/command-generation/adapters/codex.d.ts +0 -16
- package/dist/core/command-generation/adapters/codex.js +0 -39
- package/dist/core/command-generation/adapters/windsurf.d.ts +0 -14
- package/dist/core/command-generation/adapters/windsurf.js +0 -38
package/dist/core/update.js
CHANGED
|
@@ -10,20 +10,21 @@ 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 {
|
|
13
|
+
import { getSkillReferenceTransformer, getTransformerForTool, transformToSkillReferences } from '../utils/command-references.js';
|
|
14
14
|
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
15
15
|
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
16
16
|
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
|
|
17
|
-
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
|
|
17
|
+
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatDeferredGlobalPromptSummary, formatCleanupSummary, formatDetectionSummary, getLegacyGlobalPromptMatches, getLegacyWorkflowIdsForTool, getToolsFromLegacyArtifacts, omitGlobalLegacyPromptFiles, pickGlobalLegacyPromptFiles, } from './legacy-cleanup.js';
|
|
18
18
|
import { isInteractive } from '../utils/interactive.js';
|
|
19
19
|
import { getGlobalConfig } from './global-config.js';
|
|
20
|
-
import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
|
|
20
|
+
import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
|
|
21
|
+
import { getOnboardingCommands } from './onboarding-commands.js';
|
|
21
22
|
import { getAvailableTools } from './available-tools.js';
|
|
22
|
-
import { WORKFLOW_TO_SKILL_DIR,
|
|
23
|
-
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, } from './migration.js';
|
|
23
|
+
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
24
|
+
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, findLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
|
|
25
|
+
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
24
26
|
const require = createRequire(import.meta.url);
|
|
25
27
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
26
|
-
const OLD_CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'archive'];
|
|
27
28
|
/**
|
|
28
29
|
* Scans installed workflow artifacts (skills and managed commands) across all configured tools.
|
|
29
30
|
* Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
|
|
@@ -41,6 +42,12 @@ export class UpdateCommand {
|
|
|
41
42
|
constructor(options = {}) {
|
|
42
43
|
this.force = options.force ?? false;
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Refreshes OpenSpec skills and commands for all configured tools,
|
|
47
|
+
* regenerating artifacts according to the effective profile and delivery mode.
|
|
48
|
+
*
|
|
49
|
+
* @param projectPath - Path to the project root containing the openspec directory
|
|
50
|
+
*/
|
|
44
51
|
async execute(projectPath) {
|
|
45
52
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
46
53
|
const openspecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
|
|
@@ -48,7 +55,17 @@ export class UpdateCommand {
|
|
|
48
55
|
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
|
49
56
|
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
|
50
57
|
}
|
|
51
|
-
// 2.
|
|
58
|
+
// 2. Migrate OpenSpec-managed skills left in renamed tool directories
|
|
59
|
+
// (e.g. .kimi -> .kimi-code) so they stay detected and get refreshed,
|
|
60
|
+
// then perform the one-time profile migration if needed before any
|
|
61
|
+
// legacy upgrade generation.
|
|
62
|
+
for (const migration of migrateLegacyToolDirs(resolvedProjectPath)) {
|
|
63
|
+
if (hasMovableContent(migration)) {
|
|
64
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
65
|
+
}
|
|
66
|
+
this.reportKeptInPlace(migration);
|
|
67
|
+
}
|
|
68
|
+
const declinedMigrations = await this.offerConsentedLegacyMigrations(resolvedProjectPath);
|
|
52
69
|
// Use detected tool directories to preserve existing opsx skills/commands.
|
|
53
70
|
const detectedTools = getAvailableTools(resolvedProjectPath);
|
|
54
71
|
migrateIfNeededShared(resolvedProjectPath, detectedTools);
|
|
@@ -58,27 +75,35 @@ export class UpdateCommand {
|
|
|
58
75
|
const delivery = globalConfig.delivery ?? 'both';
|
|
59
76
|
const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
60
77
|
const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
61
|
-
const shouldGenerateSkills = delivery !== 'commands';
|
|
62
|
-
const shouldGenerateCommands = delivery !== 'skills';
|
|
63
78
|
// 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config
|
|
64
|
-
const
|
|
79
|
+
const legacyUpgrade = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
|
|
80
|
+
const { newlyConfiguredTools, workflowOverrides: legacyWorkflowOverrides, deferredGlobalCleanup, } = legacyUpgrade;
|
|
65
81
|
// 5. Find configured tools
|
|
66
82
|
const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
|
|
67
83
|
if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
|
|
84
|
+
if (deferredGlobalCleanup) {
|
|
85
|
+
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
86
|
+
}
|
|
87
|
+
if (declinedMigrations.length > 0) {
|
|
88
|
+
// Not an unconfigured project — a configured one the user chose to
|
|
89
|
+
// leave in its former directory. Saying "run init" would be wrong.
|
|
90
|
+
for (const migration of declinedMigrations) {
|
|
91
|
+
console.log(chalk.yellow(`Nothing to update: this project's OpenSpec files are still in ${migration.from}/, ` +
|
|
92
|
+
`which OpenSpec no longer writes.`));
|
|
93
|
+
console.log(chalk.dim(`Re-run "openspec update" and accept the move to ${migration.to}/ to resume updates.`));
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
68
97
|
console.log(chalk.yellow('No configured tools found.'));
|
|
69
98
|
console.log(chalk.dim('Run "openspec init" to set up tools.'));
|
|
70
99
|
return;
|
|
71
100
|
}
|
|
72
|
-
// 6. Check version status for all configured tools
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const toolStatuses = configuredTools.map((toolId) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return { ...status, configured: true };
|
|
79
|
-
}
|
|
80
|
-
return status;
|
|
81
|
-
});
|
|
101
|
+
// 6. Check version status for all configured tools, against the same workflow set
|
|
102
|
+
// the generation loop below writes — otherwise a legacy-upgraded tool would be
|
|
103
|
+
// fingerprinted against commands it was never given.
|
|
104
|
+
const toolStatuses = configuredTools.map((toolId) => getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION, {
|
|
105
|
+
workflows: legacyWorkflowOverrides[toolId] ?? desiredWorkflows,
|
|
106
|
+
}));
|
|
82
107
|
const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
|
|
83
108
|
// 7. Smart update detection
|
|
84
109
|
const toolsNeedingVersionUpdate = toolStatuses
|
|
@@ -90,30 +115,38 @@ export class UpdateCommand {
|
|
|
90
115
|
...toolsNeedingConfigSync,
|
|
91
116
|
]);
|
|
92
117
|
const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
|
|
93
|
-
if (!this.force && toolsToUpdateSet.size === 0) {
|
|
118
|
+
if (!this.force && toolsToUpdateSet.size === 0 && newlyConfiguredTools.length === 0) {
|
|
119
|
+
if (deferredGlobalCleanup) {
|
|
120
|
+
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
121
|
+
}
|
|
94
122
|
// All tools are up to date
|
|
95
123
|
this.displayUpToDateMessage(toolStatuses);
|
|
96
124
|
// Still check for new tool directories and extra workflows
|
|
97
125
|
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
98
126
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
|
|
99
|
-
this.
|
|
127
|
+
this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);
|
|
128
|
+
this.displaySetupNotes(configuredTools);
|
|
100
129
|
return;
|
|
101
130
|
}
|
|
102
131
|
// 8. Display update plan
|
|
103
132
|
if (this.force) {
|
|
104
133
|
console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
|
|
105
134
|
}
|
|
135
|
+
else if (toolsToUpdateSet.size === 0) {
|
|
136
|
+
console.log('No additional refresh needed after legacy migration.');
|
|
137
|
+
}
|
|
106
138
|
else {
|
|
107
139
|
this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
|
|
108
140
|
}
|
|
109
141
|
console.log();
|
|
110
142
|
// 9. Determine what to generate based on delivery
|
|
111
|
-
const
|
|
112
|
-
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
|
|
143
|
+
const deliveryIncludesCommands = delivery !== 'skills';
|
|
113
144
|
// 10. Update tools (all if force, otherwise only those needing update)
|
|
114
145
|
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
|
|
115
146
|
const updatedTools = [];
|
|
116
147
|
const failedTools = [];
|
|
148
|
+
const skillsInvocableCommandSkips = [];
|
|
149
|
+
const zeroArtifactTools = [];
|
|
117
150
|
let removedCommandCount = 0;
|
|
118
151
|
let removedSkillCount = 0;
|
|
119
152
|
let removedDeselectedCommandCount = 0;
|
|
@@ -125,21 +158,32 @@ export class UpdateCommand {
|
|
|
125
158
|
const spinner = ora(`Updating ${tool.name}...`).start();
|
|
126
159
|
try {
|
|
127
160
|
const skillsDir = path.join(resolvedProjectPath, tool.skillsDir, 'skills');
|
|
161
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
162
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
163
|
+
const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
|
|
164
|
+
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
165
|
+
const commandContents = getCommandContents(toolWorkflows);
|
|
128
166
|
// Generate skill files if delivery includes skills
|
|
129
167
|
if (shouldGenerateSkills) {
|
|
130
168
|
for (const { template, dirName } of skillTemplates) {
|
|
131
169
|
const skillDir = path.join(skillsDir, dirName);
|
|
132
170
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
133
|
-
|
|
134
|
-
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
|
|
171
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
135
172
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
136
173
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
137
174
|
}
|
|
138
|
-
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir,
|
|
175
|
+
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsDir, toolWorkflows);
|
|
139
176
|
}
|
|
140
177
|
// Delete skill directories if delivery is commands-only
|
|
141
|
-
if (
|
|
178
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery)) {
|
|
142
179
|
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
180
|
+
// A tool with no command adapter now has zero OpenSpec artifacts;
|
|
181
|
+
// say so like init does, rather than deleting its skills silently
|
|
182
|
+
// and letting tool detection re-suggest an init that would also
|
|
183
|
+
// generate nothing under this delivery setting.
|
|
184
|
+
if (!shouldGenerateCommandsForTool(tool.value, delivery)) {
|
|
185
|
+
zeroArtifactTools.push(tool.name);
|
|
186
|
+
}
|
|
143
187
|
}
|
|
144
188
|
// Generate commands if delivery includes commands
|
|
145
189
|
if (shouldGenerateCommands) {
|
|
@@ -150,11 +194,14 @@ export class UpdateCommand {
|
|
|
150
194
|
const commandFile = path.isAbsolute(cmd.path) ? cmd.path : path.join(resolvedProjectPath, cmd.path);
|
|
151
195
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
152
196
|
}
|
|
153
|
-
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId,
|
|
197
|
+
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
|
|
154
198
|
}
|
|
155
199
|
}
|
|
200
|
+
else if (deliveryIncludesCommands && resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
|
|
201
|
+
skillsInvocableCommandSkips.push(tool.value);
|
|
202
|
+
}
|
|
156
203
|
// Delete command files if delivery is skills-only
|
|
157
|
-
if (
|
|
204
|
+
if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
|
|
158
205
|
removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
|
|
159
206
|
}
|
|
160
207
|
spinner.succeed(`Updated ${tool.name}`);
|
|
@@ -168,6 +215,9 @@ export class UpdateCommand {
|
|
|
168
215
|
});
|
|
169
216
|
}
|
|
170
217
|
}
|
|
218
|
+
if (deferredGlobalCleanup) {
|
|
219
|
+
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
220
|
+
}
|
|
171
221
|
// 11. Summary
|
|
172
222
|
console.log();
|
|
173
223
|
if (updatedTools.length > 0) {
|
|
@@ -176,26 +226,60 @@ export class UpdateCommand {
|
|
|
176
226
|
if (failedTools.length > 0) {
|
|
177
227
|
console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
|
|
178
228
|
}
|
|
229
|
+
if (skillsInvocableCommandSkips.length > 0) {
|
|
230
|
+
console.log(chalk.dim(`Commands skipped for: ${skillsInvocableCommandSkips.join(', ')} (uses skills)`));
|
|
231
|
+
}
|
|
179
232
|
if (removedCommandCount > 0) {
|
|
180
233
|
console.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
|
|
181
234
|
}
|
|
182
235
|
if (removedSkillCount > 0) {
|
|
183
236
|
console.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
|
|
184
237
|
}
|
|
238
|
+
if (zeroArtifactTools.length > 0) {
|
|
239
|
+
const names = zeroArtifactTools.join(', ');
|
|
240
|
+
console.log(chalk.yellow(`No skills or commands remain for ${names}: delivery is set to 'commands' but ` +
|
|
241
|
+
`${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
|
|
242
|
+
`Run 'openspec config set delivery both' to generate skills.`));
|
|
243
|
+
}
|
|
185
244
|
if (removedDeselectedCommandCount > 0) {
|
|
186
245
|
console.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
|
|
187
246
|
}
|
|
188
247
|
if (removedDeselectedSkillCount > 0) {
|
|
189
248
|
console.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
|
|
190
249
|
}
|
|
191
|
-
// 12. Show onboarding message for newly configured tools from legacy upgrade
|
|
250
|
+
// 12. Show onboarding message for newly configured tools from legacy upgrade.
|
|
251
|
+
// Command tools get the command name their files answer to, skill-only
|
|
252
|
+
// tools their documented skill invocation, and disagreements fall back to
|
|
253
|
+
// naming the skill.
|
|
192
254
|
if (newlyConfiguredTools.length > 0) {
|
|
255
|
+
const referenceFor = (command) => {
|
|
256
|
+
const neutralForm = `the ${transformToSkillReferences(command).slice(1)} skill`;
|
|
257
|
+
const forms = new Set(newlyConfiguredTools.map((toolId) => {
|
|
258
|
+
if (shouldGenerateCommandsForTool(toolId, delivery)) {
|
|
259
|
+
// Name the command the tool's files actually answer to:
|
|
260
|
+
// /opsx-<id> where the filename is the command name.
|
|
261
|
+
const transformer = getTransformerForTool(toolId, delivery, resolveCommandSurfaceCapability(toolId), resolveCommandInvocation(toolId));
|
|
262
|
+
return transformer ? transformer(command) : command;
|
|
263
|
+
}
|
|
264
|
+
return getSkillReferenceTransformer(toolId)(command);
|
|
265
|
+
}));
|
|
266
|
+
return forms.size === 1 ? [...forms][0] : neutralForm;
|
|
267
|
+
};
|
|
268
|
+
// Only hint at workflows these tools actually received. A legacy upgrade
|
|
269
|
+
// can install a narrower set than the profile (inferred Codex prompts).
|
|
270
|
+
const installedWorkflows = [
|
|
271
|
+
...new Set(newlyConfiguredTools.flatMap((toolId) => legacyWorkflowOverrides[toolId] ?? desiredWorkflows)),
|
|
272
|
+
];
|
|
273
|
+
const entries = getOnboardingCommands(installedWorkflows).map(({ command, description }) => [referenceFor(command), description]);
|
|
193
274
|
console.log();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
275
|
+
if (entries.length > 0) {
|
|
276
|
+
const width = Math.max(...entries.map(([reference]) => reference.length));
|
|
277
|
+
console.log(chalk.bold('Getting started:'));
|
|
278
|
+
for (const [reference, description] of entries) {
|
|
279
|
+
console.log(` ${reference.padEnd(width)} ${description}`);
|
|
280
|
+
}
|
|
281
|
+
console.log();
|
|
282
|
+
}
|
|
199
283
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
200
284
|
}
|
|
201
285
|
const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
|
|
@@ -203,7 +287,8 @@ export class UpdateCommand {
|
|
|
203
287
|
this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
|
|
204
288
|
// 14. Display note about extra workflows not in profile
|
|
205
289
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
|
|
206
|
-
this.
|
|
290
|
+
this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);
|
|
291
|
+
this.displaySetupNotes(configuredAndNewTools);
|
|
207
292
|
// 15. List affected tools
|
|
208
293
|
if (updatedTools.length > 0) {
|
|
209
294
|
const toolDisplayNames = updatedTools;
|
|
@@ -240,6 +325,18 @@ export class UpdateCommand {
|
|
|
240
325
|
console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
|
|
241
326
|
}
|
|
242
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Shows manual setup notes for configured tools that need extra
|
|
330
|
+
* configuration before they pick up generated files.
|
|
331
|
+
*/
|
|
332
|
+
displaySetupNotes(toolIds) {
|
|
333
|
+
for (const toolId of toolIds) {
|
|
334
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
335
|
+
if (tool?.setupNote) {
|
|
336
|
+
console.log(chalk.yellow(`Setup required for ${tool.name}: ${tool.setupNote}`));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
243
340
|
/**
|
|
244
341
|
* Detects new tool directories that aren't currently configured and displays a hint.
|
|
245
342
|
*/
|
|
@@ -268,21 +365,23 @@ export class UpdateCommand {
|
|
|
268
365
|
}
|
|
269
366
|
}
|
|
270
367
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
368
|
+
* Point out core workflows a custom profile is missing, so releases that
|
|
369
|
+
* grow CORE_WORKFLOWS stay discoverable. Keep custom profiles user-owned;
|
|
370
|
+
* do not mutate them.
|
|
273
371
|
*/
|
|
274
|
-
|
|
372
|
+
displayMissingCoreWorkflowsNote(profile, workflows) {
|
|
275
373
|
if (profile !== 'custom' || !workflows) {
|
|
276
374
|
return;
|
|
277
375
|
}
|
|
278
376
|
const workflowSet = new Set(workflows);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
if (!matchesOldCore) {
|
|
377
|
+
const missing = CORE_WORKFLOWS.filter((workflow) => !workflowSet.has(workflow));
|
|
378
|
+
if (missing.length === 0) {
|
|
282
379
|
return;
|
|
283
380
|
}
|
|
284
|
-
|
|
285
|
-
|
|
381
|
+
const label = missing.length === 1 ? 'workflow' : 'workflows';
|
|
382
|
+
const pronoun = missing.length === 1 ? 'it' : 'them';
|
|
383
|
+
console.log(chalk.dim(`Note: Your custom profile is missing ${missing.length} core ${label}: ${missing.join(', ')}`));
|
|
384
|
+
console.log(chalk.dim(`Run \`openspec config profile\` to add ${pronoun}, or \`openspec config profile core\` to use the core set.`));
|
|
286
385
|
}
|
|
287
386
|
/**
|
|
288
387
|
* Removes skill directories for workflows when delivery changed to commands-only.
|
|
@@ -384,6 +483,72 @@ export class UpdateCommand {
|
|
|
384
483
|
}
|
|
385
484
|
return removed;
|
|
386
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Offers to move OpenSpec content out of a renamed tool's former directory
|
|
488
|
+
* when the old location might still be the live one — today, Windsurf's
|
|
489
|
+
* `.windsurf/` after the Devin Desktop rebrand.
|
|
490
|
+
*
|
|
491
|
+
* Interactive runs are asked, because nothing on disk distinguishes a user
|
|
492
|
+
* who took the rebrand from one still on a pre-rebrand Windsurf build that
|
|
493
|
+
* reads only `.windsurf/`. `--force` and non-interactive runs migrate, which
|
|
494
|
+
* is what an unattended upgrade wants.
|
|
495
|
+
*/
|
|
496
|
+
/** Surfaces files the move left behind rather than overwriting. */
|
|
497
|
+
reportKeptInPlace(migration) {
|
|
498
|
+
const notice = keptInPlaceNotice(migration);
|
|
499
|
+
if (notice)
|
|
500
|
+
console.log(chalk.dim(notice));
|
|
501
|
+
}
|
|
502
|
+
async offerConsentedLegacyMigrations(projectPath) {
|
|
503
|
+
const pending = findLegacyToolMigrations(projectPath).filter((m) => m.needsConsent);
|
|
504
|
+
const declined = [];
|
|
505
|
+
if (pending.length === 0)
|
|
506
|
+
return declined;
|
|
507
|
+
for (const migration of pending) {
|
|
508
|
+
// Nothing movable: every legacy file differs from its counterpart, so
|
|
509
|
+
// there is no move to offer. Still say so — silence would leave two
|
|
510
|
+
// divergent copies the user never hears about.
|
|
511
|
+
if (!hasMovableContent(migration)) {
|
|
512
|
+
this.reportKeptInPlace(migration);
|
|
513
|
+
console.log();
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
console.log(chalk.yellow(legacyMigrationNotice(migration)));
|
|
517
|
+
if (!this.force && isInteractive()) {
|
|
518
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
519
|
+
let shouldMigrate;
|
|
520
|
+
try {
|
|
521
|
+
shouldMigrate = await confirm({
|
|
522
|
+
message: `Move ${describeLegacyMigration(migration)} from ${migration.from}/ to ${migration.to}/?`,
|
|
523
|
+
default: true,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
// Closed stdin is not consent, and it must not abort the update.
|
|
528
|
+
shouldMigrate = false;
|
|
529
|
+
}
|
|
530
|
+
if (!shouldMigrate) {
|
|
531
|
+
// Say what declining costs. OpenSpec writes the current root now, so
|
|
532
|
+
// the files keep working where they are, but OpenSpec stops managing
|
|
533
|
+
// them — it no longer looks in the former directory.
|
|
534
|
+
console.log(chalk.dim(`Left in place. OpenSpec writes ${migration.to}/ now and will not manage ` +
|
|
535
|
+
`${migration.from}/, so those files stay as they are until you move them. ` +
|
|
536
|
+
`You will be asked again next run.`));
|
|
537
|
+
console.log();
|
|
538
|
+
declined.push(migration);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
for (const applied of migrateLegacyToolDirs(projectPath, [migration.toolId])) {
|
|
543
|
+
if (hasMovableContent(applied)) {
|
|
544
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(applied)}: ${applied.from} → ${applied.to}`));
|
|
545
|
+
}
|
|
546
|
+
this.reportKeptInPlace(applied);
|
|
547
|
+
}
|
|
548
|
+
console.log();
|
|
549
|
+
}
|
|
550
|
+
return declined;
|
|
551
|
+
}
|
|
387
552
|
/**
|
|
388
553
|
* Detect and handle legacy OpenSpec artifacts.
|
|
389
554
|
* Unlike init, update warns but continues if legacy files found in non-interactive mode.
|
|
@@ -393,25 +558,37 @@ export class UpdateCommand {
|
|
|
393
558
|
// Detect legacy artifacts
|
|
394
559
|
const detection = await detectLegacyArtifacts(projectPath);
|
|
395
560
|
if (!detection.hasLegacyArtifacts) {
|
|
396
|
-
return []; // No legacy artifacts found
|
|
561
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} }; // No legacy artifacts found
|
|
397
562
|
}
|
|
398
563
|
// Show what was detected
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
564
|
+
const immediateSummary = formatDetectionSummary(omitGlobalLegacyPromptFiles(detection));
|
|
565
|
+
const deferredSummary = formatDeferredGlobalPromptSummary(detection);
|
|
566
|
+
if (immediateSummary || deferredSummary) {
|
|
567
|
+
console.log();
|
|
568
|
+
if (immediateSummary) {
|
|
569
|
+
console.log(immediateSummary);
|
|
570
|
+
console.log();
|
|
571
|
+
}
|
|
572
|
+
if (deferredSummary) {
|
|
573
|
+
console.log(deferredSummary);
|
|
574
|
+
console.log();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
402
577
|
const canPrompt = isInteractive();
|
|
403
578
|
if (this.force) {
|
|
404
|
-
|
|
405
|
-
await this.
|
|
406
|
-
|
|
407
|
-
|
|
579
|
+
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
580
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
581
|
+
return {
|
|
582
|
+
...legacyUpgrade,
|
|
583
|
+
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
584
|
+
};
|
|
408
585
|
}
|
|
409
586
|
if (!canPrompt) {
|
|
410
587
|
// Non-interactive mode without --force: warn and continue
|
|
411
588
|
// (Unlike init, update doesn't abort - user may just want to update skills)
|
|
412
589
|
console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
|
|
413
590
|
console.log();
|
|
414
|
-
return [];
|
|
591
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
415
592
|
}
|
|
416
593
|
// Interactive mode: prompt for confirmation
|
|
417
594
|
const { confirm } = await import('@inquirer/prompts');
|
|
@@ -420,14 +597,47 @@ export class UpdateCommand {
|
|
|
420
597
|
default: true,
|
|
421
598
|
});
|
|
422
599
|
if (shouldCleanup) {
|
|
423
|
-
await this.
|
|
424
|
-
|
|
425
|
-
return
|
|
600
|
+
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
601
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
602
|
+
return {
|
|
603
|
+
...legacyUpgrade,
|
|
604
|
+
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
605
|
+
};
|
|
426
606
|
}
|
|
427
607
|
else {
|
|
428
608
|
console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
|
|
429
609
|
console.log();
|
|
430
|
-
return [];
|
|
610
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Cleans approved repo-local legacy artifacts before configured tools refresh.
|
|
615
|
+
*/
|
|
616
|
+
async performImmediateLegacyCleanup(projectPath, detection) {
|
|
617
|
+
const immediateDetection = omitGlobalLegacyPromptFiles(detection);
|
|
618
|
+
if (immediateDetection.hasLegacyArtifacts) {
|
|
619
|
+
await this.performLegacyCleanup(projectPath, immediateDetection);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Cleans approved global Codex prompts after configured tools refresh so newly
|
|
624
|
+
* installed replacement skills can retire their prompts in the same run.
|
|
625
|
+
*/
|
|
626
|
+
async performDeferredGlobalPromptCleanup(projectPath, detection) {
|
|
627
|
+
const availableCodexWorkflows = new Set(scanInstalledWorkflows(projectPath, ['codex']));
|
|
628
|
+
const removableMatches = getLegacyGlobalPromptMatches(detection)
|
|
629
|
+
.filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
|
|
630
|
+
if (removableMatches.length > 0) {
|
|
631
|
+
await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(detection, removableMatches.map((prompt) => prompt.path)));
|
|
632
|
+
}
|
|
633
|
+
const blockedMatches = getLegacyGlobalPromptMatches(detection)
|
|
634
|
+
.filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
|
|
635
|
+
if (blockedMatches.length > 0) {
|
|
636
|
+
console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
|
|
637
|
+
for (const prompt of blockedMatches) {
|
|
638
|
+
console.log(chalk.dim(` - ${prompt.toolId}: ${prompt.path}`));
|
|
639
|
+
}
|
|
640
|
+
console.log();
|
|
431
641
|
}
|
|
432
642
|
}
|
|
433
643
|
/**
|
|
@@ -445,14 +655,14 @@ export class UpdateCommand {
|
|
|
445
655
|
console.log();
|
|
446
656
|
}
|
|
447
657
|
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
658
|
+
* Upgrades unconfigured legacy tools into the skills-based setup and carries
|
|
659
|
+
* workflow overrides for migrations that should mirror legacy Codex prompts.
|
|
450
660
|
*/
|
|
451
661
|
async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) {
|
|
452
662
|
// Get tools that had legacy artifacts
|
|
453
663
|
const legacyTools = getToolsFromLegacyArtifacts(detection);
|
|
454
664
|
if (legacyTools.length === 0) {
|
|
455
|
-
return [];
|
|
665
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
456
666
|
}
|
|
457
667
|
// Get currently configured tools
|
|
458
668
|
const configuredTools = getConfiguredToolsForProfileSync(projectPath);
|
|
@@ -460,13 +670,13 @@ export class UpdateCommand {
|
|
|
460
670
|
// Filter to tools that aren't already configured
|
|
461
671
|
const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
|
|
462
672
|
if (unconfiguredLegacyTools.length === 0) {
|
|
463
|
-
return [];
|
|
673
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
464
674
|
}
|
|
465
675
|
// Get valid tools (those with skillsDir)
|
|
466
676
|
const validToolIds = new Set(getToolsWithSkillsDir());
|
|
467
677
|
const validUnconfiguredTools = unconfiguredLegacyTools.filter((t) => validToolIds.has(t));
|
|
468
678
|
if (validUnconfiguredTools.length === 0) {
|
|
469
|
-
return [];
|
|
679
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
470
680
|
}
|
|
471
681
|
// Show what tools were detected from legacy artifacts
|
|
472
682
|
console.log(chalk.bold('Tools detected from legacy artifacts:'));
|
|
@@ -502,15 +712,13 @@ export class UpdateCommand {
|
|
|
502
712
|
if (selectedTools.length === 0) {
|
|
503
713
|
console.log(chalk.dim('Skipping tool setup.'));
|
|
504
714
|
console.log();
|
|
505
|
-
return [];
|
|
715
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
506
716
|
}
|
|
507
717
|
}
|
|
718
|
+
const inferredCodexWorkflows = getLegacyWorkflowIdsForTool(detection, 'codex');
|
|
508
719
|
// Create skills/commands for selected tools using effective profile+delivery.
|
|
509
720
|
const newlyConfigured = [];
|
|
510
|
-
const
|
|
511
|
-
const shouldGenerateCommands = delivery !== 'skills';
|
|
512
|
-
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
|
|
513
|
-
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
|
|
721
|
+
const workflowOverrides = {};
|
|
514
722
|
for (const toolId of selectedTools) {
|
|
515
723
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
516
724
|
if (!tool?.skillsDir)
|
|
@@ -518,13 +726,22 @@ export class UpdateCommand {
|
|
|
518
726
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
519
727
|
try {
|
|
520
728
|
const skillsDir = path.join(projectPath, tool.skillsDir, 'skills');
|
|
729
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
730
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
731
|
+
const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
|
|
732
|
+
? inferredCodexWorkflows
|
|
733
|
+
: desiredWorkflows);
|
|
734
|
+
if (tool.value === 'codex' && inferredCodexWorkflows.length > 0) {
|
|
735
|
+
workflowOverrides[tool.value] = inferredCodexWorkflows;
|
|
736
|
+
}
|
|
737
|
+
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
738
|
+
const commandContents = getCommandContents(toolWorkflows);
|
|
521
739
|
// Create skill files when delivery includes skills
|
|
522
740
|
if (shouldGenerateSkills) {
|
|
523
741
|
for (const { template, dirName } of skillTemplates) {
|
|
524
742
|
const skillDir = path.join(skillsDir, dirName);
|
|
525
743
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
526
|
-
|
|
527
|
-
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
|
|
744
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
528
745
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
529
746
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
530
747
|
}
|
|
@@ -551,7 +768,7 @@ export class UpdateCommand {
|
|
|
551
768
|
if (newlyConfigured.length > 0) {
|
|
552
769
|
console.log();
|
|
553
770
|
}
|
|
554
|
-
return newlyConfigured;
|
|
771
|
+
return { newlyConfiguredTools: newlyConfigured, workflowOverrides };
|
|
555
772
|
}
|
|
556
773
|
}
|
|
557
774
|
//# sourceMappingURL=update.js.map
|
|
@@ -19,6 +19,9 @@ export declare const VALIDATION_MESSAGES: {
|
|
|
19
19
|
readonly CHANGE_WHY_TOO_LONG: "Why section should not exceed 1000 characters";
|
|
20
20
|
readonly CHANGE_WHAT_EMPTY: "What Changes section cannot be empty";
|
|
21
21
|
readonly CHANGE_NO_DELTAS: "Change must have at least one delta";
|
|
22
|
+
readonly CHANGE_SKIP_SPECS_CONFLICT: "skip_specs is set in .openspec.yaml but spec files exist under specs/. Remove skip_specs or delete the delta spec files";
|
|
23
|
+
readonly CHANGE_SKIP_SPECS_ACCEPTED: "skip_specs is set in .openspec.yaml: change declares no spec-level behavior changes, zero deltas accepted";
|
|
24
|
+
readonly CHANGE_SKIP_SPECS_INVALID_METADATA: "skip_specs is set but .openspec.yaml is not valid change metadata, so the marker is not honored. Fix the metadata";
|
|
22
25
|
readonly CHANGE_TOO_MANY_DELTAS: "Consider splitting changes with more than 10 deltas";
|
|
23
26
|
readonly DELTA_SPEC_EMPTY: "Spec name cannot be empty";
|
|
24
27
|
readonly DELTA_DESCRIPTION_EMPTY: "Delta description cannot be empty";
|
|
@@ -26,7 +29,7 @@ export declare const VALIDATION_MESSAGES: {
|
|
|
26
29
|
readonly REQUIREMENT_TOO_LONG: "Requirement text is very long (>500 characters). Consider breaking it down.";
|
|
27
30
|
readonly DELTA_DESCRIPTION_TOO_BRIEF: "Delta description is too brief";
|
|
28
31
|
readonly DELTA_MISSING_REQUIREMENTS: "Delta should include requirements";
|
|
29
|
-
readonly GUIDE_NO_DELTAS: "No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one \"#### Scenario:\" block. Tip: run \"openspec change show <change-id> --json --deltas-only\" to inspect parsed deltas.";
|
|
32
|
+
readonly GUIDE_NO_DELTAS: "No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one \"#### Scenario:\" block. If this change intentionally modifies no specs (pure refactor, tooling, docs), set \"skip_specs: true\" in the change's .openspec.yaml instead. Tip: run \"openspec change show <change-id> --json --deltas-only\" to inspect parsed deltas.";
|
|
30
33
|
readonly GUIDE_MISSING_SPEC_SECTIONS: "Missing required sections. Expected headers: \"## Purpose\" and \"## Requirements\". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...";
|
|
31
34
|
readonly GUIDE_MISSING_CHANGE_SECTIONS: "Missing required sections. Expected headers: \"## Why\" and \"## What Changes\". Ensure deltas are documented in specs/ using delta headers.";
|
|
32
35
|
readonly GUIDE_SCENARIO_FORMAT: "Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...";
|
|
@@ -23,6 +23,9 @@ export const VALIDATION_MESSAGES = {
|
|
|
23
23
|
CHANGE_WHY_TOO_LONG: `Why section should not exceed ${MAX_WHY_SECTION_LENGTH} characters`,
|
|
24
24
|
CHANGE_WHAT_EMPTY: 'What Changes section cannot be empty',
|
|
25
25
|
CHANGE_NO_DELTAS: 'Change must have at least one delta',
|
|
26
|
+
CHANGE_SKIP_SPECS_CONFLICT: 'skip_specs is set in .openspec.yaml but spec files exist under specs/. Remove skip_specs or delete the delta spec files',
|
|
27
|
+
CHANGE_SKIP_SPECS_ACCEPTED: 'skip_specs is set in .openspec.yaml: change declares no spec-level behavior changes, zero deltas accepted',
|
|
28
|
+
CHANGE_SKIP_SPECS_INVALID_METADATA: 'skip_specs is set but .openspec.yaml is not valid change metadata, so the marker is not honored. Fix the metadata',
|
|
26
29
|
CHANGE_TOO_MANY_DELTAS: `Consider splitting changes with more than ${MAX_DELTAS_PER_CHANGE} deltas`,
|
|
27
30
|
DELTA_SPEC_EMPTY: 'Spec name cannot be empty',
|
|
28
31
|
DELTA_DESCRIPTION_EMPTY: 'Delta description cannot be empty',
|
|
@@ -32,7 +35,7 @@ export const VALIDATION_MESSAGES = {
|
|
|
32
35
|
DELTA_DESCRIPTION_TOO_BRIEF: 'Delta description is too brief',
|
|
33
36
|
DELTA_MISSING_REQUIREMENTS: 'Delta should include requirements',
|
|
34
37
|
// Guidance snippets (appended to primary messages for remediation)
|
|
35
|
-
GUIDE_NO_DELTAS: 'No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
|
38
|
+
GUIDE_NO_DELTAS: 'No deltas found. Ensure your change has a specs/ directory with capability folders (e.g. specs/http-server/spec.md) containing .md files that use delta headers (## ADDED/MODIFIED/REMOVED/RENAMED Requirements) and that each requirement includes at least one "#### Scenario:" block. If this change intentionally modifies no specs (pure refactor, tooling, docs), set "skip_specs: true" in the change\'s .openspec.yaml instead. Tip: run "openspec change show <change-id> --json --deltas-only" to inspect parsed deltas.',
|
|
36
39
|
GUIDE_MISSING_SPEC_SECTIONS: 'Missing required sections. Expected headers: "## Purpose" and "## Requirements". Example:\n## Purpose\n[brief purpose]\n\n## Requirements\n### Requirement: Clear requirement statement\nUsers SHALL ...\n\n#### Scenario: Descriptive name\n- **WHEN** ...\n- **THEN** ...',
|
|
37
40
|
GUIDE_MISSING_CHANGE_SECTIONS: 'Missing required sections. Expected headers: "## Why" and "## What Changes". Ensure deltas are documented in specs/ using delta headers.',
|
|
38
41
|
GUIDE_SCENARIO_FORMAT: 'Scenarios must use level-4 headers. Convert bullet lists into:\n#### Scenario: Short name\n- **WHEN** ...\n- **THEN** ...\n- **AND** ...',
|