@fission-ai/openspec 1.6.0 → 1.8.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 +31 -2
- package/dist/cli/index.js +79 -8
- package/dist/commands/change.d.ts +0 -1
- package/dist/commands/change.js +105 -51
- 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 +110 -31
- package/dist/commands/show.js +5 -2
- package/dist/commands/spec.js +49 -29
- package/dist/commands/validate.js +20 -5
- 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 +121 -33
- package/dist/commands/workflow/shared.d.ts +13 -2
- package/dist/commands/workflow/shared.js +34 -4
- package/dist/commands/workflow/status.js +9 -4
- package/dist/commands/workflow/templates.js +15 -5
- package/dist/core/archive.d.ts +14 -1
- package/dist/core/archive.js +1332 -155
- package/dist/core/artifact-graph/graph.d.ts +16 -0
- package/dist/core/artifact-graph/graph.js +30 -6
- package/dist/core/artifact-graph/index.d.ts +1 -1
- package/dist/core/artifact-graph/index.js +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +30 -3
- package/dist/core/artifact-graph/instruction-loader.js +70 -12
- package/dist/core/artifact-graph/outputs.d.ts +1 -0
- package/dist/core/artifact-graph/outputs.js +67 -5
- package/dist/core/artifact-graph/resolver.d.ts +14 -0
- package/dist/core/artifact-graph/resolver.js +69 -15
- package/dist/core/artifact-graph/types.js +21 -3
- package/dist/core/available-tools.d.ts +2 -2
- package/dist/core/available-tools.js +14 -3
- package/dist/core/change-metadata/schema.d.ts +2 -0
- package/dist/core/change-metadata/schema.js +13 -0
- package/dist/core/change-status-policy.d.ts +1 -1
- package/dist/core/change-status-policy.js +2 -2
- 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 +16 -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 +10 -1
- package/dist/core/config-schema.js +67 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +45 -5
- package/dist/core/file-state.js +56 -24
- package/dist/core/github-copilot/cloud-agent.d.ts +95 -0
- package/dist/core/github-copilot/cloud-agent.js +497 -0
- package/dist/core/global-config.d.ts +16 -0
- package/dist/core/init.d.ts +58 -0
- package/dist/core/init.js +450 -69
- package/dist/core/legacy-cleanup.d.ts +59 -0
- package/dist/core/legacy-cleanup.js +253 -24
- package/dist/core/list.js +5 -7
- package/dist/core/migration.d.ts +73 -1
- package/dist/core/migration.js +360 -11
- 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 +18 -0
- package/dist/core/parsers/requirement-blocks.js +120 -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.d.ts +1 -1
- package/dist/core/parsers/spec-structure.js +20 -30
- package/dist/core/profile-sync-drift.d.ts +0 -8
- package/dist/core/profile-sync-drift.js +48 -49
- package/dist/core/project-config.d.ts +28 -6
- package/dist/core/project-config.js +108 -8
- 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/index.d.ts +1 -0
- package/dist/core/shared/index.js +1 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +41 -0
- package/dist/core/shared/skill-paths.d.ts +13 -0
- package/dist/core/shared/skill-paths.js +23 -0
- package/dist/core/shared/tool-detection.d.ts +25 -5
- package/dist/core/shared/tool-detection.js +166 -36
- package/dist/core/shared-skill-target.d.ts +16 -0
- package/dist/core/shared-skill-target.js +156 -0
- package/dist/core/specs-apply.d.ts +80 -33
- package/dist/core/specs-apply.js +659 -171
- package/dist/core/store/git.d.ts +13 -0
- package/dist/core/store/git.js +24 -0
- package/dist/core/templates/skill-templates.d.ts +1 -1
- package/dist/core/templates/skill-templates.js +1 -1
- package/dist/core/templates/workflows/apply-change.d.ts +10 -0
- package/dist/core/templates/workflows/apply-change.js +48 -158
- package/dist/core/templates/workflows/archive-change.js +170 -42
- package/dist/core/templates/workflows/bulk-archive-change.js +256 -75
- package/dist/core/templates/workflows/continue-change.js +38 -46
- package/dist/core/templates/workflows/explore.js +54 -18
- 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 +13 -7
- package/dist/core/templates/workflows/propose.js +116 -46
- 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 +260 -32
- package/dist/core/templates/workflows/update-change.js +26 -16
- package/dist/core/templates/workflows/verify-change.js +16 -10
- package/dist/core/update.d.ts +40 -5
- package/dist/core/update.js +389 -93
- package/dist/core/validation/constants.d.ts +4 -1
- package/dist/core/validation/constants.js +4 -1
- package/dist/core/validation/validator.d.ts +22 -8
- package/dist/core/validation/validator.js +222 -50
- package/dist/core/version-check.d.ts +152 -0
- package/dist/core/version-check.js +731 -0
- package/dist/core/view.js +12 -17
- package/dist/prompts/searchable-multi-select.js +1 -1
- package/dist/telemetry/config.d.ts +3 -4
- package/dist/telemetry/index.d.ts +10 -5
- package/dist/telemetry/index.js +75 -41
- package/dist/ui/welcome-screen.d.ts +9 -1
- package/dist/ui/welcome-screen.js +91 -38
- package/dist/utils/change-metadata.d.ts +43 -0
- package/dist/utils/change-metadata.js +123 -7
- package/dist/utils/change-utils.d.ts +10 -4
- package/dist/utils/change-utils.js +20 -13
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +92 -6
- package/dist/utils/command-references.js +180 -7
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/file-system.d.ts +10 -0
- package/dist/utils/file-system.js +70 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/interactive.d.ts +23 -0
- package/dist/utils/interactive.js +32 -0
- 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 +115 -0
- package/dist/utils/task-progress.d.ts +17 -0
- package/dist/utils/task-progress.js +41 -13
- package/package.json +14 -12
- package/schemas/spec-driven/schema.yaml +67 -10
- package/schemas/spec-driven/templates/design.md +2 -2
- package/schemas/spec-driven/templates/proposal.md +10 -4
- 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,23 @@ 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
|
-
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, } from './shared/index.js';
|
|
17
|
-
import { detectLegacyArtifacts, cleanupLegacyArtifacts, formatCleanupSummary, formatDetectionSummary, getToolsFromLegacyArtifacts, } from './legacy-cleanup.js';
|
|
16
|
+
import { getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, getToolsWithSkillsDir, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, } from './shared/index.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';
|
|
26
|
+
import { writeSharedSkillTarget } from './shared-skill-target.js';
|
|
27
|
+
import { includesGitHubCopilot, writeCopilotCloudFiles, removeCopilotCloudFiles, isCopilotCloudEnabled, readCopilotCloudOptIn, findUnmanagedCloudFiles } from './github-copilot/cloud-agent.js';
|
|
24
28
|
const require = createRequire(import.meta.url);
|
|
25
29
|
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
26
|
-
const OLD_CORE_WORKFLOWS = ['propose', 'explore', 'apply', 'archive'];
|
|
27
30
|
/**
|
|
28
31
|
* Scans installed workflow artifacts (skills and managed commands) across all configured tools.
|
|
29
32
|
* Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
|
|
@@ -41,6 +44,12 @@ export class UpdateCommand {
|
|
|
41
44
|
constructor(options = {}) {
|
|
42
45
|
this.force = options.force ?? false;
|
|
43
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Refreshes OpenSpec skills and commands for all configured tools,
|
|
49
|
+
* regenerating artifacts according to the effective profile and delivery mode.
|
|
50
|
+
*
|
|
51
|
+
* @param projectPath - Path to the project root containing the openspec directory
|
|
52
|
+
*/
|
|
44
53
|
async execute(projectPath) {
|
|
45
54
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
46
55
|
const openspecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
|
|
@@ -48,7 +57,17 @@ export class UpdateCommand {
|
|
|
48
57
|
if (!await FileSystemUtils.directoryExists(openspecPath)) {
|
|
49
58
|
throw new Error(`No OpenSpec directory found. Run 'openspec init' first.`);
|
|
50
59
|
}
|
|
51
|
-
// 2.
|
|
60
|
+
// 2. Migrate OpenSpec-managed skills left in renamed tool directories
|
|
61
|
+
// (e.g. .kimi -> .kimi-code) so they stay detected and get refreshed,
|
|
62
|
+
// then perform the one-time profile migration if needed before any
|
|
63
|
+
// legacy upgrade generation.
|
|
64
|
+
for (const migration of migrateLegacyToolDirs(resolvedProjectPath)) {
|
|
65
|
+
if (hasMovableContent(migration)) {
|
|
66
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
67
|
+
}
|
|
68
|
+
this.reportKeptInPlace(migration);
|
|
69
|
+
}
|
|
70
|
+
const declinedMigrations = await this.offerConsentedLegacyMigrations(resolvedProjectPath);
|
|
52
71
|
// Use detected tool directories to preserve existing opsx skills/commands.
|
|
53
72
|
const detectedTools = getAvailableTools(resolvedProjectPath);
|
|
54
73
|
migrateIfNeededShared(resolvedProjectPath, detectedTools);
|
|
@@ -58,31 +77,47 @@ export class UpdateCommand {
|
|
|
58
77
|
const delivery = globalConfig.delivery ?? 'both';
|
|
59
78
|
const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
60
79
|
const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
61
|
-
const shouldGenerateSkills = delivery !== 'commands';
|
|
62
|
-
const shouldGenerateCommands = delivery !== 'skills';
|
|
63
80
|
// 4. Detect and handle legacy artifacts + upgrade legacy tools using effective config
|
|
64
|
-
const
|
|
81
|
+
const legacyUpgrade = await this.handleLegacyCleanup(resolvedProjectPath, desiredWorkflows, delivery);
|
|
82
|
+
const { newlyConfiguredTools, workflowOverrides: legacyWorkflowOverrides, deferredGlobalCleanup, } = legacyUpgrade;
|
|
65
83
|
// 5. Find configured tools
|
|
66
84
|
const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath);
|
|
85
|
+
const configuredAndNewTools = [...new Set([...configuredTools, ...newlyConfiguredTools])];
|
|
67
86
|
if (configuredTools.length === 0 && newlyConfiguredTools.length === 0) {
|
|
87
|
+
if (deferredGlobalCleanup) {
|
|
88
|
+
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
89
|
+
}
|
|
90
|
+
if (declinedMigrations.length > 0) {
|
|
91
|
+
// Not an unconfigured project — a configured one the user chose to
|
|
92
|
+
// leave in its former directory. Saying "run init" would be wrong.
|
|
93
|
+
for (const migration of declinedMigrations) {
|
|
94
|
+
console.log(chalk.yellow(`Nothing to update: this project's OpenSpec files are still in ${migration.from}/, ` +
|
|
95
|
+
`which OpenSpec no longer writes.`));
|
|
96
|
+
console.log(chalk.dim(`Re-run "openspec update" and accept the move to ${migration.to}/ to resume updates.`));
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
68
101
|
console.log(chalk.yellow('No configured tools found.'));
|
|
69
102
|
console.log(chalk.dim('Run "openspec init" to set up tools.'));
|
|
70
103
|
return;
|
|
71
104
|
}
|
|
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
|
-
});
|
|
105
|
+
// 6. Check version status for all configured tools, against the same workflow set
|
|
106
|
+
// the generation loop below writes — otherwise a legacy-upgraded tool would be
|
|
107
|
+
// fingerprinted against commands it was never given.
|
|
108
|
+
const toolStatuses = configuredTools.map((toolId) => getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION, {
|
|
109
|
+
workflows: legacyWorkflowOverrides[toolId] ?? desiredWorkflows,
|
|
110
|
+
}));
|
|
82
111
|
const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
|
|
83
112
|
// 7. Smart update detection
|
|
84
113
|
const toolsNeedingVersionUpdate = toolStatuses
|
|
85
|
-
.filter((s) =>
|
|
114
|
+
.filter((s) => {
|
|
115
|
+
if (!s.needsUpdate || delivery !== 'commands') {
|
|
116
|
+
return s.needsUpdate;
|
|
117
|
+
}
|
|
118
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === s.toolId);
|
|
119
|
+
return !tool || !hasGlobalSkillTarget(tool);
|
|
120
|
+
})
|
|
86
121
|
.map((s) => s.toolId);
|
|
87
122
|
const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools);
|
|
88
123
|
const toolsToUpdateSet = new Set([
|
|
@@ -90,56 +125,82 @@ export class UpdateCommand {
|
|
|
90
125
|
...toolsNeedingConfigSync,
|
|
91
126
|
]);
|
|
92
127
|
const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
|
|
93
|
-
if (!this.force && toolsToUpdateSet.size === 0) {
|
|
128
|
+
if (!this.force && toolsToUpdateSet.size === 0 && newlyConfiguredTools.length === 0) {
|
|
129
|
+
if (deferredGlobalCleanup) {
|
|
130
|
+
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
131
|
+
}
|
|
94
132
|
// All tools are up to date
|
|
95
133
|
this.displayUpToDateMessage(toolStatuses);
|
|
134
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
96
135
|
// Still check for new tool directories and extra workflows
|
|
97
136
|
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
98
137
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredTools, desiredWorkflows);
|
|
99
|
-
this.
|
|
138
|
+
this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);
|
|
139
|
+
this.displaySetupNotes(configuredTools);
|
|
100
140
|
return;
|
|
101
141
|
}
|
|
102
142
|
// 8. Display update plan
|
|
103
143
|
if (this.force) {
|
|
104
144
|
console.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
|
|
105
145
|
}
|
|
146
|
+
else if (toolsToUpdateSet.size === 0) {
|
|
147
|
+
console.log('No additional refresh needed after legacy migration.');
|
|
148
|
+
}
|
|
106
149
|
else {
|
|
107
150
|
this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate);
|
|
108
151
|
}
|
|
109
152
|
console.log();
|
|
110
153
|
// 9. Determine what to generate based on delivery
|
|
111
|
-
const
|
|
112
|
-
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
|
|
154
|
+
const deliveryIncludesCommands = delivery !== 'skills';
|
|
113
155
|
// 10. Update tools (all if force, otherwise only those needing update)
|
|
114
156
|
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
|
|
115
157
|
const updatedTools = [];
|
|
116
158
|
const failedTools = [];
|
|
159
|
+
const skillsInvocableCommandSkips = [];
|
|
160
|
+
const zeroArtifactTools = [];
|
|
117
161
|
let removedCommandCount = 0;
|
|
118
162
|
let removedSkillCount = 0;
|
|
119
163
|
let removedDeselectedCommandCount = 0;
|
|
120
164
|
let removedDeselectedSkillCount = 0;
|
|
121
165
|
for (const toolId of toolsToUpdate) {
|
|
122
166
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
123
|
-
if (!tool
|
|
167
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
124
168
|
continue;
|
|
125
169
|
const spinner = ora(`Updating ${tool.name}...`).start();
|
|
126
170
|
try {
|
|
127
|
-
const skillsDir =
|
|
171
|
+
const skillsDir = resolveToolSkillsDir(resolvedProjectPath, tool);
|
|
172
|
+
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
|
|
173
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
174
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
175
|
+
const toolWorkflows = legacyWorkflowOverrides[tool.value] ?? desiredWorkflows;
|
|
176
|
+
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
177
|
+
const commandContents = getCommandContents(toolWorkflows);
|
|
128
178
|
// Generate skill files if delivery includes skills
|
|
129
179
|
if (shouldGenerateSkills) {
|
|
130
180
|
for (const { template, dirName } of skillTemplates) {
|
|
131
181
|
const skillDir = path.join(skillsDir, dirName);
|
|
132
182
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
133
|
-
|
|
134
|
-
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
|
|
183
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
135
184
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
185
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
|
|
136
186
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
137
187
|
}
|
|
138
|
-
|
|
188
|
+
writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
189
|
+
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows);
|
|
139
190
|
}
|
|
140
191
|
// Delete skill directories if delivery is commands-only
|
|
141
|
-
if (!
|
|
142
|
-
removedSkillCount += await this.removeSkillDirs(skillsDir);
|
|
192
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) && !hasGlobalSkillTarget(tool)) {
|
|
193
|
+
removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir);
|
|
194
|
+
// Persist the selected owner even when commands-only delivery leaves
|
|
195
|
+
// this target with no generated skills.
|
|
196
|
+
writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
197
|
+
// A tool with no command adapter now has zero OpenSpec artifacts;
|
|
198
|
+
// say so like init does, rather than deleting its skills silently
|
|
199
|
+
// and letting tool detection re-suggest an init that would also
|
|
200
|
+
// generate nothing under this delivery setting.
|
|
201
|
+
if (!shouldGenerateCommandsForTool(tool.value, delivery)) {
|
|
202
|
+
zeroArtifactTools.push(tool.name);
|
|
203
|
+
}
|
|
143
204
|
}
|
|
144
205
|
// Generate commands if delivery includes commands
|
|
145
206
|
if (shouldGenerateCommands) {
|
|
@@ -147,18 +208,27 @@ export class UpdateCommand {
|
|
|
147
208
|
if (adapter) {
|
|
148
209
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
149
210
|
for (const cmd of generatedCommands) {
|
|
150
|
-
const commandFile =
|
|
211
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(resolvedProjectPath, cmd.path);
|
|
151
212
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
152
213
|
}
|
|
153
|
-
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId,
|
|
214
|
+
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
|
|
154
215
|
}
|
|
155
216
|
}
|
|
217
|
+
else if (deliveryIncludesCommands && resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
|
|
218
|
+
skillsInvocableCommandSkips.push(tool.value);
|
|
219
|
+
}
|
|
156
220
|
// Delete command files if delivery is skills-only
|
|
157
|
-
if (
|
|
221
|
+
if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
|
|
158
222
|
removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
|
|
159
223
|
}
|
|
160
224
|
spinner.succeed(`Updated ${tool.name}`);
|
|
161
225
|
updatedTools.push(tool.name);
|
|
226
|
+
for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
|
|
227
|
+
if (hasMovableContent(migration)) {
|
|
228
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
229
|
+
}
|
|
230
|
+
this.reportKeptInPlace(migration);
|
|
231
|
+
}
|
|
162
232
|
}
|
|
163
233
|
catch (error) {
|
|
164
234
|
spinner.fail(`Failed to update ${tool.name}`);
|
|
@@ -168,6 +238,9 @@ export class UpdateCommand {
|
|
|
168
238
|
});
|
|
169
239
|
}
|
|
170
240
|
}
|
|
241
|
+
if (deferredGlobalCleanup) {
|
|
242
|
+
await this.performDeferredGlobalPromptCleanup(resolvedProjectPath, deferredGlobalCleanup);
|
|
243
|
+
}
|
|
171
244
|
// 11. Summary
|
|
172
245
|
console.log();
|
|
173
246
|
if (updatedTools.length > 0) {
|
|
@@ -176,34 +249,69 @@ export class UpdateCommand {
|
|
|
176
249
|
if (failedTools.length > 0) {
|
|
177
250
|
console.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
|
|
178
251
|
}
|
|
252
|
+
if (skillsInvocableCommandSkips.length > 0) {
|
|
253
|
+
console.log(chalk.dim(`Commands skipped for: ${skillsInvocableCommandSkips.join(', ')} (uses skills)`));
|
|
254
|
+
}
|
|
179
255
|
if (removedCommandCount > 0) {
|
|
180
256
|
console.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
|
|
181
257
|
}
|
|
182
258
|
if (removedSkillCount > 0) {
|
|
183
259
|
console.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
|
|
184
260
|
}
|
|
261
|
+
if (zeroArtifactTools.length > 0) {
|
|
262
|
+
const names = zeroArtifactTools.join(', ');
|
|
263
|
+
console.log(chalk.yellow(`No skills or commands remain for ${names}: delivery is set to 'commands' but ` +
|
|
264
|
+
`${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
|
|
265
|
+
`Run 'openspec config set delivery both' to generate skills.`));
|
|
266
|
+
}
|
|
185
267
|
if (removedDeselectedCommandCount > 0) {
|
|
186
268
|
console.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
|
|
187
269
|
}
|
|
188
270
|
if (removedDeselectedSkillCount > 0) {
|
|
189
271
|
console.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
|
|
190
272
|
}
|
|
191
|
-
// 12. Show onboarding message for newly configured tools from legacy upgrade
|
|
273
|
+
// 12. Show onboarding message for newly configured tools from legacy upgrade.
|
|
274
|
+
// Command tools get the command name their files answer to, skill-only
|
|
275
|
+
// tools their documented skill invocation, and disagreements fall back to
|
|
276
|
+
// naming the skill.
|
|
192
277
|
if (newlyConfiguredTools.length > 0) {
|
|
278
|
+
const referenceFor = (command) => {
|
|
279
|
+
const neutralForm = `the ${transformToSkillReferences(command).slice(1)} skill`;
|
|
280
|
+
const forms = new Set(newlyConfiguredTools.map((toolId) => {
|
|
281
|
+
if (shouldGenerateCommandsForTool(toolId, delivery)) {
|
|
282
|
+
// Name the command the tool's files actually answer to:
|
|
283
|
+
// /opsx-<id> where the filename is the command name.
|
|
284
|
+
const transformer = getTransformerForTool(toolId, delivery, resolveCommandSurfaceCapability(toolId), resolveCommandInvocation(toolId));
|
|
285
|
+
return transformer ? transformer(command) : command;
|
|
286
|
+
}
|
|
287
|
+
return getSkillReferenceTransformer(toolId)(command);
|
|
288
|
+
}));
|
|
289
|
+
return forms.size === 1 ? [...forms][0] : neutralForm;
|
|
290
|
+
};
|
|
291
|
+
// Only hint at workflows these tools actually received. A legacy upgrade
|
|
292
|
+
// can install a narrower set than the profile (inferred Codex prompts).
|
|
293
|
+
const installedWorkflows = [
|
|
294
|
+
...new Set(newlyConfiguredTools.flatMap((toolId) => legacyWorkflowOverrides[toolId] ?? desiredWorkflows)),
|
|
295
|
+
];
|
|
296
|
+
const entries = getOnboardingCommands(installedWorkflows).map(({ command, description }) => [referenceFor(command), description]);
|
|
193
297
|
console.log();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
298
|
+
if (entries.length > 0) {
|
|
299
|
+
const width = Math.max(...entries.map(([reference]) => reference.length));
|
|
300
|
+
console.log(chalk.bold('Getting started:'));
|
|
301
|
+
for (const [reference, description] of entries) {
|
|
302
|
+
console.log(` ${reference.padEnd(width)} ${description}`);
|
|
303
|
+
}
|
|
304
|
+
console.log();
|
|
305
|
+
}
|
|
199
306
|
console.log(`Learn more: ${chalk.cyan('https://github.com/Fission-AI/OpenSpec')}`);
|
|
200
307
|
}
|
|
201
|
-
|
|
308
|
+
await this.syncCopilotCloudFiles(resolvedProjectPath, configuredAndNewTools);
|
|
202
309
|
// 13. Detect new tool directories not currently configured
|
|
203
310
|
this.detectNewTools(resolvedProjectPath, configuredAndNewTools);
|
|
204
311
|
// 14. Display note about extra workflows not in profile
|
|
205
312
|
this.displayExtraWorkflowsNote(resolvedProjectPath, configuredAndNewTools, desiredWorkflows);
|
|
206
|
-
this.
|
|
313
|
+
this.displayMissingCoreWorkflowsNote(profile, globalConfig.workflows);
|
|
314
|
+
this.displaySetupNotes(configuredAndNewTools);
|
|
207
315
|
// 15. List affected tools
|
|
208
316
|
if (updatedTools.length > 0) {
|
|
209
317
|
const toolDisplayNames = updatedTools;
|
|
@@ -211,6 +319,51 @@ export class UpdateCommand {
|
|
|
211
319
|
}
|
|
212
320
|
console.log();
|
|
213
321
|
console.log(chalk.dim('Restart your IDE for changes to take effect.'));
|
|
322
|
+
if (failedTools.length > 0) {
|
|
323
|
+
throw new Error(`OpenSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async syncCopilotCloudFiles(projectPath, configuredTools) {
|
|
327
|
+
try {
|
|
328
|
+
if (includesGitHubCopilot(configuredTools)) {
|
|
329
|
+
// Cloud files are opt-in (see cloud-agent.ts). `update` never prompts,
|
|
330
|
+
// so it only refreshes files the user has already opted into (via
|
|
331
|
+
// `openspec init` or a `githubCopilot.cloudAgent: true` config), or that
|
|
332
|
+
// a pre-opt-in project already has. Opting in is a deliberate init/config
|
|
333
|
+
// step, never a silent side effect of running update.
|
|
334
|
+
if (await isCopilotCloudEnabled(projectPath)) {
|
|
335
|
+
await writeCopilotCloudFiles(projectPath);
|
|
336
|
+
const collisions = await findUnmanagedCloudFiles(projectPath);
|
|
337
|
+
if (collisions.length > 0) {
|
|
338
|
+
console.log(chalk.dim(`Left your existing ${collisions.join(' and ')} untouched — add the OpenSpec ` +
|
|
339
|
+
`install step by hand so the Copilot cloud agent can run openspec.`));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Explicit opt-out (githubCopilot.cloudAgent: false) means "not here":
|
|
344
|
+
// remove any managed files a prior opt-in left behind (customized files
|
|
345
|
+
// are preserved). If the user simply never decided, stay quiet unless
|
|
346
|
+
// we're at an interactive terminal, where a one-line hint aids discovery.
|
|
347
|
+
if (readCopilotCloudOptIn(projectPath) === false) {
|
|
348
|
+
const removed = await removeCopilotCloudFiles(projectPath);
|
|
349
|
+
if (removed > 0) {
|
|
350
|
+
console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (opted out of cloud files)`));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (isInteractive()) {
|
|
354
|
+
console.log(chalk.dim("GitHub Copilot cloud coding-agent files are available (opt-in). Enable with 'openspec init --copilot-cloud'."));
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const removed = await removeCopilotCloudFiles(projectPath);
|
|
359
|
+
if (removed > 0) {
|
|
360
|
+
console.log(chalk.dim(`Removed: ${removed} Copilot cloud agent file(s) (github-copilot not configured)`));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
console.warn(`Warning: failed to sync Copilot cloud agent files: ${message}`);
|
|
366
|
+
}
|
|
214
367
|
}
|
|
215
368
|
/**
|
|
216
369
|
* Display message when all tools are up to date.
|
|
@@ -240,6 +393,18 @@ export class UpdateCommand {
|
|
|
240
393
|
console.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
|
|
241
394
|
}
|
|
242
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Shows manual setup notes for configured tools that need extra
|
|
398
|
+
* configuration before they pick up generated files.
|
|
399
|
+
*/
|
|
400
|
+
displaySetupNotes(toolIds) {
|
|
401
|
+
for (const toolId of toolIds) {
|
|
402
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
403
|
+
if (tool?.setupNote) {
|
|
404
|
+
console.log(chalk.yellow(`Setup required for ${tool.name}: ${tool.setupNote}`));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
243
408
|
/**
|
|
244
409
|
* Detects new tool directories that aren't currently configured and displays a hint.
|
|
245
410
|
*/
|
|
@@ -268,38 +433,41 @@ export class UpdateCommand {
|
|
|
268
433
|
}
|
|
269
434
|
}
|
|
270
435
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
436
|
+
* Point out core workflows a custom profile is missing, so releases that
|
|
437
|
+
* grow CORE_WORKFLOWS stay discoverable. Keep custom profiles user-owned;
|
|
438
|
+
* do not mutate them.
|
|
273
439
|
*/
|
|
274
|
-
|
|
440
|
+
displayMissingCoreWorkflowsNote(profile, workflows) {
|
|
275
441
|
if (profile !== 'custom' || !workflows) {
|
|
276
442
|
return;
|
|
277
443
|
}
|
|
278
444
|
const workflowSet = new Set(workflows);
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
if (!matchesOldCore) {
|
|
445
|
+
const missing = CORE_WORKFLOWS.filter((workflow) => !workflowSet.has(workflow));
|
|
446
|
+
if (missing.length === 0) {
|
|
282
447
|
return;
|
|
283
448
|
}
|
|
284
|
-
|
|
285
|
-
|
|
449
|
+
const label = missing.length === 1 ? 'workflow' : 'workflows';
|
|
450
|
+
const pronoun = missing.length === 1 ? 'it' : 'them';
|
|
451
|
+
console.log(chalk.dim(`Note: Your custom profile is missing ${missing.length} core ${label}: ${missing.join(', ')}`));
|
|
452
|
+
console.log(chalk.dim(`Run \`openspec config profile\` to add ${pronoun}, or \`openspec config profile core\` to use the core set.`));
|
|
286
453
|
}
|
|
287
454
|
/**
|
|
288
455
|
* Removes skill directories for workflows when delivery changed to commands-only.
|
|
289
456
|
* Returns the number of directories removed.
|
|
290
457
|
*/
|
|
291
|
-
async removeSkillDirs(skillsDir) {
|
|
458
|
+
async removeSkillDirs(skillsRoot, skillsDir) {
|
|
292
459
|
let removed = 0;
|
|
293
460
|
for (const workflow of ALL_WORKFLOWS) {
|
|
294
461
|
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
295
462
|
if (!dirName)
|
|
296
463
|
continue;
|
|
297
464
|
const skillDir = path.join(skillsDir, dirName);
|
|
465
|
+
if (!fs.existsSync(skillDir))
|
|
466
|
+
continue;
|
|
467
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
298
468
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
removed++;
|
|
302
|
-
}
|
|
469
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
470
|
+
removed++;
|
|
303
471
|
}
|
|
304
472
|
catch {
|
|
305
473
|
// Ignore errors
|
|
@@ -311,7 +479,7 @@ export class UpdateCommand {
|
|
|
311
479
|
* Removes skill directories for workflows that are no longer selected in the active profile.
|
|
312
480
|
* Returns the number of directories removed.
|
|
313
481
|
*/
|
|
314
|
-
async removeUnselectedSkillDirs(skillsDir, desiredWorkflows) {
|
|
482
|
+
async removeUnselectedSkillDirs(skillsRoot, skillsDir, desiredWorkflows) {
|
|
315
483
|
const desiredSet = new Set(desiredWorkflows);
|
|
316
484
|
let removed = 0;
|
|
317
485
|
for (const workflow of ALL_WORKFLOWS) {
|
|
@@ -321,11 +489,12 @@ export class UpdateCommand {
|
|
|
321
489
|
if (!dirName)
|
|
322
490
|
continue;
|
|
323
491
|
const skillDir = path.join(skillsDir, dirName);
|
|
492
|
+
if (!fs.existsSync(skillDir))
|
|
493
|
+
continue;
|
|
494
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
324
495
|
try {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
removed++;
|
|
328
|
-
}
|
|
496
|
+
await fs.promises.rm(skillDir, { recursive: true, force: true });
|
|
497
|
+
removed++;
|
|
329
498
|
}
|
|
330
499
|
catch {
|
|
331
500
|
// Ignore errors
|
|
@@ -344,7 +513,7 @@ export class UpdateCommand {
|
|
|
344
513
|
return 0;
|
|
345
514
|
for (const workflow of ALL_WORKFLOWS) {
|
|
346
515
|
const cmdPath = adapter.getFilePath(workflow);
|
|
347
|
-
const fullPath =
|
|
516
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
348
517
|
try {
|
|
349
518
|
if (fs.existsSync(fullPath)) {
|
|
350
519
|
await fs.promises.unlink(fullPath);
|
|
@@ -371,7 +540,7 @@ export class UpdateCommand {
|
|
|
371
540
|
if (desiredSet.has(workflow))
|
|
372
541
|
continue;
|
|
373
542
|
const cmdPath = adapter.getFilePath(workflow);
|
|
374
|
-
const fullPath =
|
|
543
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
375
544
|
try {
|
|
376
545
|
if (fs.existsSync(fullPath)) {
|
|
377
546
|
await fs.promises.unlink(fullPath);
|
|
@@ -384,6 +553,72 @@ export class UpdateCommand {
|
|
|
384
553
|
}
|
|
385
554
|
return removed;
|
|
386
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* Offers to move OpenSpec content out of a renamed tool's former directory
|
|
558
|
+
* when the old location might still be the live one — today, Windsurf's
|
|
559
|
+
* `.windsurf/` after the Devin Desktop rebrand.
|
|
560
|
+
*
|
|
561
|
+
* Interactive runs are asked, because nothing on disk distinguishes a user
|
|
562
|
+
* who took the rebrand from one still on a pre-rebrand Windsurf build that
|
|
563
|
+
* reads only `.windsurf/`. `--force` and non-interactive runs migrate, which
|
|
564
|
+
* is what an unattended upgrade wants.
|
|
565
|
+
*/
|
|
566
|
+
/** Surfaces files the move left behind rather than overwriting. */
|
|
567
|
+
reportKeptInPlace(migration) {
|
|
568
|
+
const notice = keptInPlaceNotice(migration);
|
|
569
|
+
if (notice)
|
|
570
|
+
console.log(chalk.dim(notice));
|
|
571
|
+
}
|
|
572
|
+
async offerConsentedLegacyMigrations(projectPath) {
|
|
573
|
+
const pending = findLegacyToolMigrations(projectPath).filter((m) => m.needsConsent);
|
|
574
|
+
const declined = [];
|
|
575
|
+
if (pending.length === 0)
|
|
576
|
+
return declined;
|
|
577
|
+
for (const migration of pending) {
|
|
578
|
+
// Nothing movable: every legacy file differs from its counterpart, so
|
|
579
|
+
// there is no move to offer. Still say so — silence would leave two
|
|
580
|
+
// divergent copies the user never hears about.
|
|
581
|
+
if (!hasMovableContent(migration)) {
|
|
582
|
+
this.reportKeptInPlace(migration);
|
|
583
|
+
console.log();
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
console.log(chalk.yellow(legacyMigrationNotice(migration)));
|
|
587
|
+
if (!this.force && isInteractive()) {
|
|
588
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
589
|
+
let shouldMigrate;
|
|
590
|
+
try {
|
|
591
|
+
shouldMigrate = await confirm({
|
|
592
|
+
message: `Move ${describeLegacyMigration(migration)} from ${migration.from}/ to ${migration.to}/?`,
|
|
593
|
+
default: true,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// Closed stdin is not consent, and it must not abort the update.
|
|
598
|
+
shouldMigrate = false;
|
|
599
|
+
}
|
|
600
|
+
if (!shouldMigrate) {
|
|
601
|
+
// Say what declining costs. OpenSpec writes the current root now, so
|
|
602
|
+
// the files keep working where they are, but OpenSpec stops managing
|
|
603
|
+
// them — it no longer looks in the former directory.
|
|
604
|
+
console.log(chalk.dim(`Left in place. OpenSpec writes ${migration.to}/ now and will not manage ` +
|
|
605
|
+
`${migration.from}/, so those files stay as they are until you move them. ` +
|
|
606
|
+
`You will be asked again next run.`));
|
|
607
|
+
console.log();
|
|
608
|
+
declined.push(migration);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
for (const applied of migrateLegacyToolDirs(projectPath, [migration.toolId])) {
|
|
613
|
+
if (hasMovableContent(applied)) {
|
|
614
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(applied)}: ${applied.from} → ${applied.to}`));
|
|
615
|
+
}
|
|
616
|
+
this.reportKeptInPlace(applied);
|
|
617
|
+
}
|
|
618
|
+
console.log();
|
|
619
|
+
}
|
|
620
|
+
return declined;
|
|
621
|
+
}
|
|
387
622
|
/**
|
|
388
623
|
* Detect and handle legacy OpenSpec artifacts.
|
|
389
624
|
* Unlike init, update warns but continues if legacy files found in non-interactive mode.
|
|
@@ -393,25 +628,37 @@ export class UpdateCommand {
|
|
|
393
628
|
// Detect legacy artifacts
|
|
394
629
|
const detection = await detectLegacyArtifacts(projectPath);
|
|
395
630
|
if (!detection.hasLegacyArtifacts) {
|
|
396
|
-
return []; // No legacy artifacts found
|
|
631
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} }; // No legacy artifacts found
|
|
397
632
|
}
|
|
398
633
|
// Show what was detected
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
634
|
+
const immediateSummary = formatDetectionSummary(omitGlobalLegacyPromptFiles(detection));
|
|
635
|
+
const deferredSummary = formatDeferredGlobalPromptSummary(detection);
|
|
636
|
+
if (immediateSummary || deferredSummary) {
|
|
637
|
+
console.log();
|
|
638
|
+
if (immediateSummary) {
|
|
639
|
+
console.log(immediateSummary);
|
|
640
|
+
console.log();
|
|
641
|
+
}
|
|
642
|
+
if (deferredSummary) {
|
|
643
|
+
console.log(deferredSummary);
|
|
644
|
+
console.log();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
402
647
|
const canPrompt = isInteractive();
|
|
403
648
|
if (this.force) {
|
|
404
|
-
|
|
405
|
-
await this.
|
|
406
|
-
|
|
407
|
-
|
|
649
|
+
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
650
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
651
|
+
return {
|
|
652
|
+
...legacyUpgrade,
|
|
653
|
+
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
654
|
+
};
|
|
408
655
|
}
|
|
409
656
|
if (!canPrompt) {
|
|
410
657
|
// Non-interactive mode without --force: warn and continue
|
|
411
658
|
// (Unlike init, update doesn't abort - user may just want to update skills)
|
|
412
659
|
console.log(chalk.yellow('⚠ Run with --force to auto-cleanup legacy files, or run interactively.'));
|
|
413
660
|
console.log();
|
|
414
|
-
return [];
|
|
661
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
415
662
|
}
|
|
416
663
|
// Interactive mode: prompt for confirmation
|
|
417
664
|
const { confirm } = await import('@inquirer/prompts');
|
|
@@ -420,14 +667,47 @@ export class UpdateCommand {
|
|
|
420
667
|
default: true,
|
|
421
668
|
});
|
|
422
669
|
if (shouldCleanup) {
|
|
423
|
-
await this.
|
|
424
|
-
|
|
425
|
-
return
|
|
670
|
+
const legacyUpgrade = await this.upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery);
|
|
671
|
+
await this.performImmediateLegacyCleanup(projectPath, detection);
|
|
672
|
+
return {
|
|
673
|
+
...legacyUpgrade,
|
|
674
|
+
deferredGlobalCleanup: pickGlobalLegacyPromptFiles(detection, detection.globalSlashCommandFiles),
|
|
675
|
+
};
|
|
426
676
|
}
|
|
427
677
|
else {
|
|
428
678
|
console.log(chalk.dim('Skipping legacy cleanup. Continuing with skill update...'));
|
|
429
679
|
console.log();
|
|
430
|
-
return [];
|
|
680
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Cleans approved repo-local legacy artifacts before configured tools refresh.
|
|
685
|
+
*/
|
|
686
|
+
async performImmediateLegacyCleanup(projectPath, detection) {
|
|
687
|
+
const immediateDetection = omitGlobalLegacyPromptFiles(detection);
|
|
688
|
+
if (immediateDetection.hasLegacyArtifacts) {
|
|
689
|
+
await this.performLegacyCleanup(projectPath, immediateDetection);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Cleans approved global Codex prompts after configured tools refresh so newly
|
|
694
|
+
* installed replacement skills can retire their prompts in the same run.
|
|
695
|
+
*/
|
|
696
|
+
async performDeferredGlobalPromptCleanup(projectPath, detection) {
|
|
697
|
+
const availableCodexWorkflows = new Set(scanInstalledWorkflows(projectPath, ['codex']));
|
|
698
|
+
const removableMatches = getLegacyGlobalPromptMatches(detection)
|
|
699
|
+
.filter((prompt) => prompt.workflowIds.every((workflowId) => availableCodexWorkflows.has(workflowId)));
|
|
700
|
+
if (removableMatches.length > 0) {
|
|
701
|
+
await this.performLegacyCleanup(projectPath, pickGlobalLegacyPromptFiles(detection, removableMatches.map((prompt) => prompt.path)));
|
|
702
|
+
}
|
|
703
|
+
const blockedMatches = getLegacyGlobalPromptMatches(detection)
|
|
704
|
+
.filter((prompt) => !removableMatches.some((match) => match.path === prompt.path));
|
|
705
|
+
if (blockedMatches.length > 0) {
|
|
706
|
+
console.log(chalk.yellow('Preserved deferred global prompts without replacement skills:'));
|
|
707
|
+
for (const prompt of blockedMatches) {
|
|
708
|
+
console.log(chalk.dim(` - ${prompt.toolId}: ${prompt.path}`));
|
|
709
|
+
}
|
|
710
|
+
console.log();
|
|
431
711
|
}
|
|
432
712
|
}
|
|
433
713
|
/**
|
|
@@ -445,14 +725,14 @@ export class UpdateCommand {
|
|
|
445
725
|
console.log();
|
|
446
726
|
}
|
|
447
727
|
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
728
|
+
* Upgrades unconfigured legacy tools into the skills-based setup and carries
|
|
729
|
+
* workflow overrides for migrations that should mirror legacy Codex prompts.
|
|
450
730
|
*/
|
|
451
731
|
async upgradeLegacyTools(projectPath, detection, canPrompt, desiredWorkflows, delivery) {
|
|
452
732
|
// Get tools that had legacy artifacts
|
|
453
733
|
const legacyTools = getToolsFromLegacyArtifacts(detection);
|
|
454
734
|
if (legacyTools.length === 0) {
|
|
455
|
-
return [];
|
|
735
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
456
736
|
}
|
|
457
737
|
// Get currently configured tools
|
|
458
738
|
const configuredTools = getConfiguredToolsForProfileSync(projectPath);
|
|
@@ -460,13 +740,13 @@ export class UpdateCommand {
|
|
|
460
740
|
// Filter to tools that aren't already configured
|
|
461
741
|
const unconfiguredLegacyTools = legacyTools.filter((t) => !configuredSet.has(t));
|
|
462
742
|
if (unconfiguredLegacyTools.length === 0) {
|
|
463
|
-
return [];
|
|
743
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
464
744
|
}
|
|
465
745
|
// Get valid tools (those with skillsDir)
|
|
466
746
|
const validToolIds = new Set(getToolsWithSkillsDir());
|
|
467
747
|
const validUnconfiguredTools = unconfiguredLegacyTools.filter((t) => validToolIds.has(t));
|
|
468
748
|
if (validUnconfiguredTools.length === 0) {
|
|
469
|
-
return [];
|
|
749
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
470
750
|
}
|
|
471
751
|
// Show what tools were detected from legacy artifacts
|
|
472
752
|
console.log(chalk.bold('Tools detected from legacy artifacts:'));
|
|
@@ -502,32 +782,42 @@ export class UpdateCommand {
|
|
|
502
782
|
if (selectedTools.length === 0) {
|
|
503
783
|
console.log(chalk.dim('Skipping tool setup.'));
|
|
504
784
|
console.log();
|
|
505
|
-
return [];
|
|
785
|
+
return { newlyConfiguredTools: [], workflowOverrides: {} };
|
|
506
786
|
}
|
|
507
787
|
}
|
|
788
|
+
const inferredCodexWorkflows = getLegacyWorkflowIdsForTool(detection, 'codex');
|
|
508
789
|
// Create skills/commands for selected tools using effective profile+delivery.
|
|
509
790
|
const newlyConfigured = [];
|
|
510
|
-
const
|
|
511
|
-
const shouldGenerateCommands = delivery !== 'skills';
|
|
512
|
-
const skillTemplates = shouldGenerateSkills ? getSkillTemplates(desiredWorkflows) : [];
|
|
513
|
-
const commandContents = shouldGenerateCommands ? getCommandContents(desiredWorkflows) : [];
|
|
791
|
+
const workflowOverrides = {};
|
|
514
792
|
for (const toolId of selectedTools) {
|
|
515
793
|
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
516
|
-
if (!tool
|
|
794
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
517
795
|
continue;
|
|
518
796
|
const spinner = ora(`Setting up ${tool.name}...`).start();
|
|
519
797
|
try {
|
|
520
|
-
const skillsDir =
|
|
798
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
799
|
+
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : projectPath;
|
|
800
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
801
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
802
|
+
const toolWorkflows = (tool.value === 'codex' && inferredCodexWorkflows.length > 0
|
|
803
|
+
? inferredCodexWorkflows
|
|
804
|
+
: desiredWorkflows);
|
|
805
|
+
if (tool.value === 'codex' && inferredCodexWorkflows.length > 0) {
|
|
806
|
+
workflowOverrides[tool.value] = inferredCodexWorkflows;
|
|
807
|
+
}
|
|
808
|
+
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
809
|
+
const commandContents = getCommandContents(toolWorkflows);
|
|
521
810
|
// Create skill files when delivery includes skills
|
|
522
811
|
if (shouldGenerateSkills) {
|
|
523
812
|
for (const { template, dirName } of skillTemplates) {
|
|
524
813
|
const skillDir = path.join(skillsDir, dirName);
|
|
525
814
|
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
526
|
-
|
|
527
|
-
const transformer = (tool.value === 'opencode' || tool.value === 'pi' || tool.value === 'oh-my-pi') ? transformToHyphenCommands : undefined;
|
|
815
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
528
816
|
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
817
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
|
|
529
818
|
await FileSystemUtils.writeFile(skillFile, skillContent);
|
|
530
819
|
}
|
|
820
|
+
writeSharedSkillTarget(projectPath, tool.value);
|
|
531
821
|
}
|
|
532
822
|
// Create commands when delivery includes commands
|
|
533
823
|
if (shouldGenerateCommands) {
|
|
@@ -535,13 +825,19 @@ export class UpdateCommand {
|
|
|
535
825
|
if (adapter) {
|
|
536
826
|
const generatedCommands = generateCommands(commandContents, adapter);
|
|
537
827
|
for (const cmd of generatedCommands) {
|
|
538
|
-
const commandFile =
|
|
828
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmd.path);
|
|
539
829
|
await FileSystemUtils.writeFile(commandFile, cmd.fileContent);
|
|
540
830
|
}
|
|
541
831
|
}
|
|
542
832
|
}
|
|
543
833
|
spinner.succeed(`Setup complete for ${tool.name}`);
|
|
544
834
|
newlyConfigured.push(toolId);
|
|
835
|
+
for (const migration of migrateLegacyToolDirs(projectPath, [tool.value], 'after-generation')) {
|
|
836
|
+
if (hasMovableContent(migration)) {
|
|
837
|
+
console.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
838
|
+
}
|
|
839
|
+
this.reportKeptInPlace(migration);
|
|
840
|
+
}
|
|
545
841
|
}
|
|
546
842
|
catch (error) {
|
|
547
843
|
spinner.fail(`Failed to set up ${tool.name}`);
|
|
@@ -551,7 +847,7 @@ export class UpdateCommand {
|
|
|
551
847
|
if (newlyConfigured.length > 0) {
|
|
552
848
|
console.log();
|
|
553
849
|
}
|
|
554
|
-
return newlyConfigured;
|
|
850
|
+
return { newlyConfiguredTools: newlyConfigured, workflowOverrides };
|
|
555
851
|
}
|
|
556
852
|
}
|
|
557
853
|
//# sourceMappingURL=update.js.map
|