@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,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Utilities
|
|
3
|
+
*
|
|
4
|
+
* One-time migration logic for existing projects when profile system is introduced.
|
|
5
|
+
* Called by both init and update commands before profile resolution.
|
|
6
|
+
*/
|
|
7
|
+
import { AI_TOOLS } from './config.js';
|
|
8
|
+
import { getGlobalConfig, getGlobalConfigPath, isGlobalConfigUnreadable, saveGlobalConfig } from './global-config.js';
|
|
9
|
+
import { CommandAdapterRegistry } from './command-generation/index.js';
|
|
10
|
+
import { resolveCommandInvocation, resolveCommandSurfaceCapability, shouldGenerateCommandsForTool, } from './command-surface.js';
|
|
11
|
+
import { WORKFLOW_TO_SKILL_DIR } from './profile-sync-drift.js';
|
|
12
|
+
import { COMMAND_IDS } from './shared/tool-detection.js';
|
|
13
|
+
import { ALL_WORKFLOWS } from './profiles.js';
|
|
14
|
+
import { getSkillReferenceTransformer, getTransformerForTool } from '../utils/command-references.js';
|
|
15
|
+
import { FileSystemUtils } from '../utils/file-system.js';
|
|
16
|
+
import { isSharedSkillTargetActive } from './shared-skill-target.js';
|
|
17
|
+
import { DISK_VIEW } from './file-view.js';
|
|
18
|
+
import { isLegacyCodexSkillEquivalentToCurrent } from './shared/skill-content-equivalence.js';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import { resolveToolSkillsDir, toolSupportsSkills } from './shared/skill-paths.js';
|
|
22
|
+
/**
|
|
23
|
+
* Former tool roots whose FreeSpec-managed content belongs under the tool's
|
|
24
|
+
* current skillsDir. User files are never touched.
|
|
25
|
+
*/
|
|
26
|
+
export const LEGACY_TOOL_ROOTS = {
|
|
27
|
+
// Kimi CLI became Kimi Code and moved from .kimi to .kimi-code.
|
|
28
|
+
kimi: [{ root: '.kimi', needsConsent: false }],
|
|
29
|
+
// Windsurf was rebranded to Devin Desktop on 2026-06-02 and its config
|
|
30
|
+
// directory moved to .devin/. Devin Desktop reads .windsurf/ only as a
|
|
31
|
+
// fallback and Devin Local does not read it at all, so moving is the right
|
|
32
|
+
// default — but a pre-rebrand Windsurf build reads ONLY .windsurf/, and
|
|
33
|
+
// nothing on disk tells that user apart, so the move is offered, not taken.
|
|
34
|
+
devin: [{ root: '.windsurf', needsConsent: true }],
|
|
35
|
+
// Codex now reads the canonical shared .agents root. Generate the current
|
|
36
|
+
// replacement first so a divergent legacy file is preserved, not overwritten.
|
|
37
|
+
codex: [{ root: '.codex', needsConsent: false, timing: 'after-generation' }],
|
|
38
|
+
// Antigravity v1.20.5 moved workspace skills and workflows to `.agents` and
|
|
39
|
+
// reads the old `.agent` root only as a fallback, so leaving a copy there
|
|
40
|
+
// just gives the agent two of everything. Same after-generation timing as
|
|
41
|
+
// Codex: the replacement is written first, so a divergent legacy file is
|
|
42
|
+
// reported and kept rather than overwritten.
|
|
43
|
+
antigravity: [{ root: '.agent', needsConsent: false, timing: 'after-generation' }],
|
|
44
|
+
};
|
|
45
|
+
function classifyManagedFile(source, destination, view = DISK_VIEW) {
|
|
46
|
+
if (isSamePath(source, destination))
|
|
47
|
+
return 'skip';
|
|
48
|
+
if (!view.exists(destination))
|
|
49
|
+
return 'move';
|
|
50
|
+
try {
|
|
51
|
+
const sourceContent = fs.readFileSync(source, 'utf-8');
|
|
52
|
+
const destinationContent = view.read(destination);
|
|
53
|
+
const equivalentGeneratedSkills = path.basename(source) === 'SKILL.md' &&
|
|
54
|
+
path.basename(destination) === 'SKILL.md' &&
|
|
55
|
+
isLegacyCodexSkillEquivalentToCurrent(sourceContent, destinationContent);
|
|
56
|
+
return sourceContent === destinationContent || equivalentGeneratedSkills
|
|
57
|
+
? 'drop'
|
|
58
|
+
: 'keep';
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 'keep';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Rewrites a generated command path from the tool's current root to a legacy
|
|
66
|
+
* one, so `.devin/workflows/freespec-apply.md` locates its `.windsurf/` twin
|
|
67
|
+
* without the migration hard-coding either layout.
|
|
68
|
+
*
|
|
69
|
+
* Returns undefined for adapters whose paths are absolute (global-scoped
|
|
70
|
+
* command files) or do not start at the tool root — neither can be relocated
|
|
71
|
+
* by swapping a leading segment.
|
|
72
|
+
*/
|
|
73
|
+
function legacyCommandPath(commandPath, currentRoot, legacyRoot) {
|
|
74
|
+
if (path.isAbsolute(commandPath))
|
|
75
|
+
return undefined;
|
|
76
|
+
const segments = commandPath.split(/[\\/]/);
|
|
77
|
+
if (segments[0] !== currentRoot)
|
|
78
|
+
return undefined;
|
|
79
|
+
segments[0] = legacyRoot;
|
|
80
|
+
return path.join(...segments);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Reports the FreeSpec content sitting under each tool's legacy root, without
|
|
84
|
+
* moving anything. Callers use this to ask before a move that needs consent.
|
|
85
|
+
*/
|
|
86
|
+
export function findLegacyToolMigrations(projectPath, timing = 'before-generation') {
|
|
87
|
+
return collectLegacyToolMigrations(projectPath, false, undefined, timing);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Lists, file by file, what `migrateLegacyToolDirs` would move or drop, without
|
|
91
|
+
* touching anything. Consent-gated roots are included: a non-interactive run
|
|
92
|
+
* moves them without asking. `view` decides what already sits at each
|
|
93
|
+
* destination, so a dry run can plan against files it has not written.
|
|
94
|
+
*/
|
|
95
|
+
export function planLegacyToolMigrations(projectPath, timing, toolIds, view) {
|
|
96
|
+
const ops = [];
|
|
97
|
+
collectLegacyToolMigrations(projectPath, false, toolIds, timing, { view, ops });
|
|
98
|
+
return ops;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Moves FreeSpec-managed skill directories (freespec-*) and command files
|
|
102
|
+
* (freespec-*) from a tool's legacy root to its current one. When the destination
|
|
103
|
+
* already exists the legacy copy is removed instead. Legacy directories are
|
|
104
|
+
* deleted only when left empty, so user files under the old location — a
|
|
105
|
+
* hand-written Cascade workflow next to the generated ones — are preserved.
|
|
106
|
+
*
|
|
107
|
+
* @param projectPath - Project root
|
|
108
|
+
* @param toolIds - Restrict the move to these tools; omit to move every tool
|
|
109
|
+
* whose legacy root needs no consent
|
|
110
|
+
*/
|
|
111
|
+
export function migrateLegacyToolDirs(projectPath, toolIds, timing = 'before-generation') {
|
|
112
|
+
return collectLegacyToolMigrations(projectPath, true, toolIds, timing);
|
|
113
|
+
}
|
|
114
|
+
function collectLegacyToolMigrations(projectPath, apply, toolIds, timing = 'before-generation', plan) {
|
|
115
|
+
const migrations = [];
|
|
116
|
+
for (const tool of AI_TOOLS) {
|
|
117
|
+
if (!tool.skillsDir)
|
|
118
|
+
continue;
|
|
119
|
+
if (toolIds && !toolIds.includes(tool.value))
|
|
120
|
+
continue;
|
|
121
|
+
for (const legacy of LEGACY_TOOL_ROOTS[tool.value] ?? []) {
|
|
122
|
+
const legacyTiming = legacy.timing ?? 'before-generation';
|
|
123
|
+
if (legacyTiming !== timing)
|
|
124
|
+
continue;
|
|
125
|
+
if (legacy.root === tool.skillsDir)
|
|
126
|
+
continue;
|
|
127
|
+
// Without an explicit tool list, only moves that need no consent run.
|
|
128
|
+
if (apply && !toolIds && legacy.needsConsent)
|
|
129
|
+
continue;
|
|
130
|
+
const legacyRootPath = path.join(projectPath, legacy.root);
|
|
131
|
+
if (!fs.existsSync(legacyRootPath))
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, legacyRootPath);
|
|
135
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, path.join(projectPath, tool.skillsDir));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
console.warn(`Skipping legacy ${legacy.root}/ migration because the directory resolves outside this project.`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const sink = plan
|
|
142
|
+
? { ...plan, toolId: tool.value, needsConsent: legacy.needsConsent }
|
|
143
|
+
: undefined;
|
|
144
|
+
const skills = migrateSkillDirs(projectPath, tool.skillsDir, legacy.root, apply, legacyTiming === 'after-generation', sink);
|
|
145
|
+
const commands = migrateCommandFiles(projectPath, tool, legacy.root, apply, legacyTiming === 'after-generation', sink);
|
|
146
|
+
if (apply) {
|
|
147
|
+
removeDirIfEmpty(path.join(legacyRootPath, 'skills'));
|
|
148
|
+
removeDirIfEmpty(path.join(legacyRootPath, 'workflows'));
|
|
149
|
+
removeDirIfEmpty(legacyRootPath);
|
|
150
|
+
}
|
|
151
|
+
// Kept-only results are retained deliberately. When every legacy file
|
|
152
|
+
// differs from its counterpart nothing is movable, and dropping the
|
|
153
|
+
// record here would leave the user with two divergent copies and no
|
|
154
|
+
// word of it.
|
|
155
|
+
if (skills.moved > 0 || commands.moved > 0 || skills.kept > 0 || commands.kept > 0) {
|
|
156
|
+
migrations.push({
|
|
157
|
+
toolId: tool.value,
|
|
158
|
+
from: legacy.root,
|
|
159
|
+
to: tool.skillsDir,
|
|
160
|
+
skillDirs: skills.moved,
|
|
161
|
+
commandFiles: commands.moved,
|
|
162
|
+
keptInPlace: skills.kept + commands.kept,
|
|
163
|
+
needsConsent: legacy.needsConsent,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return migrations;
|
|
169
|
+
}
|
|
170
|
+
function migrateSkillDirs(projectPath, currentRoot, legacyRoot, apply, requireDestination = false, sink) {
|
|
171
|
+
const view = sink?.view ?? DISK_VIEW;
|
|
172
|
+
const legacySkillsDir = path.join(projectPath, legacyRoot, 'skills');
|
|
173
|
+
if (!fs.existsSync(legacySkillsDir))
|
|
174
|
+
return { moved: 0, kept: 0 };
|
|
175
|
+
const currentSkillsDir = path.join(projectPath, currentRoot, 'skills');
|
|
176
|
+
let moved = 0;
|
|
177
|
+
let kept = 0;
|
|
178
|
+
for (const workflowId of ALL_WORKFLOWS) {
|
|
179
|
+
const dirName = WORKFLOW_TO_SKILL_DIR[workflowId];
|
|
180
|
+
const source = path.join(legacySkillsDir, dirName);
|
|
181
|
+
const sourceSkill = path.join(source, 'SKILL.md');
|
|
182
|
+
if (!fs.existsSync(sourceSkill))
|
|
183
|
+
continue;
|
|
184
|
+
const destination = path.join(currentSkillsDir, dirName);
|
|
185
|
+
const destinationSkill = path.join(destination, 'SKILL.md');
|
|
186
|
+
if (requireDestination && !view.exists(destinationSkill))
|
|
187
|
+
continue;
|
|
188
|
+
if (!areProjectArtifacts(projectPath, sourceSkill, destinationSkill)) {
|
|
189
|
+
console.warn(`Skipping legacy ${legacyRoot}/skills/${dirName} migration because it resolves outside this project.`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const disposition = classifyManagedFile(sourceSkill, destinationSkill, view);
|
|
193
|
+
if (disposition === 'skip')
|
|
194
|
+
continue;
|
|
195
|
+
if (disposition === 'keep') {
|
|
196
|
+
kept++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!apply) {
|
|
200
|
+
sink?.ops.push({
|
|
201
|
+
toolId: sink.toolId,
|
|
202
|
+
source: sourceSkill,
|
|
203
|
+
destination: destinationSkill,
|
|
204
|
+
disposition,
|
|
205
|
+
needsConsent: sink.needsConsent,
|
|
206
|
+
});
|
|
207
|
+
moved++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
// Move the generated file, never the directory around it. A skill
|
|
212
|
+
// directory can also hold files the user wrote, and this destination is
|
|
213
|
+
// one FreeSpec deletes on its own — commands-only delivery and a
|
|
214
|
+
// deselected workflow both remove the whole skill directory. Carrying a
|
|
215
|
+
// user's file across would be handing it to that later removal.
|
|
216
|
+
if (disposition === 'drop') {
|
|
217
|
+
fs.rmSync(sourceSkill, { force: true });
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
221
|
+
fs.renameSync(sourceSkill, destinationSkill);
|
|
222
|
+
}
|
|
223
|
+
// Anything the user left beside it stays under the legacy root.
|
|
224
|
+
removeDirIfEmpty(source);
|
|
225
|
+
moved++;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Leave the legacy directory in place if it cannot be moved
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return { moved, kept };
|
|
232
|
+
}
|
|
233
|
+
function migrateCommandFiles(projectPath, tool, legacyRoot, apply, requireDestination = false, sink) {
|
|
234
|
+
const view = sink?.view ?? DISK_VIEW;
|
|
235
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
236
|
+
if (!adapter || !tool.skillsDir)
|
|
237
|
+
return { moved: 0, kept: 0 };
|
|
238
|
+
let moved = 0;
|
|
239
|
+
let kept = 0;
|
|
240
|
+
for (const commandId of COMMAND_IDS) {
|
|
241
|
+
const currentPath = adapter.getFilePath(commandId);
|
|
242
|
+
const legacyPath = legacyCommandPath(currentPath, tool.skillsDir, legacyRoot);
|
|
243
|
+
if (!legacyPath)
|
|
244
|
+
continue;
|
|
245
|
+
const source = FileSystemUtils.resolveProjectArtifactPath(projectPath, legacyPath);
|
|
246
|
+
if (!fs.existsSync(source))
|
|
247
|
+
continue;
|
|
248
|
+
const destination = FileSystemUtils.resolveProjectArtifactPath(projectPath, currentPath.split(/[\\/]/).join(path.sep));
|
|
249
|
+
// An after-generation move runs once the tool has written its replacement.
|
|
250
|
+
// No replacement means this command is not one FreeSpec installs now — a
|
|
251
|
+
// skills-only delivery or a deselected workflow — so relocating the legacy
|
|
252
|
+
// file would resurrect it under the current root.
|
|
253
|
+
if (requireDestination && !view.exists(destination))
|
|
254
|
+
continue;
|
|
255
|
+
if (!areProjectArtifacts(projectPath, source, destination)) {
|
|
256
|
+
console.warn(`Skipping legacy ${legacyPath} migration because it resolves outside this project.`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const disposition = classifyManagedFile(source, destination, view);
|
|
260
|
+
if (disposition === 'skip')
|
|
261
|
+
continue;
|
|
262
|
+
if (disposition === 'keep') {
|
|
263
|
+
kept++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!apply) {
|
|
267
|
+
sink?.ops.push({
|
|
268
|
+
toolId: sink.toolId,
|
|
269
|
+
source,
|
|
270
|
+
destination,
|
|
271
|
+
disposition,
|
|
272
|
+
needsConsent: sink.needsConsent,
|
|
273
|
+
});
|
|
274
|
+
moved++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
if (disposition === 'drop') {
|
|
279
|
+
fs.rmSync(source, { force: true });
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
283
|
+
fs.renameSync(source, destination);
|
|
284
|
+
}
|
|
285
|
+
moved++;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// Leave the legacy file in place if it cannot be moved
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { moved, kept };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Summarizes what a migration moved, e.g. "6 skills and 6 commands".
|
|
295
|
+
*/
|
|
296
|
+
export function describeLegacyMigration(migration) {
|
|
297
|
+
const parts = [];
|
|
298
|
+
if (migration.skillDirs > 0) {
|
|
299
|
+
parts.push(`${migration.skillDirs} skill${migration.skillDirs === 1 ? '' : 's'}`);
|
|
300
|
+
}
|
|
301
|
+
if (migration.commandFiles > 0) {
|
|
302
|
+
parts.push(`${migration.commandFiles} command${migration.commandFiles === 1 ? '' : 's'}`);
|
|
303
|
+
}
|
|
304
|
+
return parts.join(' and ');
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Names FreeSpec-managed files the move deliberately left behind, so a user
|
|
308
|
+
* who customized one knows there are now two copies to reconcile.
|
|
309
|
+
*/
|
|
310
|
+
export function keptInPlaceNotice(migration) {
|
|
311
|
+
if (migration.keptInPlace === 0)
|
|
312
|
+
return undefined;
|
|
313
|
+
const n = migration.keptInPlace;
|
|
314
|
+
// Deliberately does not claim the difference came from an edit: an older
|
|
315
|
+
// FreeSpec version's output differs too. Either way nothing was overwritten,
|
|
316
|
+
// and the user is the one who decides which copy to keep.
|
|
317
|
+
return (`Left ${n} file${n === 1 ? '' : 's'} in ${migration.from}/ that ` +
|
|
318
|
+
`differ${n === 1 ? 's' : ''} from the copy in ${migration.to}/. Nothing was ` +
|
|
319
|
+
`overwritten — compare the two and delete the ${migration.from}/ copy once ` +
|
|
320
|
+
`you have kept anything you customized.`);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Whether a migration has anything to move, as opposed to only files left in
|
|
324
|
+
* place. Callers use this to avoid offering a move of nothing.
|
|
325
|
+
*/
|
|
326
|
+
export function hasMovableContent(migration) {
|
|
327
|
+
return migration.skillDirs > 0 || migration.commandFiles > 0;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Explains why a consent-gated move is being offered, in the user's terms.
|
|
331
|
+
* Keyed by tool so the reason is specific rather than a generic "files moved".
|
|
332
|
+
*/
|
|
333
|
+
export function legacyMigrationNotice(migration) {
|
|
334
|
+
if (migration.toolId === 'devin') {
|
|
335
|
+
return (`Windsurf is now Devin Desktop, and its config directory moved from ` +
|
|
336
|
+
`${migration.from}/ to ${migration.to}/. Devin Desktop reads ${migration.from}/ ` +
|
|
337
|
+
`only as a fallback, and Devin Local does not read it at all.`);
|
|
338
|
+
}
|
|
339
|
+
return `${migration.from}/ is the former location for this tool; ${migration.to}/ is current.`;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Whether two paths are the same file on disk once symlinks are resolved.
|
|
343
|
+
*
|
|
344
|
+
* Symlinking one tool root at the other is a realistic way to straddle a
|
|
345
|
+
* rebrand (`ln -s .devin .windsurf` to keep an older build working). Without
|
|
346
|
+
* this check the "destination already exists, drop the legacy copy" branch
|
|
347
|
+
* deletes the destination itself, taking the only copy with it.
|
|
348
|
+
*/
|
|
349
|
+
function isSamePath(a, b) {
|
|
350
|
+
try {
|
|
351
|
+
return fs.realpathSync(a) === fs.realpathSync(b);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function areProjectArtifacts(projectPath, ...artifactPaths) {
|
|
358
|
+
try {
|
|
359
|
+
for (const artifactPath of artifactPaths) {
|
|
360
|
+
FileSystemUtils.assertProjectArtifactPath(projectPath, artifactPath);
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function removeDirIfEmpty(dirPath) {
|
|
369
|
+
try {
|
|
370
|
+
if (fs.readdirSync(dirPath).length === 0) {
|
|
371
|
+
fs.rmdirSync(dirPath);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Missing or non-empty directory — nothing to do
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function scanInstalledWorkflowArtifacts(projectPath, tools, includeLegacyRoots = false, view = DISK_VIEW) {
|
|
379
|
+
const installed = new Set();
|
|
380
|
+
let hasSkills = false;
|
|
381
|
+
let hasCommands = false;
|
|
382
|
+
for (const tool of tools) {
|
|
383
|
+
if (!toolSupportsSkills(tool))
|
|
384
|
+
continue;
|
|
385
|
+
const skillsDirs = [];
|
|
386
|
+
if (tool.globalSkillsDir) {
|
|
387
|
+
skillsDirs.push(resolveToolSkillsDir(projectPath, tool));
|
|
388
|
+
}
|
|
389
|
+
else if (isSharedSkillTargetActive(projectPath, tool.value, view)) {
|
|
390
|
+
skillsDirs.push(resolveToolSkillsDir(projectPath, tool));
|
|
391
|
+
if (includeLegacyRoots) {
|
|
392
|
+
skillsDirs.push(...(tool.legacySkillsDirs ?? []).map((root) => path.join(projectPath, root, 'skills')));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (const skillsDir of skillsDirs) {
|
|
396
|
+
for (const workflowId of ALL_WORKFLOWS) {
|
|
397
|
+
const skillDirName = WORKFLOW_TO_SKILL_DIR[workflowId];
|
|
398
|
+
const skillFile = path.join(skillsDir, skillDirName, 'SKILL.md');
|
|
399
|
+
if (view.exists(skillFile)) {
|
|
400
|
+
installed.add(workflowId);
|
|
401
|
+
hasSkills = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const adapter = CommandAdapterRegistry.get(tool.value);
|
|
406
|
+
if (!adapter)
|
|
407
|
+
continue;
|
|
408
|
+
// A root the tool has moved away from still holds the command files the
|
|
409
|
+
// user installed there. Reading only the current root would report a
|
|
410
|
+
// commands install as skills-only, and delivery inferred from that answer
|
|
411
|
+
// deletes those commands on the next update.
|
|
412
|
+
const legacyRoots = includeLegacyRoots
|
|
413
|
+
? (LEGACY_TOOL_ROOTS[tool.value] ?? []).map((legacy) => legacy.root)
|
|
414
|
+
: [];
|
|
415
|
+
for (const workflowId of ALL_WORKFLOWS) {
|
|
416
|
+
const commandPath = adapter.getFilePath(workflowId);
|
|
417
|
+
const candidates = [commandPath];
|
|
418
|
+
if (tool.skillsDir) {
|
|
419
|
+
for (const root of legacyRoots) {
|
|
420
|
+
const legacyPath = legacyCommandPath(commandPath, tool.skillsDir, root);
|
|
421
|
+
if (legacyPath)
|
|
422
|
+
candidates.push(legacyPath);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
for (const candidate of candidates) {
|
|
426
|
+
const fullPath = path.isAbsolute(candidate)
|
|
427
|
+
? candidate
|
|
428
|
+
: path.join(projectPath, candidate);
|
|
429
|
+
if (view.exists(fullPath)) {
|
|
430
|
+
installed.add(workflowId);
|
|
431
|
+
hasCommands = true;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
workflows: ALL_WORKFLOWS.filter((workflowId) => installed.has(workflowId)),
|
|
439
|
+
hasSkills,
|
|
440
|
+
hasCommands,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Scans installed workflow files across all detected tools and returns
|
|
445
|
+
* the union of installed workflow IDs.
|
|
446
|
+
*/
|
|
447
|
+
export function scanInstalledWorkflows(projectPath, tools, view = DISK_VIEW) {
|
|
448
|
+
return scanInstalledWorkflowArtifacts(projectPath, tools, false, view).workflows;
|
|
449
|
+
}
|
|
450
|
+
function inferDelivery(artifacts) {
|
|
451
|
+
if (artifacts.hasSkills && artifacts.hasCommands) {
|
|
452
|
+
return 'both';
|
|
453
|
+
}
|
|
454
|
+
if (artifacts.hasCommands) {
|
|
455
|
+
return 'commands';
|
|
456
|
+
}
|
|
457
|
+
return 'skills';
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Performs one-time migration if the global config does not yet have a profile field.
|
|
461
|
+
* Called by both init and update before profile resolution.
|
|
462
|
+
*
|
|
463
|
+
* - If no profile field exists and workflows are installed: sets profile to 'custom'
|
|
464
|
+
* with the detected workflows, preserving the user's existing setup.
|
|
465
|
+
* - If no profile field exists and no workflows are installed: no-op (defaults apply).
|
|
466
|
+
* - If profile field already exists: no-op.
|
|
467
|
+
*/
|
|
468
|
+
export function computeProfileMigration(projectPath, tools, view = DISK_VIEW) {
|
|
469
|
+
// A config that cannot be parsed, or is not a JSON object, is never saved
|
|
470
|
+
// over; skip migration rather than fail init or update on it.
|
|
471
|
+
if (isGlobalConfigUnreadable()) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
// Check raw config file for profile field presence
|
|
475
|
+
const configPath = getGlobalConfigPath();
|
|
476
|
+
let rawConfig = {};
|
|
477
|
+
try {
|
|
478
|
+
if (fs.existsSync(configPath)) {
|
|
479
|
+
rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return null; // Can't read config, skip migration
|
|
484
|
+
}
|
|
485
|
+
// If profile is already explicitly set, no migration needed
|
|
486
|
+
if (rawConfig.profile !== undefined) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
// Scan for installed workflows
|
|
490
|
+
const artifacts = scanInstalledWorkflowArtifacts(projectPath, tools, true, view);
|
|
491
|
+
const installedWorkflows = artifacts.workflows;
|
|
492
|
+
if (installedWorkflows.length === 0) {
|
|
493
|
+
// No workflows installed, new user — defaults will apply
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
profile: 'custom',
|
|
498
|
+
workflows: installedWorkflows,
|
|
499
|
+
...(rawConfig.delivery === undefined ? { delivery: inferDelivery(artifacts) } : {}),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** The global config the one-time profile migration saves. */
|
|
503
|
+
export function applyProfileMigration(migration) {
|
|
504
|
+
const config = getGlobalConfig();
|
|
505
|
+
// Migrate: set profile to custom with detected workflows
|
|
506
|
+
config.profile = migration.profile;
|
|
507
|
+
config.workflows = migration.workflows;
|
|
508
|
+
if (migration.delivery !== undefined) {
|
|
509
|
+
config.delivery = migration.delivery;
|
|
510
|
+
}
|
|
511
|
+
return config;
|
|
512
|
+
}
|
|
513
|
+
export function migrateIfNeeded(projectPath, tools) {
|
|
514
|
+
const migration = computeProfileMigration(projectPath, tools);
|
|
515
|
+
if (!migration) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const config = applyProfileMigration(migration);
|
|
519
|
+
const installedWorkflows = migration.workflows;
|
|
520
|
+
saveGlobalConfig(config);
|
|
521
|
+
console.log(`Migrated: custom profile with ${installedWorkflows.length} workflows`);
|
|
522
|
+
// Each detected tool resolves to a propose reference for its surface: the
|
|
523
|
+
// command name its generated files answer to when commands will exist for it
|
|
524
|
+
// under the effective delivery (/freespec:propose when namespaced under freespec/,
|
|
525
|
+
// /freespec-propose when the filename is the command), its documented skill
|
|
526
|
+
// invocation otherwise. When the tools disagree — including command tools
|
|
527
|
+
// mixed with skill-only tools — stay syntax-neutral rather than advertise a
|
|
528
|
+
// form that is wrong for one of them.
|
|
529
|
+
const effectiveDelivery = config.delivery ?? 'both';
|
|
530
|
+
const proposeReferences = new Set(tools.map((tool) => {
|
|
531
|
+
if (shouldGenerateCommandsForTool(tool.value, effectiveDelivery)) {
|
|
532
|
+
const transformer = getTransformerForTool(tool.value, effectiveDelivery, resolveCommandSurfaceCapability(tool.value), resolveCommandInvocation(tool.value));
|
|
533
|
+
return transformer ? transformer('/freespec:propose') : '/freespec:propose';
|
|
534
|
+
}
|
|
535
|
+
return getSkillReferenceTransformer(tool.value)('/freespec:propose');
|
|
536
|
+
}));
|
|
537
|
+
const proposeReference = proposeReferences.size === 1 ? [...proposeReferences][0] : 'the freespec-propose skill';
|
|
538
|
+
console.log(`New in this version: ${proposeReference}. Try 'freespec config profile core' for the streamlined experience.`);
|
|
539
|
+
}
|
|
540
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding command hints.
|
|
3
|
+
*
|
|
4
|
+
* The commands shown to a user after setup must be limited to the workflows
|
|
5
|
+
* their profile actually installs, otherwise we advertise slash commands that
|
|
6
|
+
* were correctly never generated.
|
|
7
|
+
*
|
|
8
|
+
* This module decides WHICH hints to show. How each one is spelled for a given
|
|
9
|
+
* tool — command, skill, or a tool-specific skill prefix — is decided by
|
|
10
|
+
* src/utils/command-references.ts at the call site.
|
|
11
|
+
*/
|
|
12
|
+
import type { WorkflowId } from './profiles.js';
|
|
13
|
+
export type OnboardingCommand = {
|
|
14
|
+
workflow: WorkflowId;
|
|
15
|
+
command: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Longest description the welcome screen can render. It shows these beside a
|
|
20
|
+
* 24-column art column and only animates at MIN_WIDTH (60) columns or wider; a
|
|
21
|
+
* longer line wraps, and the animation's cursor-up count assumes unwrapped
|
|
22
|
+
* lines. See src/ui/welcome-screen.ts.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DESCRIPTION_BUDGET = 17;
|
|
25
|
+
/**
|
|
26
|
+
* Returns the onboarding hints for the installed workflows, in lifecycle order.
|
|
27
|
+
* Returns an empty array when none of the onboarding workflows are installed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getOnboardingCommands(workflows: readonly string[]): OnboardingCommand[];
|
|
30
|
+
//# sourceMappingURL=onboarding-commands.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding command hints.
|
|
3
|
+
*
|
|
4
|
+
* The commands shown to a user after setup must be limited to the workflows
|
|
5
|
+
* their profile actually installs, otherwise we advertise slash commands that
|
|
6
|
+
* were correctly never generated.
|
|
7
|
+
*
|
|
8
|
+
* This module decides WHICH hints to show. How each one is spelled for a given
|
|
9
|
+
* tool — command, skill, or a tool-specific skill prefix — is decided by
|
|
10
|
+
* src/utils/command-references.ts at the call site.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Longest description the welcome screen can render. It shows these beside a
|
|
14
|
+
* 24-column art column and only animates at MIN_WIDTH (60) columns or wider; a
|
|
15
|
+
* longer line wraps, and the animation's cursor-up count assumes unwrapped
|
|
16
|
+
* lines. See src/ui/welcome-screen.ts.
|
|
17
|
+
*/
|
|
18
|
+
export const DESCRIPTION_BUDGET = 17;
|
|
19
|
+
/**
|
|
20
|
+
* Ordered onboarding hints. Each entry is shown only when its workflow is
|
|
21
|
+
* installed, so the list follows the change lifecycle: start, then build,
|
|
22
|
+
* then implement.
|
|
23
|
+
*/
|
|
24
|
+
const ONBOARDING_COMMANDS = [
|
|
25
|
+
{ workflow: '新建', command: '/freespec:新建', description: '建变更目录' },
|
|
26
|
+
{ workflow: '深思', command: '/freespec:深思', description: '把问题想透并写全答案' },
|
|
27
|
+
{ workflow: '需求', command: '/freespec:需求', description: '写需求文档' },
|
|
28
|
+
{ workflow: '写Java', command: '/freespec:写Java', description: '用 Java 实现' },
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Returns the onboarding hints for the installed workflows, in lifecycle order.
|
|
32
|
+
* Returns an empty array when none of the onboarding workflows are installed.
|
|
33
|
+
*/
|
|
34
|
+
export function getOnboardingCommands(workflows) {
|
|
35
|
+
const installed = new Set(workflows);
|
|
36
|
+
return ONBOARDING_COMMANDS.filter((entry) => installed.has(entry.workflow));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=onboarding-commands.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DEFAULT_SCHEMA as DEFAULT_OPENSPEC_SCHEMA } from './change-layout.js';
|
|
2
|
+
import { type StoreDiagnostic } from './store/errors.js';
|
|
3
|
+
export declare const OPENSPEC_ROOT_DIR = "freespec";
|
|
4
|
+
export declare const OPENSPEC_CONFIG_YAML = "freespec/config.yaml";
|
|
5
|
+
export declare const OPENSPEC_CONFIG_YML = "freespec/config.yml";
|
|
6
|
+
export declare const OPENSPEC_SPECS_DIR = "freespec/specs";
|
|
7
|
+
/** Change folders sit directly under `freespec/`, named `<名称>变更`. */
|
|
8
|
+
export declare const OPENSPEC_CHANGES_DIR = "freespec";
|
|
9
|
+
export { DEFAULT_OPENSPEC_SCHEMA };
|
|
10
|
+
export declare const DIRECTORY_ANCHOR_FILE_NAME = ".gitkeep";
|
|
11
|
+
export declare const ANCHORED_OPENSPEC_DIRS: readonly [];
|
|
12
|
+
export interface CreatedPathLedgerEntry {
|
|
13
|
+
relativePath: string;
|
|
14
|
+
absolutePath: string;
|
|
15
|
+
kind: 'directory' | 'file';
|
|
16
|
+
}
|
|
17
|
+
export interface FreeSpecRootInspection {
|
|
18
|
+
present: boolean | null;
|
|
19
|
+
config: {
|
|
20
|
+
present: boolean | null;
|
|
21
|
+
path?: string;
|
|
22
|
+
};
|
|
23
|
+
specs: {
|
|
24
|
+
present: boolean | null;
|
|
25
|
+
};
|
|
26
|
+
changes: {
|
|
27
|
+
present: boolean | null;
|
|
28
|
+
};
|
|
29
|
+
archive: {
|
|
30
|
+
present: boolean | null;
|
|
31
|
+
};
|
|
32
|
+
healthy: boolean;
|
|
33
|
+
diagnostics: StoreDiagnostic[];
|
|
34
|
+
}
|
|
35
|
+
export interface EnsureFreeSpecRootResult {
|
|
36
|
+
inspection: FreeSpecRootInspection;
|
|
37
|
+
createdArtifacts: string[];
|
|
38
|
+
createdPaths: CreatedPathLedgerEntry[];
|
|
39
|
+
}
|
|
40
|
+
export declare function inspectFreeSpecRoot(storeRoot: string): Promise<FreeSpecRootInspection>;
|
|
41
|
+
export declare function ensureDirectoryAnchor(storeRoot: string, relativeDir: string, ledger?: CreatedPathLedgerEntry[]): Promise<void>;
|
|
42
|
+
export interface EnsureFreeSpecRootOptions {
|
|
43
|
+
anchorEmptyDirectories?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function ensureFreeSpecRoot(storeRoot: string, options?: EnsureFreeSpecRootOptions): Promise<EnsureFreeSpecRootResult>;
|
|
46
|
+
export declare function rollbackCreatedPaths(entries: CreatedPathLedgerEntry[]): Promise<void>;
|
|
47
|
+
//# sourceMappingURL=openspec-root.d.ts.map
|