@niujiajie/freespec 0.0.0-stage → 1.15.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/LICENSE +22 -0
- package/README.md +58 -2
- package/bin/freespec.js +5 -0
- package/dist/cli/index.d.ts +25 -0
- package/dist/cli/index.js +359 -0
- package/dist/commands/shared-output.d.ts +18 -0
- package/dist/commands/shared-output.js +61 -0
- package/dist/commands/workflow/default-schema.d.ts +2 -0
- package/dist/commands/workflow/default-schema.js +4 -0
- package/dist/commands/workflow/index.d.ts +13 -0
- package/dist/commands/workflow/index.js +10 -0
- package/dist/commands/workflow/instructions.d.ts +39 -0
- package/dist/commands/workflow/instructions.js +565 -0
- package/dist/commands/workflow/new-change.d.ts +19 -0
- package/dist/commands/workflow/new-change.js +135 -0
- package/dist/commands/workflow/shared.d.ts +94 -0
- package/dist/commands/workflow/shared.js +169 -0
- package/dist/commands/workflow/status.d.ts +18 -0
- package/dist/commands/workflow/status.js +201 -0
- package/dist/core/artifact-graph/graph.d.ts +72 -0
- package/dist/core/artifact-graph/graph.js +165 -0
- package/dist/core/artifact-graph/index.d.ts +9 -0
- package/dist/core/artifact-graph/index.js +14 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +208 -0
- package/dist/core/artifact-graph/instruction-loader.js +268 -0
- package/dist/core/artifact-graph/outputs.d.ts +19 -0
- package/dist/core/artifact-graph/outputs.js +351 -0
- package/dist/core/artifact-graph/resolver.d.ts +89 -0
- package/dist/core/artifact-graph/resolver.js +342 -0
- package/dist/core/artifact-graph/schema.d.ts +27 -0
- package/dist/core/artifact-graph/schema.js +160 -0
- package/dist/core/artifact-graph/state.d.ts +12 -0
- package/dist/core/artifact-graph/state.js +31 -0
- package/dist/core/artifact-graph/types.d.ts +40 -0
- package/dist/core/artifact-graph/types.js +59 -0
- package/dist/core/available-tools.d.ts +18 -0
- package/dist/core/available-tools.js +49 -0
- package/dist/core/change-layout.d.ts +17 -0
- package/dist/core/change-layout.js +30 -0
- package/dist/core/change-metadata/index.d.ts +2 -0
- package/dist/core/change-metadata/index.js +2 -0
- package/dist/core/change-metadata/schema.d.ts +23 -0
- package/dist/core/change-metadata/schema.js +46 -0
- package/dist/core/change-status-policy.d.ts +49 -0
- package/dist/core/change-status-policy.js +50 -0
- package/dist/core/command-generation/adapters/amazon-q.d.ts +17 -0
- package/dist/core/command-generation/adapters/amazon-q.js +32 -0
- package/dist/core/command-generation/adapters/antigravity.d.ts +13 -0
- package/dist/core/command-generation/adapters/antigravity.js +27 -0
- package/dist/core/command-generation/adapters/atomcode.d.ts +24 -0
- package/dist/core/command-generation/adapters/atomcode.js +46 -0
- package/dist/core/command-generation/adapters/auggie.d.ts +13 -0
- package/dist/core/command-generation/adapters/auggie.js +28 -0
- package/dist/core/command-generation/adapters/bob.d.ts +18 -0
- package/dist/core/command-generation/adapters/bob.js +33 -0
- package/dist/core/command-generation/adapters/claude.d.ts +13 -0
- package/dist/core/command-generation/adapters/claude.js +32 -0
- package/dist/core/command-generation/adapters/cline.d.ts +14 -0
- package/dist/core/command-generation/adapters/cline.js +27 -0
- package/dist/core/command-generation/adapters/codeassistant.d.ts +15 -0
- package/dist/core/command-generation/adapters/codeassistant.js +29 -0
- package/dist/core/command-generation/adapters/codebuddy.d.ts +13 -0
- package/dist/core/command-generation/adapters/codebuddy.js +29 -0
- package/dist/core/command-generation/adapters/codestudio.d.ts +13 -0
- package/dist/core/command-generation/adapters/codestudio.js +27 -0
- package/dist/core/command-generation/adapters/command-code.d.ts +19 -0
- package/dist/core/command-generation/adapters/command-code.js +34 -0
- package/dist/core/command-generation/adapters/continue.d.ts +13 -0
- package/dist/core/command-generation/adapters/continue.js +33 -0
- package/dist/core/command-generation/adapters/costrict.d.ts +13 -0
- package/dist/core/command-generation/adapters/costrict.js +28 -0
- package/dist/core/command-generation/adapters/crush.d.ts +13 -0
- package/dist/core/command-generation/adapters/crush.js +30 -0
- package/dist/core/command-generation/adapters/cursor.d.ts +14 -0
- package/dist/core/command-generation/adapters/cursor.js +31 -0
- 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/easycode.d.ts +17 -0
- package/dist/core/command-generation/adapters/easycode.js +28 -0
- package/dist/core/command-generation/adapters/factory.d.ts +13 -0
- package/dist/core/command-generation/adapters/factory.js +28 -0
- package/dist/core/command-generation/adapters/gemini.d.ts +13 -0
- package/dist/core/command-generation/adapters/gemini.js +27 -0
- package/dist/core/command-generation/adapters/gigacode.d.ts +17 -0
- package/dist/core/command-generation/adapters/gigacode.js +31 -0
- package/dist/core/command-generation/adapters/github-copilot.d.ts +13 -0
- package/dist/core/command-generation/adapters/github-copilot.js +27 -0
- package/dist/core/command-generation/adapters/iflow.d.ts +13 -0
- package/dist/core/command-generation/adapters/iflow.js +30 -0
- package/dist/core/command-generation/adapters/index.d.ts +40 -0
- package/dist/core/command-generation/adapters/index.js +40 -0
- package/dist/core/command-generation/adapters/junie.d.ts +13 -0
- package/dist/core/command-generation/adapters/junie.js +27 -0
- package/dist/core/command-generation/adapters/kilocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/kilocode.js +23 -0
- package/dist/core/command-generation/adapters/kiro.d.ts +13 -0
- package/dist/core/command-generation/adapters/kiro.js +27 -0
- package/dist/core/command-generation/adapters/lingma.d.ts +13 -0
- package/dist/core/command-generation/adapters/lingma.js +30 -0
- package/dist/core/command-generation/adapters/oh-my-pi.d.ts +21 -0
- package/dist/core/command-generation/adapters/oh-my-pi.js +42 -0
- package/dist/core/command-generation/adapters/opencode.d.ts +14 -0
- package/dist/core/command-generation/adapters/opencode.js +38 -0
- package/dist/core/command-generation/adapters/pi.d.ts +18 -0
- package/dist/core/command-generation/adapters/pi.js +39 -0
- package/dist/core/command-generation/adapters/qoder.d.ts +13 -0
- package/dist/core/command-generation/adapters/qoder.js +30 -0
- package/dist/core/command-generation/adapters/qwen.d.ts +17 -0
- package/dist/core/command-generation/adapters/qwen.js +31 -0
- package/dist/core/command-generation/adapters/roocode.d.ts +14 -0
- package/dist/core/command-generation/adapters/roocode.js +27 -0
- package/dist/core/command-generation/adapters/trae.d.ts +13 -0
- package/dist/core/command-generation/adapters/trae.js +28 -0
- 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 +29 -0
- package/dist/core/command-generation/generator.js +43 -0
- package/dist/core/command-generation/index.d.ts +22 -0
- package/dist/core/command-generation/index.js +24 -0
- 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.d.ts +36 -0
- package/dist/core/command-generation/registry.js +114 -0
- package/dist/core/command-generation/toml.d.ts +19 -0
- package/dist/core/command-generation/toml.js +39 -0
- package/dist/core/command-generation/types.d.ts +63 -0
- package/dist/core/command-generation/types.js +8 -0
- package/dist/core/command-generation/yaml.d.ts +36 -0
- package/dist/core/command-generation/yaml.js +48 -0
- package/dist/core/command-surface.d.ts +15 -0
- package/dist/core/command-surface.js +33 -0
- package/dist/core/config-prompts.d.ts +9 -0
- package/dist/core/config-prompts.js +56 -0
- package/dist/core/config-schema.d.ts +98 -0
- package/dist/core/config-schema.js +308 -0
- package/dist/core/config.d.ts +50 -0
- package/dist/core/config.js +122 -0
- package/dist/core/file-state.d.ts +36 -0
- package/dist/core/file-state.js +144 -0
- package/dist/core/file-view.d.ts +13 -0
- package/dist/core/file-view.js +14 -0
- package/dist/core/global-config.d.ts +101 -0
- package/dist/core/global-config.js +182 -0
- package/dist/core/id.d.ts +17 -0
- package/dist/core/id.js +30 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.js +7 -0
- package/dist/core/init.d.ts +57 -0
- package/dist/core/init.js +836 -0
- package/dist/core/list.d.ts +11 -0
- package/dist/core/list.js +197 -0
- package/dist/core/migration.d.ts +121 -0
- package/dist/core/migration.js +540 -0
- package/dist/core/onboarding-commands.d.ts +30 -0
- package/dist/core/onboarding-commands.js +38 -0
- package/dist/core/openspec-root.d.ts +47 -0
- package/dist/core/openspec-root.js +196 -0
- package/dist/core/planning-home.d.ts +16 -0
- package/dist/core/planning-home.js +67 -0
- package/dist/core/profile-sync-drift.d.ts +31 -0
- package/dist/core/profile-sync-drift.js +201 -0
- package/dist/core/profiles.d.ts +19 -0
- package/dist/core/profiles.js +33 -0
- package/dist/core/project-config.d.ts +132 -0
- package/dist/core/project-config.js +519 -0
- package/dist/core/references.d.ts +10 -0
- package/dist/core/references.js +46 -0
- package/dist/core/root-selection.d.ts +59 -0
- package/dist/core/root-selection.js +132 -0
- package/dist/core/shared/allowed-tools.d.ts +12 -0
- package/dist/core/shared/allowed-tools.js +12 -0
- package/dist/core/shared/ide-restart.d.ts +26 -0
- package/dist/core/shared/ide-restart.js +42 -0
- package/dist/core/shared/index.d.ts +11 -0
- package/dist/core/shared/index.js +11 -0
- package/dist/core/shared/skill-content-equivalence.d.ts +7 -0
- package/dist/core/shared/skill-content-equivalence.js +45 -0
- package/dist/core/shared/skill-files.d.ts +7 -0
- package/dist/core/shared/skill-files.js +43 -0
- package/dist/core/shared/skill-generation.d.ts +49 -0
- package/dist/core/shared/skill-generation.js +125 -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 +99 -0
- package/dist/core/shared/tool-detection.js +318 -0
- package/dist/core/shared-skill-target.d.ts +51 -0
- package/dist/core/shared-skill-target.js +239 -0
- package/dist/core/store/errors.d.ts +20 -0
- package/dist/core/store/errors.js +22 -0
- package/dist/core/store/index.d.ts +2 -0
- package/dist/core/store/index.js +2 -0
- package/dist/core/styles/palette.d.ts +7 -0
- package/dist/core/styles/palette.js +8 -0
- package/dist/core/templates/optional-workflow.d.ts +69 -0
- package/dist/core/templates/optional-workflow.js +140 -0
- package/dist/core/templates/types.d.ts +19 -0
- package/dist/core/templates/types.js +5 -0
- package/dist/core/templates/workflows/personal.d.ts +22 -0
- package/dist/core/templates/workflows/personal.js +225 -0
- package/dist/core/update-plan.d.ts +127 -0
- package/dist/core/update-plan.js +390 -0
- package/dist/core/update.d.ts +154 -0
- package/dist/core/update.js +723 -0
- package/dist/core/version-check.d.ts +188 -0
- package/dist/core/version-check.js +906 -0
- package/dist/core/zod-issues.d.ts +4 -0
- package/dist/core/zod-issues.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/prompts/searchable-multi-select.d.ts +35 -0
- package/dist/prompts/searchable-multi-select.js +188 -0
- package/dist/telemetry/config.d.ts +37 -0
- package/dist/telemetry/config.js +148 -0
- package/dist/telemetry/index.d.ts +39 -0
- package/dist/telemetry/index.js +217 -0
- package/dist/telemetry/opt-out.d.ts +17 -0
- package/dist/telemetry/opt-out.js +27 -0
- package/dist/ui/ascii-patterns.d.ts +16 -0
- package/dist/ui/ascii-patterns.js +133 -0
- package/dist/ui/welcome-screen.d.ts +18 -0
- package/dist/ui/welcome-screen.js +199 -0
- package/dist/utils/change-metadata.d.ts +116 -0
- package/dist/utils/change-metadata.js +324 -0
- package/dist/utils/change-utils.d.ts +79 -0
- package/dist/utils/change-utils.js +121 -0
- package/dist/utils/ci.d.ts +12 -0
- package/dist/utils/ci.js +16 -0
- package/dist/utils/command-references.d.ts +104 -0
- package/dist/utils/command-references.js +190 -0
- package/dist/utils/date.d.ts +8 -0
- package/dist/utils/date.js +13 -0
- package/dist/utils/executable-path.d.ts +5 -0
- package/dist/utils/executable-path.js +44 -0
- package/dist/utils/file-system.d.ts +58 -0
- package/dist/utils/file-system.js +432 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/interactive.d.ts +85 -0
- package/dist/utils/interactive.js +174 -0
- package/dist/utils/item-discovery.d.ts +15 -0
- package/dist/utils/item-discovery.js +33 -0
- package/dist/utils/line-endings.d.ts +34 -0
- package/dist/utils/line-endings.js +46 -0
- package/dist/utils/match.d.ts +3 -0
- package/dist/utils/match.js +22 -0
- package/dist/utils/nested-change.d.ts +35 -0
- package/dist/utils/nested-change.js +185 -0
- package/dist/utils/shell-detection.d.ts +20 -0
- package/dist/utils/shell-detection.js +94 -0
- package/dist/utils/task-progress.d.ts +67 -0
- package/dist/utils/task-progress.js +184 -0
- package/package.json +74 -4
- package/schemas//351/273/230/350/256/244/schema.yaml +68 -0
- package/schemas//351/273/230/350/256/244/templates//344/273/273/345/212/241/346/270/205/345/215/225.md +25 -0
- package/schemas//351/273/230/350/256/244/templates//346/265/213/350/257/225/347/224/250/344/276/213.md +34 -0
- package/schemas//351/273/230/350/256/244/templates//350/256/276/350/256/241/346/226/207/346/241/243.md +172 -0
- package/schemas//351/273/230/350/256/244/templates//351/234/200/346/261/202/346/226/207/346/241/243.md +19 -0
- package/skill-files//344/273/273/345/212/241/references//346/212/244/346/240/217.md +43 -0
- package/skill-files//344/273/273/345/212/241/references//347/262/222/345/272/246.md +23 -0
- package/skill-files//344/273/273/345/212/241/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//344/273/273/345/212/241/scripts/check-tasks.mjs +83 -0
- package/skill-files//345/206/231Java/references/Java/350/247/204/350/214/203.md +25 -0
- package/skill-files//345/206/231Java/references//345/215/261/351/231/251/345/206/231/346/263/225.md +27 -0
- package/skill-files//345/206/231Java/references//345/256/214/346/210/220/345/257/271/347/205/247.md +15 -0
- package/skill-files//345/206/231Java/references//346/212/244/346/240/217.md +35 -0
- package/skill-files//345/206/231Java/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/206/231Java/references//350/270/251/345/235/221.md +17 -0
- package/skill-files//345/256/241Java/references//346/212/244/346/240/217.md +19 -0
- package/skill-files//345/256/241Java/references//347/244/272/344/276/213.md +23 -0
- package/skill-files//345/256/241Java/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//345/256/241Java/references//350/247/204/345/210/231.md +23 -0
- package/skill-files//345/256/241Java/references//351/242/206/345/237/237/346/243/200/351/241/271.md +58 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//346/212/244/346/240/217.md +18 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//347/273/264/345/272/246.md +17 -0
- package/skill-files//345/256/241/346/226/207/346/241/243/references//350/270/251/345/235/221.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//344/272/244/346/216/245.md +11 -0
- package/skill-files//346/226/260/345/273/272/references//346/212/244/346/240/217.md +14 -0
- package/skill-files//346/226/260/345/273/272/references//347/272/246/346/235/237.md +7 -0
- package/skill-files//346/267/261/346/200/235/config/prompts.yaml +114 -0
- package/skill-files//346/267/261/346/200/235/references//344/270/211/346/226/271/346/240/270/345/257/271.md +72 -0
- package/skill-files//346/267/261/346/200/235/references//344/276/213/345/255/220.md +39 -0
- package/skill-files//346/267/261/346/200/235/references//346/200/216/344/271/210/346/203/263.md +61 -0
- package/skill-files//346/267/261/346/200/235/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//346/267/261/346/200/235/references//347/202/271/351/200/211/346/217/220/351/227/256.md +66 -0
- package/skill-files//346/267/261/346/200/235/references//347/231/273/350/256/260.md +45 -0
- package/skill-files//346/267/261/346/200/235/references//347/272/246/346/235/237.md +9 -0
- package/skill-files//347/224/250/344/276/213/references//346/200/216/344/271/210/345/206/231.md +13 -0
- package/skill-files//347/224/250/344/276/213/references//346/212/244/346/240/217.md +42 -0
- package/skill-files//347/224/250/344/276/213/references//347/272/246/346/235/237.md +5 -0
- package/skill-files//347/224/250/344/276/213/scripts/check-cases.mjs +70 -0
- package/skill-files//350/256/276/350/256/241/references//345/207/206/345/244/207.md +25 -0
- package/skill-files//350/256/276/350/256/241/references//346/212/244/346/240/217.md +134 -0
- package/skill-files//350/256/276/350/256/241/references//346/226/207/351/243/216.md +17 -0
- package/skill-files//350/256/276/350/256/241/references//347/225/214/351/235/242.md +18 -0
- package/skill-files//350/256/276/350/256/241/references//347/261/273/345/236/213.md +19 -0
- package/skill-files//350/256/276/350/256/241/references//347/272/246/346/235/237.md +25 -0
- package/skill-files//350/256/276/350/256/241/scripts/_smoke.mjs +72 -0
- package/skill-files//350/256/276/350/256/241/scripts/check-design.mjs +331 -0
- package/skill-files//351/234/200/346/261/202/references//346/212/244/346/240/217.md +26 -0
- package/skill-files//351/234/200/346/261/202/references//346/226/207/351/243/216.md +23 -0
- package/skill-files//351/234/200/346/261/202/references//346/267/261/346/200/235/346/221/204/345/205/245.md +21 -0
- package/skill-files//351/234/200/346/261/202/references//347/272/246/346/235/237.md +8 -0
- package/skill-files//351/234/200/346/261/202/references//351/234/200/346/261/202/345/276/205/345/256/232.md +13 -0
- package/skill-files//351/234/200/346/261/202/scripts/check-requirements.mjs +60 -0
- package/skill-files//351/252/214/346/224/266/references//346/212/244/346/240/217.md +27 -0
- package/skill-files//351/252/214/346/224/266/references//346/240/270/345/257/271.md +17 -0
- package/skill-files//351/252/214/346/224/266/references//347/272/246/346/235/237.md +7 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Refreshes FreeSpec skills and commands for configured tools.
|
|
5
|
+
* Supports profile-aware updates, delivery changes, migration, and smart update detection.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
12
|
+
import { getTransformerForTool } from '../utils/command-references.js';
|
|
13
|
+
import { AI_TOOLS, OPENSPEC_DIR_NAME } from './config.js';
|
|
14
|
+
import { generateCommands, CommandAdapterRegistry, } from './command-generation/index.js';
|
|
15
|
+
import { getToolSkillStatus, getToolVersionStatus, getSkillTemplates, getCommandContents, generateSkillContent, copySkillExtras, hasGlobalSkillTarget, resolveToolSkillsDir, toolSupportsSkills, formatIdeRestart, } from './shared/index.js';
|
|
16
|
+
import { isInteractive } from '../utils/interactive.js';
|
|
17
|
+
import { getGlobalConfig, getGlobalConfigPath, serializeGlobalConfig } from './global-config.js';
|
|
18
|
+
import { getProfileWorkflows, ALL_WORKFLOWS, CORE_WORKFLOWS } from './profiles.js';
|
|
19
|
+
import { getAvailableTools } from './available-tools.js';
|
|
20
|
+
import { WORKFLOW_TO_SKILL_DIR, getConfiguredToolsForProfileSync, getToolsNeedingProfileSync, } from './profile-sync-drift.js';
|
|
21
|
+
import { scanInstalledWorkflows as scanInstalledWorkflowsShared, migrateIfNeeded as migrateIfNeededShared, computeProfileMigration, applyProfileMigration, findLegacyToolMigrations, planLegacyToolMigrations, migrateLegacyToolDirs, describeLegacyMigration, legacyMigrationNotice, keptInPlaceNotice, hasMovableContent, } from './migration.js';
|
|
22
|
+
import { resolveCommandSurfaceCapability, resolveCommandInvocation, shouldGenerateCommandsForTool, shouldGenerateSkillsForTool, shouldReconcileCommandFilesForTool, shouldRemoveSkillsForTool, } from './command-surface.js';
|
|
23
|
+
import { resolveSharedSkillWriters, sharedSkillTargetMarkerPath, } from './shared-skill-target.js';
|
|
24
|
+
import { DISK_VIEW } from './file-view.js';
|
|
25
|
+
import { realFileOps, UpdatePlanRecorder, } from './update-plan.js';
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const { version: OPENSPEC_VERSION } = require('../../package.json');
|
|
28
|
+
function throwIfUpdateFailed(failedTools) {
|
|
29
|
+
if (failedTools.length > 0) {
|
|
30
|
+
throw new Error(`FreeSpec update failed for: ${failedTools.map((tool) => tool.name).join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checkout artifacts that are not real content drift: a UTF-8 BOM and the CRLF
|
|
35
|
+
* line endings a Windows clone with `core.autocrlf` reintroduces on every
|
|
36
|
+
* checkout of committed generated files.
|
|
37
|
+
*/
|
|
38
|
+
function normalizeGeneratedFile(content) {
|
|
39
|
+
return content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Scans installed workflow artifacts (skills and managed commands) across all configured tools.
|
|
43
|
+
* Returns the union of detected workflow IDs that match ALL_WORKFLOWS.
|
|
44
|
+
*
|
|
45
|
+
* Wrapper around the shared migration module's scanInstalledWorkflows that accepts tool IDs.
|
|
46
|
+
*/
|
|
47
|
+
export function scanInstalledWorkflows(projectPath, toolIds, view) {
|
|
48
|
+
const tools = toolIds
|
|
49
|
+
.map((id) => AI_TOOLS.find((t) => t.value === id))
|
|
50
|
+
.filter((t) => t != null);
|
|
51
|
+
return scanInstalledWorkflowsShared(projectPath, tools, view);
|
|
52
|
+
}
|
|
53
|
+
export class UpdateCommand {
|
|
54
|
+
force;
|
|
55
|
+
dryRun;
|
|
56
|
+
recorder = null;
|
|
57
|
+
ops = realFileOps;
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.force = options.force ?? false;
|
|
60
|
+
this.dryRun = options.dryRun ?? false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reports what `execute` would add, change or delete, writing nothing.
|
|
64
|
+
*
|
|
65
|
+
* Same code path as the real update: the plan is recorded by the write seam
|
|
66
|
+
* the generation loop already goes through, so it cannot drift from it.
|
|
67
|
+
*/
|
|
68
|
+
async plan(projectPath) {
|
|
69
|
+
if (!this.dryRun) {
|
|
70
|
+
throw new Error('plan() requires the update command to be constructed with dryRun: true');
|
|
71
|
+
}
|
|
72
|
+
await this.execute(projectPath);
|
|
73
|
+
return this.recorder?.getPlan() ?? { changes: [], notes: [], failures: [] };
|
|
74
|
+
}
|
|
75
|
+
/** Prints progress on a real run, stays quiet while planning. */
|
|
76
|
+
log(...args) {
|
|
77
|
+
if (this.dryRun)
|
|
78
|
+
return;
|
|
79
|
+
console.log(...args);
|
|
80
|
+
}
|
|
81
|
+
/** A spinner on a real run, a no-op while planning. */
|
|
82
|
+
startSpinner(text) {
|
|
83
|
+
if (this.dryRun) {
|
|
84
|
+
return { succeed: () => { }, fail: () => { }, info: () => { } };
|
|
85
|
+
}
|
|
86
|
+
return ora(text).start();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Refreshes FreeSpec skills and commands for all configured tools,
|
|
90
|
+
* regenerating artifacts according to the effective profile and delivery mode.
|
|
91
|
+
*
|
|
92
|
+
* @param projectPath - Path to the project root containing the freespec directory
|
|
93
|
+
*/
|
|
94
|
+
async execute(projectPath) {
|
|
95
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
96
|
+
const freespecPath = path.join(resolvedProjectPath, OPENSPEC_DIR_NAME);
|
|
97
|
+
if (this.dryRun) {
|
|
98
|
+
this.recorder = new UpdatePlanRecorder(resolvedProjectPath);
|
|
99
|
+
this.ops = this.recorder;
|
|
100
|
+
}
|
|
101
|
+
// 1. Check freespec directory exists
|
|
102
|
+
if (!await FileSystemUtils.directoryExists(freespecPath)) {
|
|
103
|
+
throw new Error(`No FreeSpec directory found. Run 'freespec init' first.`);
|
|
104
|
+
}
|
|
105
|
+
// 2. Migrate FreeSpec-managed skills left in renamed tool directories
|
|
106
|
+
// (e.g. .kimi -> .kimi-code) so they stay detected and get refreshed,
|
|
107
|
+
// then perform the one-time profile migration if needed before any
|
|
108
|
+
// legacy upgrade generation.
|
|
109
|
+
if (!this.dryRun) {
|
|
110
|
+
for (const migration of migrateLegacyToolDirs(resolvedProjectPath)) {
|
|
111
|
+
if (hasMovableContent(migration)) {
|
|
112
|
+
this.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
113
|
+
}
|
|
114
|
+
this.reportKeptInPlace(migration);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// A dry run plans the same moves. Every later step reads `this.view` -
|
|
118
|
+
// the disk in a real run, the tree as planned in a dry run - so it sees
|
|
119
|
+
// the moved files exactly as the real update would.
|
|
120
|
+
if (this.dryRun) {
|
|
121
|
+
await this.planLegacyToolMoves(resolvedProjectPath, 'before-generation');
|
|
122
|
+
}
|
|
123
|
+
const declinedMigrations = this.dryRun
|
|
124
|
+
? []
|
|
125
|
+
: await this.offerConsentedLegacyMigrations(resolvedProjectPath);
|
|
126
|
+
// Use detected tool directories to preserve existing freespec skills/commands.
|
|
127
|
+
const detectedTools = getAvailableTools(resolvedProjectPath, this.view);
|
|
128
|
+
if (!this.dryRun) {
|
|
129
|
+
migrateIfNeededShared(resolvedProjectPath, detectedTools);
|
|
130
|
+
}
|
|
131
|
+
// 3. Read global config for profile/delivery. A dry run plans the one-time
|
|
132
|
+
// profile migration's save instead of making it, and resolves against
|
|
133
|
+
// the config that save would leave — otherwise the preview would plan
|
|
134
|
+
// for a different profile than the real update acts on.
|
|
135
|
+
let globalConfig = getGlobalConfig();
|
|
136
|
+
if (this.recorder) {
|
|
137
|
+
const migration = computeProfileMigration(resolvedProjectPath, detectedTools, this.view);
|
|
138
|
+
if (migration) {
|
|
139
|
+
globalConfig = applyProfileMigration(migration);
|
|
140
|
+
await this.recorder.writeFile(getGlobalConfigPath(), serializeGlobalConfig(globalConfig), 'freespec');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const profile = globalConfig.profile ?? 'core';
|
|
144
|
+
const delivery = globalConfig.delivery ?? 'both';
|
|
145
|
+
const profileWorkflows = getProfileWorkflows(profile, globalConfig.workflows);
|
|
146
|
+
const desiredWorkflows = profileWorkflows.filter((workflow) => ALL_WORKFLOWS.includes(workflow));
|
|
147
|
+
// 5. Find configured tools
|
|
148
|
+
const configuredTools = getConfiguredToolsForProfileSync(resolvedProjectPath, this.view);
|
|
149
|
+
if (configuredTools.length === 0) {
|
|
150
|
+
if (declinedMigrations.length > 0) {
|
|
151
|
+
// Not an unconfigured project — a configured one the user chose to
|
|
152
|
+
// leave in its former directory. Saying "run init" would be wrong.
|
|
153
|
+
for (const migration of declinedMigrations) {
|
|
154
|
+
this.log(chalk.yellow(`Nothing to update: this project's FreeSpec files are still in ${migration.from}/, ` +
|
|
155
|
+
`which FreeSpec no longer writes.`));
|
|
156
|
+
this.log(chalk.dim(`Re-run "freespec update" and accept the move to ${migration.to}/ to resume updates.`));
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.log(chalk.yellow('No configured tools found.'));
|
|
161
|
+
this.log(chalk.dim('Run "freespec init" to set up tools.'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// 6. Check version status for all configured tools, against the same workflow set
|
|
165
|
+
// the generation loop below writes — otherwise a legacy-upgraded tool would be
|
|
166
|
+
// fingerprinted against commands it was never given.
|
|
167
|
+
const toolStatuses = configuredTools.map((toolId) => getToolVersionStatus(resolvedProjectPath, toolId, OPENSPEC_VERSION, {
|
|
168
|
+
workflows: desiredWorkflows,
|
|
169
|
+
delivery,
|
|
170
|
+
view: this.view,
|
|
171
|
+
}));
|
|
172
|
+
const statusByTool = new Map(toolStatuses.map((status) => [status.toolId, status]));
|
|
173
|
+
// 7. Smart update detection
|
|
174
|
+
const toolsNeedingVersionUpdate = toolStatuses
|
|
175
|
+
.filter((s) => {
|
|
176
|
+
if (!s.needsUpdate || delivery !== 'commands') {
|
|
177
|
+
return s.needsUpdate;
|
|
178
|
+
}
|
|
179
|
+
const tool = AI_TOOLS.find((candidate) => candidate.value === s.toolId);
|
|
180
|
+
return !tool || !hasGlobalSkillTarget(tool);
|
|
181
|
+
})
|
|
182
|
+
.map((s) => s.toolId);
|
|
183
|
+
const toolsNeedingConfigSync = getToolsNeedingProfileSync(resolvedProjectPath, desiredWorkflows, delivery, configuredTools, this.view);
|
|
184
|
+
const toolsWithDriftedSkills = this.findToolsWithDriftedSkills(resolvedProjectPath, configuredTools, delivery, (toolId) => desiredWorkflows);
|
|
185
|
+
const toolsToUpdateSet = new Set([
|
|
186
|
+
...toolsNeedingVersionUpdate,
|
|
187
|
+
...toolsNeedingConfigSync,
|
|
188
|
+
...toolsWithDriftedSkills,
|
|
189
|
+
]);
|
|
190
|
+
const toolsUpToDate = toolStatuses.filter((s) => !toolsToUpdateSet.has(s.toolId));
|
|
191
|
+
if (!this.force && toolsToUpdateSet.size === 0) {
|
|
192
|
+
// All tools are up to date
|
|
193
|
+
this.displayUpToDateMessage(toolStatuses);
|
|
194
|
+
// Still check for new tool directories and extra workflows
|
|
195
|
+
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
196
|
+
this.displayProfileNotes(resolvedProjectPath, configuredTools, desiredWorkflows, profile, delivery);
|
|
197
|
+
this.displaySetupNotes(configuredTools);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// 8. Display update plan
|
|
201
|
+
if (this.force) {
|
|
202
|
+
this.log(`Force updating ${configuredTools.length} tool(s): ${configuredTools.join(', ')}`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
this.displayUpdatePlan([...toolsToUpdateSet], statusByTool, toolsUpToDate, new Set(toolsWithDriftedSkills));
|
|
206
|
+
}
|
|
207
|
+
this.log();
|
|
208
|
+
// 9. Determine what to generate based on delivery
|
|
209
|
+
const deliveryIncludesCommands = delivery !== 'skills';
|
|
210
|
+
// 10. Update tools (all if force, otherwise only those needing update)
|
|
211
|
+
const toolsToUpdate = this.force ? configuredTools : [...toolsToUpdateSet];
|
|
212
|
+
const sharedSkillWriters = resolveSharedSkillWriters(resolvedProjectPath, configuredTools
|
|
213
|
+
.map((toolId) => AI_TOOLS.find((tool) => tool.value === toolId))
|
|
214
|
+
.filter((tool) => tool !== undefined), this.view);
|
|
215
|
+
const updatedTools = [];
|
|
216
|
+
const updatedToolIds = [];
|
|
217
|
+
const failedTools = [];
|
|
218
|
+
const skillsInvocableCommandSkips = [];
|
|
219
|
+
const zeroArtifactTools = [];
|
|
220
|
+
let removedCommandCount = 0;
|
|
221
|
+
let removedSkillCount = 0;
|
|
222
|
+
let removedDeselectedCommandCount = 0;
|
|
223
|
+
let removedDeselectedSkillCount = 0;
|
|
224
|
+
for (const toolId of toolsToUpdate) {
|
|
225
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
226
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
227
|
+
continue;
|
|
228
|
+
const spinner = this.startSpinner(`Updating ${tool.name}...`);
|
|
229
|
+
try {
|
|
230
|
+
const skillsDir = resolveToolSkillsDir(resolvedProjectPath, tool);
|
|
231
|
+
const skillsRoot = hasGlobalSkillTarget(tool) ? skillsDir : resolvedProjectPath;
|
|
232
|
+
const shouldGenerateSkills = shouldGenerateSkillsForTool(tool.value, delivery);
|
|
233
|
+
const shouldGenerateCommands = shouldGenerateCommandsForTool(tool.value, delivery);
|
|
234
|
+
const writesSkills = !tool.skillsDir || sharedSkillWriters.has(tool.value);
|
|
235
|
+
const toolWorkflows = desiredWorkflows;
|
|
236
|
+
const skillTemplates = getSkillTemplates(toolWorkflows);
|
|
237
|
+
const commandContents = getCommandContents(toolWorkflows);
|
|
238
|
+
// Generate skill files if delivery includes skills
|
|
239
|
+
if (shouldGenerateSkills && writesSkills) {
|
|
240
|
+
for (const { template, dirName } of skillTemplates) {
|
|
241
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
242
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
243
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
244
|
+
const skillContent = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
245
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillFile);
|
|
246
|
+
await this.ops.writeFile(skillFile, skillContent, tool.value);
|
|
247
|
+
await copySkillExtras(skillDir, template.name);
|
|
248
|
+
}
|
|
249
|
+
await this.writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
250
|
+
removedDeselectedSkillCount += await this.removeUnselectedSkillDirs(skillsRoot, skillsDir, toolWorkflows, tool.value);
|
|
251
|
+
}
|
|
252
|
+
// Delete skill directories if delivery is commands-only
|
|
253
|
+
if (shouldRemoveSkillsForTool(tool.value, delivery) &&
|
|
254
|
+
writesSkills &&
|
|
255
|
+
!hasGlobalSkillTarget(tool)) {
|
|
256
|
+
removedSkillCount += await this.removeSkillDirs(skillsRoot, skillsDir, tool.value);
|
|
257
|
+
// Persist the selected owner even when commands-only delivery leaves
|
|
258
|
+
// this target with no generated skills.
|
|
259
|
+
await this.writeSharedSkillTarget(resolvedProjectPath, tool.value);
|
|
260
|
+
// A tool with no command adapter now has zero FreeSpec artifacts;
|
|
261
|
+
// say so like init does, rather than deleting its skills silently
|
|
262
|
+
// and letting tool detection re-suggest an init that would also
|
|
263
|
+
// generate nothing under this delivery setting.
|
|
264
|
+
if (!shouldGenerateCommandsForTool(tool.value, delivery)) {
|
|
265
|
+
zeroArtifactTools.push(tool.name);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Generate commands if delivery includes commands
|
|
269
|
+
if (shouldGenerateCommands) {
|
|
270
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
271
|
+
if (adapter) {
|
|
272
|
+
const generatedCommands = generateCommands(commandContents, adapter);
|
|
273
|
+
for (const cmd of generatedCommands) {
|
|
274
|
+
const commandFile = FileSystemUtils.resolveProjectArtifactPath(resolvedProjectPath, cmd.path);
|
|
275
|
+
await this.ops.writeFile(commandFile, cmd.fileContent, tool.value);
|
|
276
|
+
}
|
|
277
|
+
removedDeselectedCommandCount += await this.removeUnselectedCommandFiles(resolvedProjectPath, toolId, toolWorkflows);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else if (deliveryIncludesCommands && resolveCommandSurfaceCapability(tool.value) === 'skills-invocable') {
|
|
281
|
+
skillsInvocableCommandSkips.push(tool.value);
|
|
282
|
+
}
|
|
283
|
+
// Delete command files if delivery is skills-only
|
|
284
|
+
if (shouldReconcileCommandFilesForTool(tool.value, delivery)) {
|
|
285
|
+
removedCommandCount += await this.removeCommandFiles(resolvedProjectPath, toolId);
|
|
286
|
+
}
|
|
287
|
+
spinner.succeed(`Updated ${tool.name}`);
|
|
288
|
+
updatedTools.push(tool.name);
|
|
289
|
+
updatedToolIds.push(tool.value);
|
|
290
|
+
if (!this.dryRun) {
|
|
291
|
+
for (const migration of migrateLegacyToolDirs(resolvedProjectPath, [tool.value], 'after-generation')) {
|
|
292
|
+
if (hasMovableContent(migration)) {
|
|
293
|
+
this.log(chalk.dim(`Migrated ${describeLegacyMigration(migration)}: ${migration.from} → ${migration.to}`));
|
|
294
|
+
}
|
|
295
|
+
this.reportKeptInPlace(migration);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
await this.planLegacyToolMoves(resolvedProjectPath, 'after-generation', [tool.value]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
spinner.fail(`Failed to update ${tool.name}`);
|
|
304
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
305
|
+
failedTools.push({ name: tool.name, error: message });
|
|
306
|
+
// A dry run reports the failure in the plan rather than throwing it:
|
|
307
|
+
// one broken tool must not cost the caller the other tools' changes.
|
|
308
|
+
this.recorder?.addFailure(tool.value, message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// 11. Summary
|
|
312
|
+
this.log();
|
|
313
|
+
if (updatedTools.length > 0) {
|
|
314
|
+
this.log(chalk.green(`✓ Updated: ${updatedTools.join(', ')} (v${OPENSPEC_VERSION})`));
|
|
315
|
+
}
|
|
316
|
+
if (failedTools.length > 0) {
|
|
317
|
+
this.log(chalk.red(`✗ Failed: ${failedTools.map(f => `${f.name} (${f.error})`).join(', ')}`));
|
|
318
|
+
}
|
|
319
|
+
if (skillsInvocableCommandSkips.length > 0) {
|
|
320
|
+
this.log(chalk.dim(`Commands skipped for: ${skillsInvocableCommandSkips.join(', ')} (uses skills)`));
|
|
321
|
+
}
|
|
322
|
+
if (removedCommandCount > 0) {
|
|
323
|
+
this.log(chalk.dim(`Removed: ${removedCommandCount} command files (delivery: skills)`));
|
|
324
|
+
}
|
|
325
|
+
if (removedSkillCount > 0) {
|
|
326
|
+
this.log(chalk.dim(`Removed: ${removedSkillCount} skill directories (delivery: commands)`));
|
|
327
|
+
}
|
|
328
|
+
if (zeroArtifactTools.length > 0) {
|
|
329
|
+
const names = zeroArtifactTools.join(', ');
|
|
330
|
+
this.log(chalk.yellow(`No skills or commands remain for ${names}: delivery is set to 'commands' but ` +
|
|
331
|
+
`${zeroArtifactTools.length === 1 ? 'it supports' : 'they support'} only skills. ` +
|
|
332
|
+
`Run 'freespec config set delivery both' to generate skills.`));
|
|
333
|
+
}
|
|
334
|
+
if (removedDeselectedCommandCount > 0) {
|
|
335
|
+
this.log(chalk.dim(`Removed: ${removedDeselectedCommandCount} command files (deselected workflows)`));
|
|
336
|
+
}
|
|
337
|
+
if (removedDeselectedSkillCount > 0) {
|
|
338
|
+
this.log(chalk.dim(`Removed: ${removedDeselectedSkillCount} skill directories (deselected workflows)`));
|
|
339
|
+
}
|
|
340
|
+
// 13. Detect new tool directories not currently configured
|
|
341
|
+
this.detectNewTools(resolvedProjectPath, configuredTools);
|
|
342
|
+
// 14. Display the profile notes
|
|
343
|
+
this.displayProfileNotes(resolvedProjectPath, configuredTools, desiredWorkflows, profile, delivery);
|
|
344
|
+
this.displaySetupNotes(configuredTools);
|
|
345
|
+
// 15. List affected tools
|
|
346
|
+
if (updatedTools.length > 0) {
|
|
347
|
+
const toolDisplayNames = updatedTools;
|
|
348
|
+
this.log(chalk.dim(`Tools: ${toolDisplayNames.join(', ')}`));
|
|
349
|
+
}
|
|
350
|
+
this.log();
|
|
351
|
+
const affectedToolIds = updatedToolIds;
|
|
352
|
+
const restartHint = formatIdeRestart(affectedToolIds, delivery);
|
|
353
|
+
if (restartHint) {
|
|
354
|
+
this.log(chalk.dim(restartHint));
|
|
355
|
+
}
|
|
356
|
+
this.stopIfUpdateFailed(failedTools);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Records the shared-skills-root owner through the write seam, so a dry run
|
|
360
|
+
* lists the marker file a real update would write.
|
|
361
|
+
*/
|
|
362
|
+
async writeSharedSkillTarget(projectPath, toolId) {
|
|
363
|
+
const marker = sharedSkillTargetMarkerPath(projectPath, toolId);
|
|
364
|
+
if (!marker)
|
|
365
|
+
return;
|
|
366
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, marker);
|
|
367
|
+
await this.ops.writeFile(marker, `${toolId}\n`, toolId);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Where a real update throws for failed tools, it also stops. A dry run
|
|
371
|
+
* stops at the same point - so it plans nothing the real update would not
|
|
372
|
+
* reach - but returns instead of throwing: the failures are already in the
|
|
373
|
+
* plan, and the caller still gets every change planned before them.
|
|
374
|
+
*
|
|
375
|
+
* @returns whether the update stops here.
|
|
376
|
+
*/
|
|
377
|
+
stopIfUpdateFailed(failedTools) {
|
|
378
|
+
if (!this.dryRun)
|
|
379
|
+
throwIfUpdateFailed(failedTools);
|
|
380
|
+
return failedTools.length > 0;
|
|
381
|
+
}
|
|
382
|
+
/** The tree as the real update would see it at this point. */
|
|
383
|
+
get view() {
|
|
384
|
+
return this.recorder?.view ?? DISK_VIEW;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Plans the moves out of former tool roots that a non-interactive update
|
|
388
|
+
* makes, against the tree as planned so far.
|
|
389
|
+
*/
|
|
390
|
+
async planLegacyToolMoves(projectPath, timing, toolIds) {
|
|
391
|
+
const recorder = this.recorder;
|
|
392
|
+
if (!recorder)
|
|
393
|
+
return;
|
|
394
|
+
for (const op of planLegacyToolMigrations(projectPath, timing, toolIds, recorder.view)) {
|
|
395
|
+
try {
|
|
396
|
+
if (op.disposition === 'move') {
|
|
397
|
+
await recorder.moveFile(op.source, op.destination, op.toolId);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
await recorder.removeFile(op.source, op.toolId);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// The real migration leaves a file it cannot move where it is, and
|
|
405
|
+
// says nothing; so does the plan.
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (op.needsConsent) {
|
|
409
|
+
recorder.addNote('legacy-tool-directories', 'Moves out of a renamed tool directory are listed as a non-interactive update makes them. At a terminal, update asks first, and declining leaves those files where they are.');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Tools whose SKILL.md bodies no longer match what this CLI generates.
|
|
415
|
+
*
|
|
416
|
+
* Skill freshness was decided solely by the `generatedBy:` line, so a body
|
|
417
|
+
* edited after generation — a "helpful" PR touching `.claude/skills/**`, a
|
|
418
|
+
* dotfile sync, another agent — left `update` reporting the install healthy.
|
|
419
|
+
* Command files never had that gap: `areCommandFilesUpToDate` content-
|
|
420
|
+
* compares them, and the same comparison belongs on the higher-authority
|
|
421
|
+
* surface. A missing skill file is left to the profile-sync check, which
|
|
422
|
+
* already knows what a partial install means.
|
|
423
|
+
*/
|
|
424
|
+
findToolsWithDriftedSkills(projectPath, toolIds, delivery, workflowsForTool) {
|
|
425
|
+
return toolIds.filter((toolId) => {
|
|
426
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
427
|
+
if (!tool || !toolSupportsSkills(tool))
|
|
428
|
+
return false;
|
|
429
|
+
if (!shouldGenerateSkillsForTool(tool.value, delivery))
|
|
430
|
+
return false;
|
|
431
|
+
// A shared skills root is generated with its owner's transformer, so
|
|
432
|
+
// only the owner may compare it. getToolSkillStatus settles ownership.
|
|
433
|
+
if (!getToolSkillStatus(projectPath, tool.value, this.view).configured)
|
|
434
|
+
return false;
|
|
435
|
+
const skillsDir = resolveToolSkillsDir(projectPath, tool);
|
|
436
|
+
const transformer = getTransformerForTool(tool.value, delivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
437
|
+
return getSkillTemplates(workflowsForTool(toolId)).some(({ template, dirName }) => {
|
|
438
|
+
const skillFile = path.join(skillsDir, dirName, 'SKILL.md');
|
|
439
|
+
if (!this.view.exists(skillFile))
|
|
440
|
+
return false;
|
|
441
|
+
try {
|
|
442
|
+
const existing = this.view.read(skillFile);
|
|
443
|
+
const generated = generateSkillContent(template, OPENSPEC_VERSION, transformer);
|
|
444
|
+
return normalizeGeneratedFile(existing) !== normalizeGeneratedFile(generated);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Display message when all tools are up to date.
|
|
454
|
+
*/
|
|
455
|
+
displayUpToDateMessage(toolStatuses) {
|
|
456
|
+
const toolNames = toolStatuses.map((s) => s.toolId);
|
|
457
|
+
this.log(chalk.green(`✓ All ${toolStatuses.length} tool(s) up to date (v${OPENSPEC_VERSION})`));
|
|
458
|
+
this.log(chalk.dim(` Tools: ${toolNames.join(', ')}`));
|
|
459
|
+
this.log();
|
|
460
|
+
this.log(chalk.dim('Use --force to refresh files anyway.'));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Display the update plan showing which tools need updating.
|
|
464
|
+
*/
|
|
465
|
+
displayUpdatePlan(toolsToUpdate, statusByTool, upToDate, driftedSkills = new Set()) {
|
|
466
|
+
const updates = toolsToUpdate.map((toolId) => {
|
|
467
|
+
const status = statusByTool.get(toolId);
|
|
468
|
+
if (status?.needsUpdate) {
|
|
469
|
+
const fromVersion = status.generatedByVersion ?? 'unknown';
|
|
470
|
+
return `${status.toolId} (${fromVersion} → ${OPENSPEC_VERSION})`;
|
|
471
|
+
}
|
|
472
|
+
// Say why: a user who edited a SKILL.md on purpose is owed the reason
|
|
473
|
+
// their edit is about to be overwritten.
|
|
474
|
+
if (driftedSkills.has(toolId)) {
|
|
475
|
+
return `${toolId} (skill files differ from the generated content)`;
|
|
476
|
+
}
|
|
477
|
+
return `${toolId} (config sync)`;
|
|
478
|
+
});
|
|
479
|
+
this.log(`Updating ${toolsToUpdate.length} tool(s): ${updates.join(', ')}`);
|
|
480
|
+
if (upToDate.length > 0) {
|
|
481
|
+
const upToDateNames = upToDate.map((s) => s.toolId);
|
|
482
|
+
this.log(chalk.dim(`Already up to date: ${upToDateNames.join(', ')}`));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Shows manual setup notes for configured tools that need extra
|
|
487
|
+
* configuration before they pick up generated files.
|
|
488
|
+
*/
|
|
489
|
+
displaySetupNotes(toolIds) {
|
|
490
|
+
for (const toolId of toolIds) {
|
|
491
|
+
const tool = AI_TOOLS.find((t) => t.value === toolId);
|
|
492
|
+
if (tool?.setupNote) {
|
|
493
|
+
this.log(chalk.yellow(`Setup required for ${tool.name}: ${tool.setupNote}`));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Detects new tool directories that aren't currently configured and displays a hint.
|
|
499
|
+
*/
|
|
500
|
+
detectNewTools(projectPath, configuredTools) {
|
|
501
|
+
const availableTools = getAvailableTools(projectPath);
|
|
502
|
+
const configuredSet = new Set(configuredTools);
|
|
503
|
+
const newTools = availableTools.filter((t) => !configuredSet.has(t.value));
|
|
504
|
+
if (newTools.length > 0) {
|
|
505
|
+
const newToolNames = newTools.map((tool) => tool.name);
|
|
506
|
+
const isSingleTool = newToolNames.length === 1;
|
|
507
|
+
const toolNoun = isSingleTool ? 'tool' : 'tools';
|
|
508
|
+
const pronoun = isSingleTool ? 'it' : 'them';
|
|
509
|
+
this.log();
|
|
510
|
+
this.log(chalk.yellow(`Detected new ${toolNoun}: ${newToolNames.join(', ')}. Run 'freespec init' to add ${pronoun}.`));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Prints the profile notes, in order, with one pointer at
|
|
515
|
+
* `freespec config profile` rather than three.
|
|
516
|
+
*
|
|
517
|
+
* Every note is evaluated: reading them as one short-circuited `||` chain
|
|
518
|
+
* would swallow whichever ran second.
|
|
519
|
+
*/
|
|
520
|
+
displayProfileNotes(projectPath, configuredTools, desiredWorkflows, profile, _delivery) {
|
|
521
|
+
this.displayExtraWorkflowsNote(projectPath, configuredTools, desiredWorkflows ?? []);
|
|
522
|
+
this.displayMissingCoreWorkflowsNote(profile, desiredWorkflows);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Displays a note about extra workflows installed that aren't in the current profile.
|
|
526
|
+
*/
|
|
527
|
+
displayExtraWorkflowsNote(projectPath, configuredTools, profileWorkflows) {
|
|
528
|
+
const installedWorkflows = scanInstalledWorkflows(projectPath, configuredTools);
|
|
529
|
+
const profileSet = new Set(profileWorkflows);
|
|
530
|
+
const extraWorkflows = installedWorkflows.filter((w) => !profileSet.has(w));
|
|
531
|
+
if (extraWorkflows.length > 0) {
|
|
532
|
+
this.log(chalk.dim(`Note: ${extraWorkflows.length} extra workflows not in profile (use \`freespec config profile\` to manage)`));
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Point out core workflows a custom profile is missing, so releases that
|
|
539
|
+
* grow CORE_WORKFLOWS stay discoverable. Keep custom profiles user-owned;
|
|
540
|
+
* do not mutate them.
|
|
541
|
+
*/
|
|
542
|
+
displayMissingCoreWorkflowsNote(profile, workflows) {
|
|
543
|
+
if (profile !== 'custom' || !workflows) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
const workflowSet = new Set(workflows);
|
|
547
|
+
const missing = CORE_WORKFLOWS.filter((workflow) => !workflowSet.has(workflow));
|
|
548
|
+
if (missing.length === 0) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
const label = missing.length === 1 ? 'workflow' : 'workflows';
|
|
552
|
+
const pronoun = missing.length === 1 ? 'it' : 'them';
|
|
553
|
+
this.log(chalk.dim(`Note: Your custom profile is missing ${missing.length} core ${label}: ${missing.join(', ')}`));
|
|
554
|
+
this.log(chalk.dim(`Run \`freespec config profile\` to add ${pronoun}, or \`freespec config profile core\` to use the core set.`));
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Removes skill directories for workflows when delivery changed to commands-only.
|
|
559
|
+
* Returns the number of directories removed.
|
|
560
|
+
*/
|
|
561
|
+
async removeSkillDirs(skillsRoot, skillsDir, toolId) {
|
|
562
|
+
let removed = 0;
|
|
563
|
+
for (const dirName of Object.values(WORKFLOW_TO_SKILL_DIR)) {
|
|
564
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
565
|
+
if (!this.view.exists(skillDir))
|
|
566
|
+
continue;
|
|
567
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
568
|
+
try {
|
|
569
|
+
await this.ops.removeDirectory(skillDir, toolId);
|
|
570
|
+
removed++;
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// Ignore errors
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return removed;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Removes skill directories for workflows that are no longer selected in the active profile.
|
|
580
|
+
* Returns the number of directories removed.
|
|
581
|
+
*/
|
|
582
|
+
async removeUnselectedSkillDirs(skillsRoot, skillsDir, desiredWorkflows, toolId) {
|
|
583
|
+
const desiredSet = new Set(desiredWorkflows);
|
|
584
|
+
let removed = 0;
|
|
585
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
586
|
+
if (desiredSet.has(workflow))
|
|
587
|
+
continue;
|
|
588
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflow];
|
|
589
|
+
if (!dirName)
|
|
590
|
+
continue;
|
|
591
|
+
const skillDir = path.join(skillsDir, dirName);
|
|
592
|
+
if (!this.view.exists(skillDir))
|
|
593
|
+
continue;
|
|
594
|
+
FileSystemUtils.assertPathWithin(skillsRoot, skillDir);
|
|
595
|
+
try {
|
|
596
|
+
await this.ops.removeDirectory(skillDir, toolId);
|
|
597
|
+
removed++;
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
// Ignore errors
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return removed;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Removes command files for workflows when delivery changed to skills-only.
|
|
607
|
+
* Returns the number of files removed.
|
|
608
|
+
*/
|
|
609
|
+
async removeCommandFiles(projectPath, toolId) {
|
|
610
|
+
let removed = 0;
|
|
611
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
612
|
+
if (!adapter)
|
|
613
|
+
return 0;
|
|
614
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
615
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
616
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
617
|
+
try {
|
|
618
|
+
if (this.view.exists(fullPath)) {
|
|
619
|
+
await this.ops.removeFile(fullPath, toolId);
|
|
620
|
+
removed++;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Ignore errors
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return removed;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Removes command files for workflows that are no longer selected in the active profile.
|
|
631
|
+
* Returns the number of files removed.
|
|
632
|
+
*/
|
|
633
|
+
async removeUnselectedCommandFiles(projectPath, toolId, desiredWorkflows) {
|
|
634
|
+
let removed = 0;
|
|
635
|
+
const adapter = CommandAdapterRegistry.get(toolId);
|
|
636
|
+
if (!adapter)
|
|
637
|
+
return 0;
|
|
638
|
+
const desiredSet = new Set(desiredWorkflows);
|
|
639
|
+
for (const workflow of ALL_WORKFLOWS) {
|
|
640
|
+
if (desiredSet.has(workflow))
|
|
641
|
+
continue;
|
|
642
|
+
const cmdPath = adapter.getFilePath(workflow);
|
|
643
|
+
const fullPath = FileSystemUtils.resolveProjectArtifactPath(projectPath, cmdPath);
|
|
644
|
+
try {
|
|
645
|
+
if (this.view.exists(fullPath)) {
|
|
646
|
+
await this.ops.removeFile(fullPath, toolId);
|
|
647
|
+
removed++;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// Ignore errors
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return removed;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Offers to move FreeSpec content out of a renamed tool's former directory
|
|
658
|
+
* when the old location might still be the live one — today, Windsurf's
|
|
659
|
+
* `.windsurf/` after the Devin Desktop rebrand.
|
|
660
|
+
*
|
|
661
|
+
* Interactive runs are asked, because nothing on disk distinguishes a user
|
|
662
|
+
* who took the rebrand from one still on a pre-rebrand Windsurf build that
|
|
663
|
+
* reads only `.windsurf/`. `--force` and non-interactive runs migrate, which
|
|
664
|
+
* is what an unattended upgrade wants.
|
|
665
|
+
*/
|
|
666
|
+
/** Surfaces files the move left behind rather than overwriting. */
|
|
667
|
+
reportKeptInPlace(migration) {
|
|
668
|
+
const notice = keptInPlaceNotice(migration);
|
|
669
|
+
if (notice)
|
|
670
|
+
this.log(chalk.dim(notice));
|
|
671
|
+
}
|
|
672
|
+
async offerConsentedLegacyMigrations(projectPath) {
|
|
673
|
+
const pending = findLegacyToolMigrations(projectPath).filter((m) => m.needsConsent);
|
|
674
|
+
const declined = [];
|
|
675
|
+
if (pending.length === 0)
|
|
676
|
+
return declined;
|
|
677
|
+
for (const migration of pending) {
|
|
678
|
+
// Nothing movable: every legacy file differs from its counterpart, so
|
|
679
|
+
// there is no move to offer. Still say so — silence would leave two
|
|
680
|
+
// divergent copies the user never hears about.
|
|
681
|
+
if (!hasMovableContent(migration)) {
|
|
682
|
+
this.reportKeptInPlace(migration);
|
|
683
|
+
this.log();
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
this.log(chalk.yellow(legacyMigrationNotice(migration)));
|
|
687
|
+
if (!this.force && isInteractive()) {
|
|
688
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
689
|
+
let shouldMigrate;
|
|
690
|
+
try {
|
|
691
|
+
shouldMigrate = await confirm({
|
|
692
|
+
message: `Move ${describeLegacyMigration(migration)} from ${migration.from}/ to ${migration.to}/?`,
|
|
693
|
+
default: true,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
// Closed stdin is not consent, and it must not abort the update.
|
|
698
|
+
shouldMigrate = false;
|
|
699
|
+
}
|
|
700
|
+
if (!shouldMigrate) {
|
|
701
|
+
// Say what declining costs. FreeSpec writes the current root now, so
|
|
702
|
+
// the files keep working where they are, but FreeSpec stops managing
|
|
703
|
+
// them — it no longer looks in the former directory.
|
|
704
|
+
this.log(chalk.dim(`Left in place. FreeSpec writes ${migration.to}/ now and will not manage ` +
|
|
705
|
+
`${migration.from}/, so those files stay as they are until you move them. ` +
|
|
706
|
+
`You will be asked again next run.`));
|
|
707
|
+
this.log();
|
|
708
|
+
declined.push(migration);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
for (const applied of migrateLegacyToolDirs(projectPath, [migration.toolId])) {
|
|
713
|
+
if (hasMovableContent(applied)) {
|
|
714
|
+
this.log(chalk.dim(`Migrated ${describeLegacyMigration(applied)}: ${applied.from} → ${applied.to}`));
|
|
715
|
+
}
|
|
716
|
+
this.reportKeptInPlace(applied);
|
|
717
|
+
}
|
|
718
|
+
this.log();
|
|
719
|
+
}
|
|
720
|
+
return declined;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
//# sourceMappingURL=update.js.map
|