@opengsd/gsd-core 1.6.1 → 1.7.0-rc.2
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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/agents/gsd-advisor-researcher.md +2 -0
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +2 -0
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debugger.md +2 -0
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-eval-auditor.md +2 -0
- package/agents/gsd-executor.md +9 -6
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +2 -0
- package/agents/gsd-plan-checker.md +2 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-project-researcher.md +2 -0
- package/agents/gsd-research-synthesizer.md +2 -0
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +2 -0
- package/agents/gsd-verifier.md +5 -2
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +411 -1146
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +134 -8
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +45 -9
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +891 -82
- package/gsd-core/bin/lib/capability-source.cjs +26 -11
- package/gsd-core/bin/lib/capability-validator.cjs +222 -2
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +469 -0
- package/gsd-core/bin/lib/init.cjs +35 -7
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +68 -40
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +57 -90
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +132 -2
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -22
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +16 -0
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1603 -0
- package/gsd-core/bin/lib/state.cjs +327 -683
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +30 -32
- package/gsd-core/workflows/complete-milestone.md +6 -10
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/forensics.md +3 -3
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/milestone-summary.md +3 -3
- package/gsd-core/workflows/new-milestone.md +6 -0
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +4 -159
- package/gsd-core/workflows/review.md +33 -2
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/gsd-core/workflows/verify-work.md +1 -2
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +6 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/run-tests.cjs +51 -1
- package/scripts/sync-manifest-versions.cjs +66 -14
- package/skills/gsd-review/SKILL.md +6 -0
- package/scripts/lint-windows-test-portability.cjs +0 -178
package/bin/install.js
CHANGED
|
@@ -37,7 +37,7 @@ const {
|
|
|
37
37
|
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
38
38
|
// conversion module's rewrite engine can consume it without importing
|
|
39
39
|
// bin/install.js. Re-exported below for back-compat consumers/tests.
|
|
40
|
-
const { getDirName } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
40
|
+
const { getDirName, getRuntimeLabel, getGlobalConfigHomeFragment, runtimeFlags, getRuntimeNewProjectCommand } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
41
41
|
const {
|
|
42
42
|
applyWorktreeBaseRef,
|
|
43
43
|
readBaseRefFromSettings,
|
|
@@ -364,6 +364,7 @@ const {
|
|
|
364
364
|
resolveRuntimeArtifactLayout,
|
|
365
365
|
} = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
|
|
366
366
|
const {
|
|
367
|
+
assertDestWithinConfigHome,
|
|
367
368
|
createRuntimeArtifactInstallPlan,
|
|
368
369
|
createRuntimeArtifactUninstallPlan,
|
|
369
370
|
} = require(path.join(_gsdLibDir, 'runtime-artifact-install-plan.cjs'));
|
|
@@ -373,8 +374,34 @@ const {
|
|
|
373
374
|
} = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'legacy-cleanup.cjs'));
|
|
374
375
|
const {
|
|
375
376
|
updateCacheFileName,
|
|
377
|
+
PACKAGE_NAME,
|
|
376
378
|
} = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'package-identity.cjs'));
|
|
377
379
|
|
|
380
|
+
// ADR-1239 Phase B: runtime-artifact install cluster extracted to install-engine.cjs.
|
|
381
|
+
// getCommitAttribution STAYS here (impure install-time config I/O); it is injected
|
|
382
|
+
// into the engine functions via the resolveAttribution parameter at each call site.
|
|
383
|
+
const installEngine = require(path.join(_gsdLibDir, 'install-engine.cjs'));
|
|
384
|
+
const {
|
|
385
|
+
installRuntimeArtifacts,
|
|
386
|
+
uninstallRuntimeArtifacts,
|
|
387
|
+
installOpencodeFamilySkills,
|
|
388
|
+
_copyStaged,
|
|
389
|
+
hasExistingSymlinkBetween,
|
|
390
|
+
preserveUserArtifacts,
|
|
391
|
+
restoreUserArtifacts,
|
|
392
|
+
migrateLegacyDevPreferencesToSkill,
|
|
393
|
+
applyOpencodeFamilyPathPrefix,
|
|
394
|
+
convertClaudeCommandToOpencodeSkill,
|
|
395
|
+
convertClaudeCommandToKiloSkill,
|
|
396
|
+
USER_OWNED_ARTIFACTS,
|
|
397
|
+
_runLegacyInstallMigrations,
|
|
398
|
+
_runLegacyUninstallCleanup,
|
|
399
|
+
_removeGsdEntries,
|
|
400
|
+
_snapshotDir,
|
|
401
|
+
_restoreDir,
|
|
402
|
+
_removeHermesBareStemDirs,
|
|
403
|
+
} = installEngine;
|
|
404
|
+
|
|
378
405
|
// Parse args
|
|
379
406
|
const args = process.argv.slice(2);
|
|
380
407
|
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
@@ -484,18 +511,11 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
|
|
484
511
|
// Local installs use the same dir name pattern
|
|
485
512
|
return `'${getDirName(runtime)}'`;
|
|
486
513
|
}
|
|
487
|
-
// Global installs
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
// Return as comma-separated for path.join() replacement
|
|
492
|
-
return "'.config', 'opencode'";
|
|
493
|
-
}
|
|
494
|
-
if (runtime === 'gemini') return "'.gemini'";
|
|
495
|
-
if (runtime === 'kilo') return "'.config', 'kilo'";
|
|
496
|
-
if (runtime === 'codex') return "'.codex'";
|
|
514
|
+
// Global installs. antigravity's home is resolved dynamically (env-overridable,
|
|
515
|
+
// multi-segment via resolveAntigravityGlobalDir + path.relative) — not a table
|
|
516
|
+
// entry. (The prior inner `if (!isGlobal) return "'.agents'"` was unreachable:
|
|
517
|
+
// !isGlobal returns at the top of this function.)
|
|
497
518
|
if (runtime === 'antigravity') {
|
|
498
|
-
if (!isGlobal) return "'.agents'";
|
|
499
519
|
const antigravityDir = resolveAntigravityGlobalDir();
|
|
500
520
|
const rel = path.relative(os.homedir(), antigravityDir);
|
|
501
521
|
const segments = rel.split(path.sep).filter(Boolean);
|
|
@@ -506,16 +526,9 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
|
|
506
526
|
// stable legacy template so generated path.join() calls remain valid.
|
|
507
527
|
return "'.gemini', 'antigravity'";
|
|
508
528
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (runtime === 'trae') return "'.trae'";
|
|
513
|
-
if (runtime === 'qwen') return "'.qwen'";
|
|
514
|
-
if (runtime === 'hermes') return "'.hermes'";
|
|
515
|
-
if (runtime === 'codebuddy') return "'.codebuddy'";
|
|
516
|
-
if (runtime === 'cline') return "'.cline'";
|
|
517
|
-
if (runtime === 'kimi') return "'.config', 'agents'";
|
|
518
|
-
return "'.claude'";
|
|
529
|
+
// All other runtimes: single source-of-truth fragment table (ADR-1239 Phase B,
|
|
530
|
+
// #1679). claude/unknown fall through to the table's default '.claude'.
|
|
531
|
+
return getGlobalConfigHomeFragment(runtime);
|
|
519
532
|
}
|
|
520
533
|
|
|
521
534
|
/**
|
|
@@ -630,6 +643,15 @@ const processAttribution = runtimeArtifactConversion.processAttribution;
|
|
|
630
643
|
const computePathPrefix = runtimeArtifactConversion._computePathPrefix;
|
|
631
644
|
const applyRuntimeContentRewritesInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesInPlace;
|
|
632
645
|
const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace;
|
|
646
|
+
// #1675 (ADR-1508): the augment converter family is single-sourced in the
|
|
647
|
+
// conversion module. install.js re-binds (does not re-define) these so there
|
|
648
|
+
// is exactly one body — the generative-drift hazard the dedup removes. The two
|
|
649
|
+
// private helpers (getAugmentSkillAdapterHeader, convertSlashCommandsToAugmentSkillMentions)
|
|
650
|
+
// live only in the conversion module now; they are no longer duplicated here.
|
|
651
|
+
// (All call sites are below this line → no TDZ hazard.)
|
|
652
|
+
const convertClaudeToAugmentMarkdown = runtimeArtifactConversion.convertClaudeToAugmentMarkdown;
|
|
653
|
+
const convertClaudeCommandToAugmentSkill = runtimeArtifactConversion.convertClaudeCommandToAugmentSkill;
|
|
654
|
+
const convertClaudeAgentToAugmentAgent = runtimeArtifactConversion.convertClaudeAgentToAugmentAgent;
|
|
633
655
|
|
|
634
656
|
function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
635
657
|
return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
|
|
@@ -2580,105 +2602,14 @@ const claudeToAugmentTools = {
|
|
|
2580
2602
|
TodoWrite: 'add_tasks',
|
|
2581
2603
|
};
|
|
2582
2604
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
converted = converted.replace(/\bRead\(/g, 'view(');
|
|
2592
|
-
converted = converted.replace(/\bWrite\(/g, 'save-file(');
|
|
2593
|
-
converted = converted.replace(/\bTodoWrite\(/g, 'add_tasks(');
|
|
2594
|
-
converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
|
|
2595
|
-
// Replace subagent_type from Claude to Augment format
|
|
2596
|
-
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
|
2597
|
-
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
|
2598
|
-
// Replace project-level Claude conventions with Augment equivalents
|
|
2599
|
-
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.augment/rules/`');
|
|
2600
|
-
converted = converted.replace(/\.\/CLAUDE\.md/g, '.augment/rules/');
|
|
2601
|
-
converted = converted.replace(/`CLAUDE\.md`/g, '`.augment/rules/`');
|
|
2602
|
-
converted = converted.replace(/\bCLAUDE\.md\b/g, '.augment/rules/');
|
|
2603
|
-
converted = converted.replace(/\.claude\/skills\//g, '.augment/skills/');
|
|
2604
|
-
// Remove Claude Code-specific bug workarounds before brand replacement
|
|
2605
|
-
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
|
2606
|
-
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
|
2607
|
-
// Replace "Claude Code" brand references with "Augment"
|
|
2608
|
-
converted = converted.replace(/\bClaude Code\b/g, 'Augment');
|
|
2609
|
-
return converted;
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
function getAugmentSkillAdapterHeader(skillName) {
|
|
2613
|
-
return `<augment_skill_adapter>
|
|
2614
|
-
## A. Skill Invocation
|
|
2615
|
-
- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
|
|
2616
|
-
- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
|
|
2617
|
-
- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
|
|
2618
|
-
|
|
2619
|
-
## B. User Prompting
|
|
2620
|
-
When the workflow needs user input, prompt the user conversationally:
|
|
2621
|
-
- Present options as a numbered list in your response text
|
|
2622
|
-
- Ask the user to reply with their choice
|
|
2623
|
-
- For multi-select, ask for comma-separated numbers
|
|
2624
|
-
|
|
2625
|
-
## C. Tool Usage
|
|
2626
|
-
Use these Augment tools when executing GSD workflows:
|
|
2627
|
-
- \`launch-process\` for running commands (terminal operations)
|
|
2628
|
-
- \`str-replace-editor\` for editing existing files
|
|
2629
|
-
- \`view\` for reading files and listing directories
|
|
2630
|
-
- \`save-file\` for creating new files
|
|
2631
|
-
- \`grep\` for searching code (or use MCP servers for advanced search)
|
|
2632
|
-
- \`web-search\`, \`web-fetch\` for web queries
|
|
2633
|
-
- \`add_tasks\`, \`view_tasklist\`, \`update_tasks\` for task management
|
|
2634
|
-
|
|
2635
|
-
## D. Subagent Spawning
|
|
2636
|
-
When the workflow needs to spawn a subagent:
|
|
2637
|
-
- Use the built-in subagent spawning capability
|
|
2638
|
-
- Define agent prompts in \`.augment/agents/\` directory
|
|
2639
|
-
</augment_skill_adapter>`;
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
function convertClaudeCommandToAugmentSkill(content, skillName) {
|
|
2643
|
-
const converted = convertClaudeToAugmentMarkdown(content);
|
|
2644
|
-
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
2645
|
-
let description = `Run GSD workflow ${skillName}.`;
|
|
2646
|
-
if (frontmatter) {
|
|
2647
|
-
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
2648
|
-
if (maybeDescription) {
|
|
2649
|
-
description = maybeDescription;
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
description = toSingleLine(description);
|
|
2653
|
-
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
|
2654
|
-
const adapter = getAugmentSkillAdapterHeader(skillName);
|
|
2655
|
-
|
|
2656
|
-
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
/**
|
|
2660
|
-
* Convert Claude Code agent markdown to Augment agent format.
|
|
2661
|
-
* Strips frontmatter fields Augment doesn't support (color, skills),
|
|
2662
|
-
* converts tool references, and cleans up for Augment agents.
|
|
2663
|
-
*/
|
|
2664
|
-
function convertClaudeAgentToAugmentAgent(content) {
|
|
2665
|
-
let converted = convertClaudeToAugmentMarkdown(content);
|
|
2666
|
-
|
|
2667
|
-
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
2668
|
-
if (!frontmatter) return converted;
|
|
2669
|
-
|
|
2670
|
-
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
|
2671
|
-
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
|
2672
|
-
|
|
2673
|
-
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
|
2674
|
-
|
|
2675
|
-
return `${cleanFrontmatter}\n${body}`;
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
/**
|
|
2679
|
-
* Copy Claude commands as Augment skills — one folder per skill with SKILL.md.
|
|
2680
|
-
* Mirrors copyCommandsAsCursorSkills but uses Augment converters.
|
|
2681
|
-
*/
|
|
2605
|
+
// #1675 (ADR-1508): the augment converter family below was a byte-identical
|
|
2606
|
+
// duplicate of runtime-artifact-conversion.cjs:
|
|
2607
|
+
// convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown,
|
|
2608
|
+
// getAugmentSkillAdapterHeader, convertClaudeCommandToAugmentSkill,
|
|
2609
|
+
// convertClaudeAgentToAugmentAgent
|
|
2610
|
+
// Deleted here and bound from runtimeArtifactConversion above (single source).
|
|
2611
|
+
// The DEFECT.GENERATIVE-FIX parity guard in
|
|
2612
|
+
// tests/enh-1511-rewrite-engine-relocation.test.cjs asserts reference identity.
|
|
2682
2613
|
|
|
2683
2614
|
function convertSlashCommandsToTraeSkillMentions(content) {
|
|
2684
2615
|
return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
@@ -5710,8 +5641,21 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
5710
5641
|
* Reads agent .md files from source, extracts metadata, writes .toml configs.
|
|
5711
5642
|
*/
|
|
5712
5643
|
function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-sandbox') {
|
|
5713
|
-
|
|
5714
|
-
const
|
|
5644
|
+
// ADR-1239 Phase B write-confinement: every Codex config write stays under targetDir.
|
|
5645
|
+
const configPath = assertDestWithinConfigHome(targetDir, 'config.toml');
|
|
5646
|
+
const agentsTomlDir = assertDestWithinConfigHome(targetDir, 'agents');
|
|
5647
|
+
const resolvedTargetRoot = path.resolve(targetDir);
|
|
5648
|
+
// Symlink-escape guard (parity with _copyStaged / copyWithPathReplacement): the
|
|
5649
|
+
// lexical gate above does not resolve symlinks, so a pre-existing config.toml or
|
|
5650
|
+
// agents/ symlink could redirect writes outside targetDir. Reject those.
|
|
5651
|
+
if (
|
|
5652
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, configPath) ||
|
|
5653
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir))
|
|
5654
|
+
) {
|
|
5655
|
+
throw new Error(
|
|
5656
|
+
`installCodexConfig: a Codex config path under "${targetDir}" contains a symlink escaping the install root — refusing to write`,
|
|
5657
|
+
);
|
|
5658
|
+
}
|
|
5715
5659
|
fs.mkdirSync(agentsTomlDir, { recursive: true });
|
|
5716
5660
|
|
|
5717
5661
|
const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
|
|
@@ -5756,7 +5700,16 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
5756
5700
|
// follows the same config-driven precedence as the Claude .md effort key.
|
|
5757
5701
|
const effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
5758
5702
|
const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier);
|
|
5759
|
-
|
|
5703
|
+
// Confine the per-agent write to the agents/ dir itself: a crafted agent
|
|
5704
|
+
// `name` containing path separators must not escape agents/ (which would let
|
|
5705
|
+
// it clobber config.toml or write elsewhere under the configHome).
|
|
5706
|
+
const agentTomlPath = assertDestWithinConfigHome(agentsTomlDir, `${name}.toml`);
|
|
5707
|
+
if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath)) {
|
|
5708
|
+
throw new Error(
|
|
5709
|
+
`installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink escaping the install root — refusing to write`,
|
|
5710
|
+
);
|
|
5711
|
+
}
|
|
5712
|
+
fs.writeFileSync(agentTomlPath, tomlContent);
|
|
5760
5713
|
}
|
|
5761
5714
|
|
|
5762
5715
|
const gsdBlock = generateCodexConfigBlock(agents, targetDir);
|
|
@@ -6326,69 +6279,9 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
|
|
6326
6279
|
return `---\n${newFrontmatter}\n---${body}`;
|
|
6327
6280
|
}
|
|
6328
6281
|
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
* skills as `skills/<name>/SKILL.md` and Kilo follows the same layout
|
|
6333
|
-
* (https://opencode.ai/docs/skills, https://kilo.ai/docs/customize/skills).
|
|
6334
|
-
*
|
|
6335
|
-
* The skill body reuses the runtime's command-frontmatter converter for tool,
|
|
6336
|
-
* path, and `/gsd:`→`/gsd-` body rewrites, then rebuilds a minimal skill
|
|
6337
|
-
* frontmatter: only `name` (lowercase-hyphen, must match the containing
|
|
6338
|
-
* directory) and `description` (1–1024 chars) are emitted, per the OpenCode
|
|
6339
|
-
* skill spec. The command's `tools:`/`permission:` block is intentionally
|
|
6340
|
-
* dropped — OpenCode skills are loaded on-demand via the native skill tool and
|
|
6341
|
-
* inherit the calling agent's permissions.
|
|
6342
|
-
*
|
|
6343
|
-
* @param {string} content - Claude command markdown (with YAML frontmatter)
|
|
6344
|
-
* @param {string} skillName - Skill directory name (e.g. gsd-help)
|
|
6345
|
-
* @param {(content: string) => string} frontmatterConverter - runtime command converter
|
|
6346
|
-
* @returns {string} SKILL.md content
|
|
6347
|
-
*/
|
|
6348
|
-
function convertClaudeCommandToOpencodeFamilySkill(content, skillName, frontmatterConverter) {
|
|
6349
|
-
const converted = frontmatterConverter(content);
|
|
6350
|
-
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
6351
|
-
let description = `Run GSD workflow ${skillName}.`;
|
|
6352
|
-
if (frontmatter) {
|
|
6353
|
-
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
|
6354
|
-
if (maybeDescription) {
|
|
6355
|
-
description = maybeDescription;
|
|
6356
|
-
}
|
|
6357
|
-
}
|
|
6358
|
-
description = toSingleLine(description);
|
|
6359
|
-
// OpenCode skill descriptions must be 1–1024 characters.
|
|
6360
|
-
if (description.length > 1024) {
|
|
6361
|
-
description = `${description.slice(0, 1021)}...`;
|
|
6362
|
-
}
|
|
6363
|
-
// `name` must be lowercase alphanumeric with single-hyphen separators and
|
|
6364
|
-
// match the containing directory name (the staged dir is `${skillName}/`).
|
|
6365
|
-
const name = yamlIdentifier(skillName);
|
|
6366
|
-
return `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---\n\n${body.trimStart()}`;
|
|
6367
|
-
}
|
|
6368
|
-
|
|
6369
|
-
/**
|
|
6370
|
-
* Convert a Claude command (.md) to an OpenCode skill (SKILL.md).
|
|
6371
|
-
* Thin wrapper over the shared OpenCode-family writer.
|
|
6372
|
-
*/
|
|
6373
|
-
function convertClaudeCommandToOpencodeSkill(content, skillName) {
|
|
6374
|
-
return convertClaudeCommandToOpencodeFamilySkill(
|
|
6375
|
-
content,
|
|
6376
|
-
skillName,
|
|
6377
|
-
(c) => convertClaudeToOpencodeFrontmatter(c),
|
|
6378
|
-
);
|
|
6379
|
-
}
|
|
6380
|
-
|
|
6381
|
-
/**
|
|
6382
|
-
* Convert a Claude command (.md) to a Kilo skill (SKILL.md).
|
|
6383
|
-
* Thin wrapper over the shared OpenCode-family writer (Kilo shares the schema).
|
|
6384
|
-
*/
|
|
6385
|
-
function convertClaudeCommandToKiloSkill(content, skillName) {
|
|
6386
|
-
return convertClaudeCommandToOpencodeFamilySkill(
|
|
6387
|
-
content,
|
|
6388
|
-
skillName,
|
|
6389
|
-
(c) => convertClaudeToKiloFrontmatter(c),
|
|
6390
|
-
);
|
|
6391
|
-
}
|
|
6282
|
+
// convertClaudeCommandToOpencodeFamilySkill, convertClaudeCommandToOpencodeSkill,
|
|
6283
|
+
// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6284
|
+
// Imported from installEngine above.
|
|
6392
6285
|
|
|
6393
6286
|
/**
|
|
6394
6287
|
* Convert Claude Code markdown command to Gemini TOML format
|
|
@@ -6471,30 +6364,8 @@ function convertClaudeToGeminiToml(content, { commandName = null } = {}) {
|
|
|
6471
6364
|
* @param {string} pathPrefix - Path prefix for file references
|
|
6472
6365
|
* @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
|
|
6473
6366
|
*/
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
* RAW Claude command/skill body, BEFORE the frontmatter converter runs.
|
|
6477
|
-
*
|
|
6478
|
-
* This is the single source of truth shared by copyFlattenedCommands (commands)
|
|
6479
|
-
* and installOpencodeFamilySkills (skills) so the two surfaces produce identical
|
|
6480
|
-
* path references. Applying pathPrefix pre-conversion (rather than rewriting an
|
|
6481
|
-
* already-converted body) is what avoids the converter's hardcoded default
|
|
6482
|
-
* config dir leaking into --local / --config-dir installs, and the
|
|
6483
|
-
* prefix-overlap double-rewrite hazard for custom dirs like `kilo-alt`. (#784)
|
|
6484
|
-
*
|
|
6485
|
-
* @param {string} content - raw Claude command markdown
|
|
6486
|
-
* @param {string} runtime - 'opencode' or 'kilo'
|
|
6487
|
-
* @param {string} pathPrefix - trailing-slash install-target prefix
|
|
6488
|
-
* @returns {string}
|
|
6489
|
-
*/
|
|
6490
|
-
function applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix) {
|
|
6491
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6492
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6493
|
-
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
|
|
6494
|
-
content = content.replace(/~\/\.opencode\//g, pathPrefix);
|
|
6495
|
-
content = content.replace(/~\/\.kilo\//g, pathPrefix);
|
|
6496
|
-
return content;
|
|
6497
|
-
}
|
|
6367
|
+
// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6368
|
+
// Imported from installEngine above.
|
|
6498
6369
|
|
|
6499
6370
|
function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
6500
6371
|
if (!fs.existsSync(srcDir)) {
|
|
@@ -6633,688 +6504,140 @@ function writeHermesCategoryDescription(categoryDir) {
|
|
|
6633
6504
|
* @param {boolean} isGlobal - Whether this is a global install
|
|
6634
6505
|
*/
|
|
6635
6506
|
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
* spurious "locally modified GSD file" warning (bug #2771).
|
|
6644
|
-
*
|
|
6645
|
-
* Invariant: a file is either distribution (manifest-tracked, diff'd against
|
|
6646
|
-
* manifest) or user artifact (preserved across installs, never diff'd). Never
|
|
6647
|
-
* both. Both preserveUserArtifacts call sites and writeManifest must agree on
|
|
6648
|
-
* this list, which is why it lives here as a single constant.
|
|
6649
|
-
*
|
|
6650
|
-
* Paths are relative to the gsd-core/ directory.
|
|
6651
|
-
*/
|
|
6652
|
-
const USER_OWNED_ARTIFACTS = ['USER-PROFILE.md'];
|
|
6653
|
-
|
|
6654
|
-
/**
|
|
6655
|
-
* Save user-generated files from destDir to an in-memory map before a wipe.
|
|
6656
|
-
*
|
|
6657
|
-
* @param {string} destDir - Directory that is about to be wiped
|
|
6658
|
-
* @param {string[]} fileNames - Relative file names (e.g. ['USER-PROFILE.md']) to preserve
|
|
6659
|
-
* @returns {Map<string, string>} Map of fileName → file content (only entries that existed)
|
|
6660
|
-
*/
|
|
6661
|
-
function preserveUserArtifacts(destDir, fileNames) {
|
|
6662
|
-
const saved = new Map();
|
|
6663
|
-
for (const name of fileNames) {
|
|
6664
|
-
const fullPath = path.join(destDir, name);
|
|
6665
|
-
if (fs.existsSync(fullPath)) {
|
|
6666
|
-
try {
|
|
6667
|
-
saved.set(name, fs.readFileSync(fullPath, 'utf8'));
|
|
6668
|
-
} catch { /* skip unreadable files */ }
|
|
6669
|
-
}
|
|
6670
|
-
}
|
|
6671
|
-
return saved;
|
|
6672
|
-
}
|
|
6673
|
-
|
|
6674
|
-
/**
|
|
6675
|
-
* Restore user-generated files saved by preserveUserArtifacts after a wipe.
|
|
6676
|
-
*
|
|
6677
|
-
* @param {string} destDir - Directory that was wiped and recreated
|
|
6678
|
-
* @param {Map<string, string>} saved - Map returned by preserveUserArtifacts
|
|
6679
|
-
*/
|
|
6680
|
-
function restoreUserArtifacts(destDir, saved) {
|
|
6681
|
-
for (const [name, content] of saved) {
|
|
6682
|
-
const fullPath = path.join(destDir, name);
|
|
6683
|
-
try {
|
|
6684
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
6685
|
-
fs.writeFileSync(fullPath, content, 'utf8');
|
|
6686
|
-
} catch { /* skip unwritable paths */ }
|
|
6687
|
-
}
|
|
6688
|
-
}
|
|
6689
|
-
|
|
6690
|
-
/**
|
|
6691
|
-
* Migrate a legacy dev-preferences.md (saved from commands/gsd/) into the
|
|
6692
|
-
* runtime-aware SKILL.md location used by the writer after #2973.
|
|
6693
|
-
*
|
|
6694
|
-
* For runtimes with a nested skills layout (e.g. Hermes: skills/gsd/<stem>/),
|
|
6695
|
-
* the target is <configDir>/skills/gsd/dev-preferences/SKILL.md.
|
|
6696
|
-
* For runtimes with a flat skills layout (prefix='gsd-'), the target is
|
|
6697
|
-
* <configDir>/skills/gsd-dev-preferences/SKILL.md.
|
|
6698
|
-
*
|
|
6699
|
-
* Skips silently if no legacy file was preserved, or if a SKILL.md already
|
|
6700
|
-
* exists at the new location (don't clobber user-customized skill content
|
|
6701
|
-
* — they may have edited the new file directly). Returns true on actual
|
|
6702
|
-
* migration so callers can log a one-line confirmation.
|
|
6703
|
-
*
|
|
6704
|
-
* @param {string} targetDir - Resolved runtime config directory (e.g. ~/.claude)
|
|
6705
|
-
* @param {Map<string, string>} saved - Map returned by preserveUserArtifacts
|
|
6706
|
-
* @param {string} [runtime] - canonical runtime ID (e.g. 'hermes', 'qwen', 'claude')
|
|
6707
|
-
* @param {'global'|'local'} [scope] - install scope
|
|
6708
|
-
* @returns {boolean} - true if a file was migrated, false otherwise
|
|
6709
|
-
*/
|
|
6710
|
-
function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = 'global') {
|
|
6711
|
-
if (!saved || !saved.has('dev-preferences.md')) return false;
|
|
6712
|
-
let skillDir;
|
|
6713
|
-
if (runtime) {
|
|
6714
|
-
const layout = resolveRuntimeArtifactLayout(runtime, targetDir, scope);
|
|
6715
|
-
const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
|
|
6716
|
-
if (!skillsKindEntry) return false; // runtime has no skills layout at this scope (e.g. cline local)
|
|
6717
|
-
const stemName = skillsKindEntry.prefix === '' ? 'dev-preferences' : 'gsd-dev-preferences';
|
|
6718
|
-
skillDir = path.join(targetDir, skillsKindEntry.destSubpath, stemName);
|
|
6719
|
-
} else {
|
|
6720
|
-
// Legacy fallback for callers that have not yet been updated to pass runtime
|
|
6721
|
-
skillDir = path.join(targetDir, 'skills', 'gsd-dev-preferences');
|
|
6722
|
-
}
|
|
6723
|
-
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
6724
|
-
if (fs.existsSync(skillFile)) return false;
|
|
6725
|
-
try {
|
|
6726
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
6727
|
-
fs.writeFileSync(skillFile, saved.get('dev-preferences.md'), 'utf8');
|
|
6728
|
-
return true;
|
|
6729
|
-
} catch {
|
|
6730
|
-
return false;
|
|
6731
|
-
}
|
|
6732
|
-
}
|
|
6507
|
+
// USER_OWNED_ARTIFACTS, preserveUserArtifacts, restoreUserArtifacts,
|
|
6508
|
+
// migrateLegacyDevPreferencesToSkill, _copyStaged, _removeGsdEntries,
|
|
6509
|
+
// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, _snapshotDir,
|
|
6510
|
+
// _restoreDir, _removeHermesBareStemDirs, installRuntimeArtifacts,
|
|
6511
|
+
// installOpencodeFamilySkills, uninstallRuntimeArtifacts:
|
|
6512
|
+
// ALL moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6513
|
+
// Imported from installEngine above.
|
|
6733
6514
|
|
|
6734
6515
|
// ---------------------------------------------------------------------------
|
|
6735
|
-
// Phase 2 — Layout-driven install/uninstall orchestrators
|
|
6516
|
+
// Phase 2 — Layout-driven install/uninstall orchestrators (moved to engine)
|
|
6517
|
+
// _applyRuntimeRewrites / _stampNonClaudeRuntimeDefaults remain here for
|
|
6518
|
+
// call sites in copyWithPathReplacement (not moved).
|
|
6736
6519
|
// ---------------------------------------------------------------------------
|
|
6737
|
-
|
|
6738
|
-
/**
|
|
6739
|
-
* Apply per-runtime content rewrites in place across every SKILL.md inside a
|
|
6740
|
-
* staged directory. Reproduces the rewrite scaffolding that the old
|
|
6741
|
-
* copyCommandsAs<Runtime>Skills functions applied between read-content and
|
|
6742
|
-
* converter-call. Applied AFTER stage (which already called the converter);
|
|
6743
|
-
* rewrites target stable path patterns the converter doesn't touch.
|
|
6744
|
-
*
|
|
6745
|
-
* For Qwen/Hermes, branding rewrites (.claude/ → .qwen/ / .hermes/) run
|
|
6746
|
-
* AFTER the slash-form path replacements but they only catch bare `.claude/`
|
|
6747
|
-
* patterns (skill-body relative refs) that the slash forms didn't consume.
|
|
6748
|
-
* This mirrors the exact ordering in the legacy copyCommandsAsClaudeSkills body.
|
|
6749
|
-
*
|
|
6750
|
-
* @param {string} stagedDir
|
|
6751
|
-
* @param {string} runtime
|
|
6752
|
-
* @param {string} pathPrefix e.g. "~/.codex/" — trailing-slash string
|
|
6753
|
-
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6754
|
-
*/
|
|
6755
|
-
// applyRuntimeContentRewritesInPlace: walk loop is now owned by
|
|
6756
|
-
// runtimeArtifactConversion.applyRuntimeContentRewritesInPlace (ADR-1508 / #1511 Phase 2).
|
|
6757
|
-
// The const binding above (~line 629) delegates here. Call sites in installRuntimeArtifacts
|
|
6758
|
-
// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
|
|
6759
|
-
|
|
6760
|
-
/**
|
|
6761
|
-
* Apply per-runtime content rewrites to flat .md files in a staged commands dir.
|
|
6762
|
-
* Used for runtimes that have a commandsKind in their layout and need content rewrites
|
|
6763
|
-
* (e.g. augment — replaces ~/.claude/ paths and applies branding conversions).
|
|
6764
|
-
*
|
|
6765
|
-
* IMPORTANT: `stageSkillsForProfile()` returns the original source directory unchanged
|
|
6766
|
-
* on a full/default profile (skills === '*'). This function MUST NOT mutate that source
|
|
6767
|
-
* directory. It always copies to a temp dir first, rewrites there, and returns the new
|
|
6768
|
-
* path so the caller installs from the temp copy, not the source.
|
|
6769
|
-
*
|
|
6770
|
-
* @param {string} stagedDir directory of staged flat .md command files (may be source dir)
|
|
6771
|
-
* @param {string} runtime
|
|
6772
|
-
* @param {string} pathPrefix
|
|
6773
|
-
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6774
|
-
* @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup)
|
|
6775
|
-
*/
|
|
6776
|
-
// applyRuntimeContentRewritesForCommandsInPlace: copy+rewrite loop is now owned by
|
|
6777
|
-
// runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace (ADR-1508 / #1511 Phase 2).
|
|
6778
|
-
// The const binding above (~line 630) delegates here. Call sites in installRuntimeArtifacts
|
|
6779
|
-
// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
|
|
6780
|
-
|
|
6781
|
-
/**
|
|
6782
|
-
* Apply the per-runtime rewrite table to a single content string.
|
|
6783
|
-
* Extracted so it can be unit-tested independently of the filesystem walk.
|
|
6784
|
-
*
|
|
6785
|
-
* @param {string} content
|
|
6786
|
-
* @param {string} runtime
|
|
6787
|
-
* @param {string} pathPrefix trailing-slash string
|
|
6788
|
-
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6789
|
-
* @returns {string}
|
|
6790
|
-
*/
|
|
6791
|
-
// _applyRuntimeRewrites: single implementation lives in runtimeArtifactConversion
|
|
6792
|
-
// (ADR-1508 / #1511 Phase 2). Bound here so install.js call sites and exports are
|
|
6793
|
-
// reference-identical to the conversion module (consistent with the walkers above).
|
|
6794
|
-
// All call sites are below this line → no TDZ hazard.
|
|
6795
6520
|
const _applyRuntimeRewrites = runtimeArtifactConversion._applyRuntimeRewrites;
|
|
6796
6521
|
const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaudeRuntimeDefaults;
|
|
6797
6522
|
|
|
6798
6523
|
/**
|
|
6799
|
-
*
|
|
6800
|
-
*
|
|
6801
|
-
*
|
|
6802
|
-
*
|
|
6803
|
-
*
|
|
6804
|
-
*
|
|
6805
|
-
*
|
|
6806
|
-
*
|
|
6807
|
-
*
|
|
6808
|
-
*
|
|
6809
|
-
*
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
}
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
let savedLegacyArtifacts = null;
|
|
6917
|
-
if (runtime === 'claude' || runtime === 'qwen' || runtime === 'hermes') {
|
|
6918
|
-
if (fs.existsSync(legacyCommandsGsd)) {
|
|
6919
|
-
savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsGsd, ['dev-preferences.md']);
|
|
6920
|
-
fs.rmSync(legacyCommandsGsd, { recursive: true });
|
|
6921
|
-
}
|
|
6922
|
-
}
|
|
6923
|
-
|
|
6924
|
-
// Hermes: remove pre-#2841 flat skills/gsd-*/ entries that lived alongside
|
|
6925
|
-
// the new skills/gsd/ nested layout.
|
|
6926
|
-
if (runtime === 'hermes') {
|
|
6927
|
-
const flatSkillsDir = path.join(configDir, 'skills');
|
|
6928
|
-
if (fs.existsSync(flatSkillsDir)) {
|
|
6929
|
-
for (const entry of fs.readdirSync(flatSkillsDir, { withFileTypes: true })) {
|
|
6930
|
-
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
6931
|
-
fs.rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
|
|
6932
|
-
}
|
|
6933
|
-
}
|
|
6934
|
-
}
|
|
6935
|
-
|
|
6936
|
-
// Hermes: bare-stem skills/gsd/<stem>/ cleanup is deferred to AFTER the
|
|
6937
|
-
// layout-driven install loop in installRuntimeArtifacts, where the exact set
|
|
6938
|
-
// of staged gsd-<stem>/ dirs is known. Removing here (before staging) would
|
|
6939
|
-
// require readGsdCommandNames() which misses skills like 'dev-preferences'
|
|
6940
|
-
// that are not in the commands directory. See _removeHermesBareStemDirs().
|
|
6941
|
-
}
|
|
6942
|
-
|
|
6943
|
-
// Migrate dev-preferences.md content → runtime-aware SKILL.md location (#2973).
|
|
6944
|
-
// Done after all layout cleanup so Hermes flat-dir removal does not delete the
|
|
6945
|
-
// newly created skill dir. No-op if skill file already exists.
|
|
6946
|
-
if (savedLegacyArtifacts) {
|
|
6947
|
-
migrateLegacyDevPreferencesToSkill(configDir, savedLegacyArtifacts, runtime, scope);
|
|
6948
|
-
}
|
|
6949
|
-
}
|
|
6950
|
-
|
|
6951
|
-
/**
|
|
6952
|
-
* Run legacy uninstall cleanup that must execute BEFORE the layout-driven
|
|
6953
|
-
* removal so old-format entries are also cleaned up.
|
|
6954
|
-
*
|
|
6955
|
-
* - Claude global/Qwen: remove legacy commands/gsd/ directory if present.
|
|
6956
|
-
* For Claude LOCAL, commands/gsd/ is the current primary location (not
|
|
6957
|
-
* legacy), so we skip removal here and let _removeGsdEntries handle it
|
|
6958
|
-
* with gsd- prefix filtering (preserving user files like dev-preferences.md).
|
|
6959
|
-
* - Hermes: remove pre-2841 flat skills/gsd-STAR entries.
|
|
6960
|
-
*
|
|
6961
|
-
* @param {string} runtime
|
|
6962
|
-
* @param {string} configDir resolved runtime config directory
|
|
6963
|
-
* @param {'global'|'local'} [scope]
|
|
6964
|
-
*/
|
|
6965
|
-
function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
|
|
6966
|
-
// commands/gsd/ is a legacy location for Qwen, Hermes, and all Claude installs.
|
|
6967
|
-
// Prior to #1367 fix, Claude-local used commands/gsd/<cmd>.md (colon-namespaced).
|
|
6968
|
-
// After #1367, Claude-local uses flat commands/gsd-<cmd>.md. The inline uninstall
|
|
6969
|
-
// block (1c) handles removal of flat files; this function handles the legacy
|
|
6970
|
-
// commands/gsd/ directory for all Claude scopes (global was already included,
|
|
6971
|
-
// local is now added since that layout is also legacy post-#1367).
|
|
6972
|
-
// #2973 / Codex review (bd1f06c9): preserve user-owned dev-preferences.md
|
|
6973
|
-
// before destructive wipe. Migration to skills/gsd-dev-preferences/SKILL.md
|
|
6974
|
-
// is deferred and returned so the caller can apply it AFTER layout-driven
|
|
6975
|
-
// removal — this prevents the layout's gsd-* prefix removal from wiping the
|
|
6976
|
-
// freshly created skill dir (same pattern as _runLegacyInstallMigrations).
|
|
6977
|
-
let savedLegacyArtifacts = null;
|
|
6978
|
-
// commands/gsd/ is a legacy location for Qwen, Hermes, and Claude global.
|
|
6979
|
-
// Claude local is intentionally excluded: the inline uninstall block (1c) handles
|
|
6980
|
-
// commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
|
|
6981
|
-
// to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
|
|
6982
|
-
// (which would redirect to skills/) conflicts with the test contract for local installs.
|
|
6983
|
-
const isLegacyCommandsGsd = runtime === 'qwen' || runtime === 'hermes' || (runtime === 'claude' && scope === 'global');
|
|
6984
|
-
if (isLegacyCommandsGsd) {
|
|
6985
|
-
const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
|
|
6986
|
-
if (fs.existsSync(legacyCommandsGsd)) {
|
|
6987
|
-
savedLegacyArtifacts = preserveUserArtifacts(legacyCommandsGsd, ['dev-preferences.md']);
|
|
6988
|
-
fs.rmSync(legacyCommandsGsd, { recursive: true });
|
|
6989
|
-
}
|
|
6990
|
-
}
|
|
6991
|
-
|
|
6992
|
-
// Hermes: pre-#2841 flat skills/gsd-*/ entries
|
|
6993
|
-
if (runtime === 'hermes') {
|
|
6994
|
-
const flatSkillsDir = path.join(configDir, 'skills');
|
|
6995
|
-
if (fs.existsSync(flatSkillsDir)) {
|
|
6996
|
-
for (const entry of fs.readdirSync(flatSkillsDir, { withFileTypes: true })) {
|
|
6997
|
-
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
6998
|
-
fs.rmSync(path.join(flatSkillsDir, entry.name), { recursive: true });
|
|
6999
|
-
}
|
|
7000
|
-
}
|
|
7001
|
-
}
|
|
7002
|
-
|
|
7003
|
-
// Hermes: pre-#947 bare-stem skills/gsd/<stem>/ entries (dirs that do NOT
|
|
7004
|
-
// start with 'gsd-') — the #3664 layout used prefix='' so GSD-owned skills
|
|
7005
|
-
// had bare names (e.g. skills/gsd/help/). These are stale on uninstall.
|
|
7006
|
-
const nestedGsdDirForUninstall = path.join(configDir, 'skills', 'gsd');
|
|
7007
|
-
if (fs.existsSync(nestedGsdDirForUninstall)) {
|
|
7008
|
-
for (const entry of fs.readdirSync(nestedGsdDirForUninstall, { withFileTypes: true })) {
|
|
7009
|
-
if (entry.isDirectory() && !entry.name.startsWith('gsd-')) {
|
|
7010
|
-
fs.rmSync(path.join(nestedGsdDirForUninstall, entry.name), { recursive: true });
|
|
7011
|
-
}
|
|
7012
|
-
}
|
|
7013
|
-
}
|
|
7014
|
-
}
|
|
7015
|
-
|
|
7016
|
-
// Return saved artifacts so the caller can migrate after layout-driven removal.
|
|
7017
|
-
return savedLegacyArtifacts;
|
|
7018
|
-
}
|
|
7019
|
-
|
|
7020
|
-
/**
|
|
7021
|
-
* Layout-driven install orchestrator.
|
|
7022
|
-
* Runs legacy migrations first, then uses resolveRuntimeArtifactLayout to
|
|
7023
|
-
* determine what artifact kinds to write and where.
|
|
7024
|
-
*
|
|
7025
|
-
* @param {string} runtime canonical runtime ID
|
|
7026
|
-
* @param {string} configDir resolved runtime config directory
|
|
7027
|
-
* @param {'global'|'local'} scope
|
|
7028
|
-
* @param {Object} resolvedProfile from resolveProfile() / resolveEffectiveProfile()
|
|
7029
|
-
*/
|
|
7030
|
-
/**
|
|
7031
|
-
* Deep-snapshot a directory tree into a Map<relPath, Buffer>.
|
|
7032
|
-
* Returns an empty Map if the directory doesn't exist.
|
|
7033
|
-
* @param {string} dir
|
|
7034
|
-
* @returns {Map<string, Buffer>}
|
|
7035
|
-
*/
|
|
7036
|
-
function _snapshotDir(dir) {
|
|
7037
|
-
const files = new Map();
|
|
7038
|
-
if (!fs.existsSync(dir)) return files;
|
|
7039
|
-
const walk = (relPath, absPath) => {
|
|
7040
|
-
for (const e of fs.readdirSync(absPath, { withFileTypes: true })) {
|
|
7041
|
-
const childRel = relPath ? path.join(relPath, e.name) : e.name;
|
|
7042
|
-
const childAbs = path.join(absPath, e.name);
|
|
7043
|
-
if (e.isDirectory()) walk(childRel, childAbs);
|
|
7044
|
-
else if (e.isFile()) files.set(childRel, fs.readFileSync(childAbs));
|
|
7045
|
-
}
|
|
7046
|
-
};
|
|
7047
|
-
walk('', dir);
|
|
7048
|
-
return files;
|
|
7049
|
-
}
|
|
7050
|
-
|
|
7051
|
-
/**
|
|
7052
|
-
* Restore a directory tree from a Map<relPath, Buffer> produced by _snapshotDir.
|
|
7053
|
-
* @param {string} dir
|
|
7054
|
-
* @param {Map<string, Buffer>} snapshot
|
|
7055
|
-
*/
|
|
7056
|
-
function _restoreDir(dir, snapshot) {
|
|
7057
|
-
for (const [relPath, buf] of snapshot) {
|
|
7058
|
-
const absPath = path.join(dir, relPath);
|
|
7059
|
-
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
7060
|
-
fs.writeFileSync(absPath, buf);
|
|
7061
|
-
}
|
|
7062
|
-
}
|
|
7063
|
-
|
|
7064
|
-
/**
|
|
7065
|
-
* After the layout-driven install loop writes new gsd-<stem>/ dirs to
|
|
7066
|
-
* skills/gsd/, remove any pre-existing bare-stem dirs (skills/gsd/<stem>/)
|
|
7067
|
-
* that correspond to the newly installed gsd-<stem> entries.
|
|
7068
|
-
*
|
|
7069
|
-
* The removal set is derived from the ACTUAL installed skill dirs (every
|
|
7070
|
-
* entry starting with 'gsd-' that is a directory), so it covers ALL shipped
|
|
7071
|
-
* GSD skills — including 'dev-preferences' and future additions — without
|
|
7072
|
-
* relying on readGsdCommandNames() which only enumerates the commands source
|
|
7073
|
-
* tree and can miss skills that ship outside that directory.
|
|
7074
|
-
*
|
|
7075
|
-
* Safety: a bare dir is ONLY removed when a corresponding gsd-<stem>/ dir was
|
|
7076
|
-
* installed this run. A user-owned dir 'skills/gsd/my-workflow/' that has no
|
|
7077
|
-
* matching 'skills/gsd/gsd-my-workflow/' is never touched.
|
|
7078
|
-
*
|
|
7079
|
-
* @param {string} nestedGsdDir absolute path to skills/gsd/ category dir
|
|
7080
|
-
*/
|
|
7081
|
-
function _removeHermesBareStemDirs(nestedGsdDir) {
|
|
7082
|
-
if (!fs.existsSync(nestedGsdDir)) return;
|
|
7083
|
-
const entries = fs.readdirSync(nestedGsdDir, { withFileTypes: true });
|
|
7084
|
-
|
|
7085
|
-
// Collect the set of stems that were installed as gsd-<stem>/ this run.
|
|
7086
|
-
const installedStems = new Set();
|
|
7087
|
-
for (const entry of entries) {
|
|
7088
|
-
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
|
7089
|
-
installedStems.add(entry.name.slice('gsd-'.length)); // e.g. 'quick', 'dev-preferences'
|
|
7090
|
-
}
|
|
7091
|
-
}
|
|
7092
|
-
|
|
7093
|
-
// Remove any bare <stem>/ dir for which gsd-<stem>/ was just installed.
|
|
7094
|
-
for (const entry of entries) {
|
|
7095
|
-
if (entry.isDirectory() && !entry.name.startsWith('gsd-') && installedStems.has(entry.name)) {
|
|
7096
|
-
fs.rmSync(path.join(nestedGsdDir, entry.name), { recursive: true });
|
|
7097
|
-
}
|
|
7098
|
-
}
|
|
7099
|
-
}
|
|
7100
|
-
|
|
7101
|
-
function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
|
7102
|
-
// Legacy cleanup before layout-driven writes
|
|
7103
|
-
_runLegacyInstallMigrations(runtime, configDir, scope);
|
|
7104
|
-
|
|
7105
|
-
const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
7106
|
-
const planResult = createRuntimeArtifactInstallPlan({
|
|
7107
|
-
layout,
|
|
7108
|
-
resolvedProfile,
|
|
7109
|
-
homedir: () => os.homedir(),
|
|
7110
|
-
platform: process.platform,
|
|
7111
|
-
resolveAttribution: getCommitAttribution,
|
|
7112
|
-
});
|
|
7113
|
-
|
|
7114
|
-
const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
|
|
7115
|
-
try {
|
|
7116
|
-
if (!planResult.ok) {
|
|
7117
|
-
throw new Error(planResult.message);
|
|
7118
|
-
}
|
|
7119
|
-
|
|
7120
|
-
const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
|
|
7121
|
-
for (const item of planResult.plan.items) {
|
|
7122
|
-
const kind = kindsByName.get(item.kind);
|
|
7123
|
-
if (!kind) throw new Error(`Install plan returned unknown artifact kind: ${item.kind}`);
|
|
7124
|
-
const dest = item.destDir;
|
|
7125
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
7126
|
-
if (kind.kind === 'skills' && fs.existsSync(dest)) {
|
|
7127
|
-
// Pre-prune: snapshot user-owned content before _removeGsdEntries wipes it,
|
|
7128
|
-
// then restore after. This preserves user dirs across a wipe-and-replace
|
|
7129
|
-
// install (#2973 / #3664).
|
|
7130
|
-
//
|
|
7131
|
-
// All runtimes (incl. Hermes after #947) use prefix='gsd-'.
|
|
7132
|
-
// _removeGsdEntries removes only gsd-* entries; non-gsd-* user dirs are
|
|
7133
|
-
// untouched. Preserve the explicit user-owned GSD-prefixed skill
|
|
7134
|
-
// gsd-dev-preferences, which GSD does not reinstall from source but must
|
|
7135
|
-
// survive the prune (#2973).
|
|
7136
|
-
const toPreserve = new Map(); // dirName -> Map<relPath, Buffer>
|
|
7137
|
-
|
|
7138
|
-
{
|
|
7139
|
-
// Preserve explicitly user-owned GSD-prefixed skill dirs.
|
|
7140
|
-
// gsd-dev-preferences is the sole user-customisable skill in this category.
|
|
7141
|
-
const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences'];
|
|
7142
|
-
for (const dirName of USER_OWNED_SKILL_DIRS) {
|
|
7143
|
-
const skillDir = path.join(dest, dirName);
|
|
7144
|
-
if (!fs.existsSync(skillDir)) continue;
|
|
7145
|
-
const snap = _snapshotDir(skillDir);
|
|
7146
|
-
if (snap.size > 0) toPreserve.set(dirName, snap);
|
|
7147
|
-
}
|
|
7148
|
-
}
|
|
7149
|
-
|
|
7150
|
-
_removeGsdEntries(dest, kind);
|
|
7151
|
-
_copyStaged(item.sourceDir, dest, kind);
|
|
7152
|
-
|
|
7153
|
-
// Restore user-owned dirs after the prune+copy
|
|
7154
|
-
for (const [dirName, snap] of toPreserve) {
|
|
7155
|
-
_restoreDir(path.join(dest, dirName), snap);
|
|
7156
|
-
}
|
|
7157
|
-
} else {
|
|
7158
|
-
// For non-skills kinds (commands, agents): no user content to preserve;
|
|
7159
|
-
// just prune stale gsd-* entries and copy new ones.
|
|
7160
|
-
_removeGsdEntries(dest, kind);
|
|
7161
|
-
_copyStaged(item.sourceDir, dest, kind);
|
|
7162
|
-
}
|
|
7163
|
-
}
|
|
7164
|
-
} finally {
|
|
7165
|
-
for (const dir of cleanupDirs) {
|
|
7166
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
7167
|
-
}
|
|
7168
|
-
}
|
|
7169
|
-
|
|
7170
|
-
// Hermes: after the install loop has written all gsd-<stem>/ dirs to
|
|
7171
|
-
// skills/gsd/, remove any stale bare-stem dirs (skills/gsd/<stem>/) that
|
|
7172
|
-
// correspond to the newly installed gsd-<stem> entries. This is the robust
|
|
7173
|
-
// replacement for the readGsdCommandNames()-based pre-install cleanup that
|
|
7174
|
-
// missed skills like 'dev-preferences' (#947 adversarial review).
|
|
7175
|
-
//
|
|
7176
|
-
// We run this AFTER the install loop so the installed set is authoritative:
|
|
7177
|
-
// every gsd-<stem>/ present now was written this run (or was there before
|
|
7178
|
-
// with the same prefix). User-owned bare dirs with no gsd-<stem> counterpart
|
|
7179
|
-
// are untouched.
|
|
7180
|
-
if (runtime === 'hermes') {
|
|
7181
|
-
const nestedGsdDirForCleanup = path.join(configDir, 'skills', 'gsd');
|
|
7182
|
-
_removeHermesBareStemDirs(nestedGsdDirForCleanup);
|
|
7183
|
-
}
|
|
7184
|
-
}
|
|
7185
|
-
|
|
7186
|
-
/**
|
|
7187
|
-
* Install the skills layout kind for an OpenCode-family runtime (OpenCode/Kilo).
|
|
7188
|
-
*
|
|
7189
|
-
* These runtimes do NOT go through installRuntimeArtifacts (their commands use a
|
|
7190
|
-
* bespoke flattened-command writer), so this writes ONLY the skills kind
|
|
7191
|
-
* alongside their existing command/ + agents/ surfaces. Uninstall is already
|
|
7192
|
-
* layout-driven (uninstallRuntimeArtifacts iterates layout.kinds), so the
|
|
7193
|
-
* skills/ dir is cleaned up automatically once the layout declares it.
|
|
7194
|
-
*
|
|
7195
|
-
* `rawCommandsDir` MUST be the SAME staged command directory the flattened
|
|
7196
|
-
* command writer consumes (the caller passes its `_stageSkills()` output) so the
|
|
7197
|
-
* command/ and skills/ surfaces always cover the identical, profile-resolved set
|
|
7198
|
-
* — including the `--minimal`/`--core-only` alias path, which stages differently
|
|
7199
|
-
* from a plain `--profile=core`.
|
|
7200
|
-
*
|
|
7201
|
-
* Mirrors copyFlattenedCommands exactly per file — pathPrefix rewrite →
|
|
7202
|
-
* attribution → command→skill conversion — guaranteeing command/ and skills/
|
|
7203
|
-
* bodies match byte-for-byte for global, --local, and --config-dir installs.
|
|
7204
|
-
* We deliberately do NOT use skillsKindEntry.stage(): that converts before any
|
|
7205
|
-
* pathPrefix is known, so its bodies would carry the converter's hardcoded
|
|
7206
|
-
* default config dir. (#784)
|
|
7207
|
-
*
|
|
7208
|
-
* @param {string} runtime - 'opencode' or 'kilo'
|
|
7209
|
-
* @param {string} targetDir - resolved runtime config directory
|
|
7210
|
-
* @param {string} rawCommandsDir - staged RAW Claude command dir (caller's _stageSkills output)
|
|
7211
|
-
* @param {string} pathPrefix - computed config-path prefix for body rewrites
|
|
7212
|
-
* @returns {number} number of gsd-* skill directories written
|
|
7213
|
-
*/
|
|
7214
|
-
function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPrefix) {
|
|
7215
|
-
const layout = resolveRuntimeArtifactLayout(runtime, targetDir);
|
|
7216
|
-
const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
|
|
7217
|
-
if (!skillsKindEntry) return 0;
|
|
7218
|
-
const rawDir = rawCommandsDir;
|
|
7219
|
-
if (!rawDir || !fs.existsSync(rawDir)) return 0;
|
|
7220
|
-
|
|
7221
|
-
const converter = runtime === 'kilo'
|
|
7222
|
-
? convertClaudeCommandToKiloSkill
|
|
7223
|
-
: convertClaudeCommandToOpencodeSkill;
|
|
7224
|
-
|
|
7225
|
-
const dest = path.join(targetDir, skillsKindEntry.destSubpath);
|
|
7226
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
7227
|
-
|
|
7228
|
-
// Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
|
|
7229
|
-
// gsd-dev-preferences is generated by the user (via generate-dev-preferences)
|
|
7230
|
-
// and lives at <configDir>/skills/gsd-dev-preferences — _removeGsdEntries
|
|
7231
|
-
// would otherwise wipe it. Mirrors the preservation in installRuntimeArtifacts
|
|
7232
|
-
// (#2973).
|
|
7233
|
-
const USER_OWNED_SKILL_DIRS = ['gsd-dev-preferences'];
|
|
7234
|
-
const toPreserve = new Map(); // dirName -> Map<relPath, Buffer>
|
|
7235
|
-
for (const dirName of USER_OWNED_SKILL_DIRS) {
|
|
7236
|
-
const skillDir = path.join(dest, dirName);
|
|
7237
|
-
if (!fs.existsSync(skillDir)) continue;
|
|
7238
|
-
const snap = _snapshotDir(skillDir);
|
|
7239
|
-
if (snap.size > 0) toPreserve.set(dirName, snap);
|
|
7240
|
-
}
|
|
7241
|
-
|
|
7242
|
-
_removeGsdEntries(dest, skillsKindEntry);
|
|
7243
|
-
|
|
7244
|
-
let count = 0;
|
|
7245
|
-
for (const entry of fs.readdirSync(rawDir, { withFileTypes: true })) {
|
|
7246
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
7247
|
-
const stem = entry.name.slice(0, -3);
|
|
7248
|
-
const skillName = `${skillsKindEntry.prefix}${stem}`;
|
|
7249
|
-
let content = fs.readFileSync(path.join(rawDir, entry.name), 'utf8');
|
|
7250
|
-
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
7251
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7252
|
-
content = converter(content, skillName);
|
|
7253
|
-
const skillDir = path.join(dest, skillName);
|
|
7254
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
7255
|
-
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
7256
|
-
count++;
|
|
7257
|
-
}
|
|
7258
|
-
|
|
7259
|
-
// Restore user-owned dirs after the prune+copy.
|
|
7260
|
-
for (const [dirName, snap] of toPreserve) {
|
|
7261
|
-
_restoreDir(path.join(dest, dirName), snap);
|
|
7262
|
-
}
|
|
7263
|
-
|
|
7264
|
-
return count;
|
|
7265
|
-
}
|
|
7266
|
-
|
|
7267
|
-
/**
|
|
7268
|
-
* Layout-driven uninstall orchestrator.
|
|
7269
|
-
* Runs legacy cleanup first, then uses resolveRuntimeArtifactLayout to
|
|
7270
|
-
* determine which GSD-owned entries to remove.
|
|
7271
|
-
*
|
|
7272
|
-
* @param {string} runtime canonical runtime ID
|
|
7273
|
-
* @param {string} configDir resolved runtime config directory
|
|
7274
|
-
* @param {'global'|'local'} scope
|
|
7275
|
-
*/
|
|
7276
|
-
function uninstallRuntimeArtifacts(runtime, configDir, scope) {
|
|
7277
|
-
// Legacy cleanup before layout-driven removal (scope-aware to avoid
|
|
7278
|
-
// removing Claude local commands/gsd/ which is the primary install dir).
|
|
7279
|
-
// Returns saved user artifacts so we can migrate AFTER layout removal
|
|
7280
|
-
// (the layout's gsd-* prefix pass would wipe a skill dir created here).
|
|
7281
|
-
const savedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope);
|
|
7282
|
-
|
|
7283
|
-
const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
7284
|
-
const plan = createRuntimeArtifactUninstallPlan(layout);
|
|
7285
|
-
const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
|
|
7286
|
-
for (const item of plan.items) {
|
|
7287
|
-
const kind = kindsByName.get(item.kind);
|
|
7288
|
-
if (!kind) {
|
|
7289
|
-
throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`);
|
|
7290
|
-
}
|
|
7291
|
-
_removeGsdEntries(item.destDir, kind);
|
|
7292
|
-
}
|
|
7293
|
-
|
|
7294
|
-
// Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove
|
|
7295
|
-
// the GSD-managed DESCRIPTION.md and then the category dir itself if it
|
|
7296
|
-
// contains no user content (#947). _removeGsdEntries removed gsd-* dirs
|
|
7297
|
-
// but left the category container and DESCRIPTION.md intact.
|
|
7298
|
-
if (runtime === 'hermes') {
|
|
7299
|
-
const nestedGsdDir = path.join(configDir, 'skills', 'gsd');
|
|
7300
|
-
if (fs.existsSync(nestedGsdDir)) {
|
|
7301
|
-
// Remove GSD-owned DESCRIPTION.md (written by writeHermesCategoryDescription)
|
|
7302
|
-
fs.rmSync(path.join(nestedGsdDir, 'DESCRIPTION.md'), { force: true });
|
|
7303
|
-
// Remove the category dir if empty (no user content remaining)
|
|
7304
|
-
const remaining = fs.readdirSync(nestedGsdDir, { withFileTypes: true });
|
|
7305
|
-
if (remaining.length === 0) {
|
|
7306
|
-
fs.rmSync(nestedGsdDir, { recursive: true, force: true });
|
|
7307
|
-
}
|
|
7308
|
-
}
|
|
7309
|
-
}
|
|
7310
|
-
|
|
7311
|
-
// #2973 / Codex review (bd1f06c9): migrate dev-preferences.md to the
|
|
7312
|
-
// runtime-aware SKILL.md location after all layout-driven removal is
|
|
7313
|
-
// complete. Do NOT restore to commands/gsd/ — the user is uninstalling.
|
|
7314
|
-
if (savedLegacyArtifacts) {
|
|
7315
|
-
migrateLegacyDevPreferencesToSkill(configDir, savedLegacyArtifacts, runtime, scope);
|
|
7316
|
-
}
|
|
7317
|
-
}
|
|
6524
|
+
* Data-driven dispatch table for copyWithPathReplacement (ADR-1239 Phase B).
|
|
6525
|
+
* Keyed by runtime id. Each entry declares ONLY what that runtime does differently.
|
|
6526
|
+
* The DEFAULT (no entry, or entry with no md/js key) = identity transform after
|
|
6527
|
+
* the uniform steps — covers claude, augment, codebuddy, kimi, etc.
|
|
6528
|
+
*
|
|
6529
|
+
* Entry shape:
|
|
6530
|
+
* mdSkipGenericRewrite?: boolean — skip the ~/.claude/ rewrite block (copilot, antigravity)
|
|
6531
|
+
* md?: (content, ctx) => string — per-runtime .md transform
|
|
6532
|
+
* mdReattributeAfter?: boolean — re-run processAttribution after md() (copilot, antigravity)
|
|
6533
|
+
* mdTomlRenameOnCommand?: boolean — when isCommand, rename dest .md → .toml (gemini)
|
|
6534
|
+
* js?: (content, ctx) => string — per-runtime .cjs/.js transform (absent = plain copyFileSync)
|
|
6535
|
+
*
|
|
6536
|
+
* ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName, runtime }
|
|
6537
|
+
*/
|
|
6538
|
+
const RUNTIME_CONTENT_DISPATCH = {
|
|
6539
|
+
opencode: {
|
|
6540
|
+
md: (content) => convertClaudeToOpencodeFrontmatter(content),
|
|
6541
|
+
},
|
|
6542
|
+
kilo: {
|
|
6543
|
+
md: (content) => convertClaudeToKiloFrontmatter(content),
|
|
6544
|
+
},
|
|
6545
|
+
gemini: {
|
|
6546
|
+
md: (content, ctx) =>
|
|
6547
|
+
convertClaudeToGeminiMarkdown(content, {
|
|
6548
|
+
isCommand: ctx.isCommand,
|
|
6549
|
+
commandName: ctx.isCommand ? ctx.entryName.replace(/\.md$/, '') : null,
|
|
6550
|
+
}),
|
|
6551
|
+
mdTomlRenameOnCommand: true,
|
|
6552
|
+
},
|
|
6553
|
+
codex: {
|
|
6554
|
+
md: (content) => convertClaudeToCodexMarkdown(content),
|
|
6555
|
+
},
|
|
6556
|
+
copilot: {
|
|
6557
|
+
mdSkipGenericRewrite: true,
|
|
6558
|
+
md: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal),
|
|
6559
|
+
mdReattributeAfter: true,
|
|
6560
|
+
js: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal),
|
|
6561
|
+
},
|
|
6562
|
+
antigravity: {
|
|
6563
|
+
mdSkipGenericRewrite: true,
|
|
6564
|
+
md: (content, ctx) => convertClaudeToAntigravityContent(content, ctx.isGlobal),
|
|
6565
|
+
mdReattributeAfter: true,
|
|
6566
|
+
js: (content, ctx) => convertClaudeToAntigravityContent(content, ctx.isGlobal),
|
|
6567
|
+
},
|
|
6568
|
+
cursor: {
|
|
6569
|
+
md: (content) => convertClaudeToCursorMarkdown(content),
|
|
6570
|
+
js: (content) => {
|
|
6571
|
+
content = content.replace(/gsd:/gi, 'gsd-');
|
|
6572
|
+
content = content.replace(/\.claude\/skills\//g, '.cursor/skills/');
|
|
6573
|
+
content = content.replace(/CLAUDE\.md/g, '.cursor/rules/');
|
|
6574
|
+
content = content.replace(/\bClaude Code\b/g, 'Cursor');
|
|
6575
|
+
return content;
|
|
6576
|
+
},
|
|
6577
|
+
},
|
|
6578
|
+
windsurf: {
|
|
6579
|
+
md: (content) => convertClaudeToWindsurfMarkdown(content),
|
|
6580
|
+
js: (content) => {
|
|
6581
|
+
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
|
|
6582
|
+
content = content.replace(/gsd:/gi, 'gsd-');
|
|
6583
|
+
content = content.replace(/\.claude\/skills\//g, '.devin/skills/');
|
|
6584
|
+
content = content.replace(/CLAUDE\.md/g, '.devin/rules');
|
|
6585
|
+
content = content.replace(/\bClaude Code\b/g, 'Windsurf');
|
|
6586
|
+
return content;
|
|
6587
|
+
},
|
|
6588
|
+
},
|
|
6589
|
+
trae: {
|
|
6590
|
+
md: (content) => convertClaudeToTraeMarkdown(content),
|
|
6591
|
+
js: (content) => {
|
|
6592
|
+
content = content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
6593
|
+
return `/gsd-${commandName}`;
|
|
6594
|
+
});
|
|
6595
|
+
content = content.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
6596
|
+
content = content.replace(/CLAUDE\.md/g, '.trae/rules/');
|
|
6597
|
+
content = content.replace(/\bClaude Code\b/g, 'Trae');
|
|
6598
|
+
return content;
|
|
6599
|
+
},
|
|
6600
|
+
},
|
|
6601
|
+
cline: {
|
|
6602
|
+
md: (content) => convertClaudeToCliineMarkdown(content),
|
|
6603
|
+
js: (content) => {
|
|
6604
|
+
content = content.replace(/\.claude\/skills\//g, '.cline/skills/');
|
|
6605
|
+
content = content.replace(/CLAUDE\.md/g, '.clinerules');
|
|
6606
|
+
content = content.replace(/\bClaude Code\b/g, 'Cline');
|
|
6607
|
+
return content;
|
|
6608
|
+
},
|
|
6609
|
+
},
|
|
6610
|
+
qwen: {
|
|
6611
|
+
md: (content) => {
|
|
6612
|
+
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
6613
|
+
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
6614
|
+
content = content.replace(/\.claude\//g, '.qwen/');
|
|
6615
|
+
return content;
|
|
6616
|
+
},
|
|
6617
|
+
js: (content) => {
|
|
6618
|
+
content = content.replace(/\.claude\/skills\//g, '.qwen/skills/');
|
|
6619
|
+
content = content.replace(/\.claude\//g, '.qwen/');
|
|
6620
|
+
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
6621
|
+
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
6622
|
+
return content;
|
|
6623
|
+
},
|
|
6624
|
+
},
|
|
6625
|
+
hermes: {
|
|
6626
|
+
md: (content) => {
|
|
6627
|
+
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
6628
|
+
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
6629
|
+
content = content.replace(/\.claude\//g, '.hermes/');
|
|
6630
|
+
return content;
|
|
6631
|
+
},
|
|
6632
|
+
js: (content) => {
|
|
6633
|
+
content = content.replace(/\.claude\/skills\//g, '.hermes/skills/');
|
|
6634
|
+
content = content.replace(/\.claude\//g, '.hermes/');
|
|
6635
|
+
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
6636
|
+
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
6637
|
+
return content;
|
|
6638
|
+
},
|
|
6639
|
+
},
|
|
6640
|
+
};
|
|
7318
6641
|
|
|
7319
6642
|
/**
|
|
7320
6643
|
* Recursively copy directory, replacing paths in .md files
|
|
@@ -7326,22 +6649,28 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) {
|
|
|
7326
6649
|
* @param {boolean} isCommand - Whether the source is a command directory
|
|
7327
6650
|
* @param {boolean} isGlobal - Whether the install is global
|
|
7328
6651
|
*/
|
|
7329
|
-
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false) {
|
|
7330
|
-
const isOpencode = runtime === 'opencode';
|
|
7331
|
-
const isKilo = runtime === 'kilo';
|
|
7332
|
-
const isGemini = runtime === 'gemini';
|
|
7333
|
-
const isCodex = runtime === 'codex';
|
|
7334
|
-
const isCopilot = runtime === 'copilot';
|
|
7335
|
-
const isAntigravity = runtime === 'antigravity';
|
|
7336
|
-
const isCursor = runtime === 'cursor';
|
|
7337
|
-
const isWindsurf = runtime === 'windsurf';
|
|
7338
|
-
const isAugment = runtime === 'augment';
|
|
7339
|
-
const isTrae = runtime === 'trae';
|
|
7340
|
-
const isQwen = runtime === 'qwen';
|
|
7341
|
-
const isHermes = runtime === 'hermes';
|
|
7342
|
-
const isCline = runtime === 'cline';
|
|
6652
|
+
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false, confinementRoot) {
|
|
7343
6653
|
const dirName = getDirName(runtime);
|
|
7344
6654
|
|
|
6655
|
+
// ADR-1239 Phase B write-confinement: refuse to wipe/write a destDir that
|
|
6656
|
+
// escapes the caller-declared install root. Runs BEFORE the rmSync below so a
|
|
6657
|
+
// crafted destDir can never delete or write outside confinementRoot.
|
|
6658
|
+
if (confinementRoot === undefined) {
|
|
6659
|
+
throw new Error(
|
|
6660
|
+
'copyWithPathReplacement: confinementRoot is required to confine writes to the install root — refusing to write',
|
|
6661
|
+
);
|
|
6662
|
+
}
|
|
6663
|
+
const resolvedConfinementRoot = path.resolve(confinementRoot);
|
|
6664
|
+
const resolvedDestDir = assertDestWithinConfigHome(confinementRoot, destDir);
|
|
6665
|
+
if (hasExistingSymlinkBetween(resolvedConfinementRoot, resolvedDestDir)) {
|
|
6666
|
+
throw new Error(
|
|
6667
|
+
`copyWithPathReplacement: destDir "${destDir}" contains a symlink escaping the install root "${confinementRoot}" — refusing to write`,
|
|
6668
|
+
);
|
|
6669
|
+
}
|
|
6670
|
+
// Use the validated absolute path for all writes below so the gate validates
|
|
6671
|
+
// exactly what is written (a relative destDir would otherwise resolve to cwd).
|
|
6672
|
+
destDir = resolvedDestDir;
|
|
6673
|
+
|
|
7345
6674
|
// Clean install: remove existing destination to prevent orphaned files
|
|
7346
6675
|
if (fs.existsSync(destDir)) {
|
|
7347
6676
|
fs.rmSync(destDir, { recursive: true });
|
|
@@ -7355,12 +6684,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7355
6684
|
const destPath = path.join(destDir, entry.name);
|
|
7356
6685
|
|
|
7357
6686
|
if (entry.isDirectory()) {
|
|
7358
|
-
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal);
|
|
6687
|
+
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal, confinementRoot);
|
|
7359
6688
|
} else if (entry.name.endsWith('.md')) {
|
|
6689
|
+
const dispatch = RUNTIME_CONTENT_DISPATCH[runtime] || {};
|
|
6690
|
+
const ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName: entry.name, runtime };
|
|
6691
|
+
|
|
7360
6692
|
// Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths
|
|
7361
|
-
// Skip generic replacement for Copilot —
|
|
6693
|
+
// Skip generic replacement for Copilot/Antigravity — their converters handle all paths
|
|
7362
6694
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
7363
|
-
if (!
|
|
6695
|
+
if (!dispatch.mdSkipGenericRewrite) {
|
|
7364
6696
|
const globalClaudeRegex = /~\/\.claude\//g;
|
|
7365
6697
|
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
7366
6698
|
const localClaudeRegex = /\.\/\.claude\//g;
|
|
@@ -7395,112 +6727,25 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7395
6727
|
// colon-canonical runtimes (Gemini).
|
|
7396
6728
|
content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
|
|
7397
6729
|
|
|
7398
|
-
//
|
|
7399
|
-
if (
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
const
|
|
7411
|
-
fs.
|
|
7412
|
-
|
|
7413
|
-
content = convertClaudeToCodexMarkdown(content);
|
|
7414
|
-
fs.writeFileSync(destPath, content);
|
|
7415
|
-
} else if (isCopilot) {
|
|
7416
|
-
content = convertClaudeToCopilotContent(content, isGlobal);
|
|
7417
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7418
|
-
fs.writeFileSync(destPath, content);
|
|
7419
|
-
} else if (isAntigravity) {
|
|
7420
|
-
content = convertClaudeToAntigravityContent(content, isGlobal);
|
|
7421
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7422
|
-
fs.writeFileSync(destPath, content);
|
|
7423
|
-
} else if (isCursor) {
|
|
7424
|
-
content = convertClaudeToCursorMarkdown(content);
|
|
7425
|
-
fs.writeFileSync(destPath, content);
|
|
7426
|
-
} else if (isWindsurf) {
|
|
7427
|
-
content = convertClaudeToWindsurfMarkdown(content);
|
|
7428
|
-
fs.writeFileSync(destPath, content);
|
|
7429
|
-
} else if (isTrae) {
|
|
7430
|
-
content = convertClaudeToTraeMarkdown(content);
|
|
7431
|
-
fs.writeFileSync(destPath, content);
|
|
7432
|
-
} else if (isCline) {
|
|
7433
|
-
content = convertClaudeToCliineMarkdown(content);
|
|
7434
|
-
fs.writeFileSync(destPath, content);
|
|
7435
|
-
} else if (isQwen) {
|
|
7436
|
-
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
7437
|
-
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
7438
|
-
content = content.replace(/\.claude\//g, '.qwen/');
|
|
7439
|
-
fs.writeFileSync(destPath, content);
|
|
7440
|
-
} else if (isHermes) {
|
|
7441
|
-
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
7442
|
-
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
7443
|
-
content = content.replace(/\.claude\//g, '.hermes/');
|
|
6730
|
+
// Apply per-runtime .md converter (if any)
|
|
6731
|
+
if (dispatch.md) content = dispatch.md(content, ctx);
|
|
6732
|
+
|
|
6733
|
+
// Re-run attribution after converter for runtimes that need it (copilot, antigravity)
|
|
6734
|
+
if (dispatch.mdReattributeAfter) content = processAttribution(content, getCommitAttribution(runtime));
|
|
6735
|
+
|
|
6736
|
+
// Gemini: rename .md → .toml for command files
|
|
6737
|
+
const finalPath = (dispatch.mdTomlRenameOnCommand && isCommand) ? destPath.replace(/\.md$/, '.toml') : destPath;
|
|
6738
|
+
fs.writeFileSync(finalPath, content);
|
|
6739
|
+
} else if (entry.name.endsWith('.cjs') || entry.name.endsWith('.js')) {
|
|
6740
|
+
const dispatch = RUNTIME_CONTENT_DISPATCH[runtime] || {};
|
|
6741
|
+
if (dispatch.js) {
|
|
6742
|
+
const ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName: entry.name, runtime };
|
|
6743
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
6744
|
+
content = dispatch.js(content, ctx);
|
|
7444
6745
|
fs.writeFileSync(destPath, content);
|
|
7445
6746
|
} else {
|
|
7446
|
-
fs.
|
|
6747
|
+
fs.copyFileSync(srcPath, destPath);
|
|
7447
6748
|
}
|
|
7448
|
-
} else if (isCopilot && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7449
|
-
// Copilot: also transform .cjs/.js files for CONV-06 and CONV-07
|
|
7450
|
-
let content = fs.readFileSync(srcPath, 'utf8');
|
|
7451
|
-
content = convertClaudeToCopilotContent(content, isGlobal);
|
|
7452
|
-
fs.writeFileSync(destPath, content);
|
|
7453
|
-
} else if (isAntigravity && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7454
|
-
// Antigravity: also transform .cjs/.js files for path/command conversions
|
|
7455
|
-
let content = fs.readFileSync(srcPath, 'utf8');
|
|
7456
|
-
content = convertClaudeToAntigravityContent(content, isGlobal);
|
|
7457
|
-
fs.writeFileSync(destPath, content);
|
|
7458
|
-
} else if (isCursor && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7459
|
-
// For Cursor, also convert Claude references in JS/CJS utility scripts
|
|
7460
|
-
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
7461
|
-
jsContent = jsContent.replace(/gsd:/gi, 'gsd-');
|
|
7462
|
-
jsContent = jsContent.replace(/\.claude\/skills\//g, '.cursor/skills/');
|
|
7463
|
-
jsContent = jsContent.replace(/CLAUDE\.md/g, '.cursor/rules/');
|
|
7464
|
-
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor');
|
|
7465
|
-
fs.writeFileSync(destPath, jsContent);
|
|
7466
|
-
} else if (isWindsurf && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7467
|
-
// For Windsurf/Devin, also convert Claude references in JS/CJS utility scripts.
|
|
7468
|
-
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
|
|
7469
|
-
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
7470
|
-
jsContent = jsContent.replace(/gsd:/gi, 'gsd-');
|
|
7471
|
-
jsContent = jsContent.replace(/\.claude\/skills\//g, '.devin/skills/');
|
|
7472
|
-
jsContent = jsContent.replace(/CLAUDE\.md/g, '.devin/rules');
|
|
7473
|
-
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf');
|
|
7474
|
-
fs.writeFileSync(destPath, jsContent);
|
|
7475
|
-
} else if (isTrae && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7476
|
-
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
7477
|
-
jsContent = jsContent.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
7478
|
-
return `/gsd-${commandName}`;
|
|
7479
|
-
});
|
|
7480
|
-
jsContent = jsContent.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
7481
|
-
jsContent = jsContent.replace(/CLAUDE\.md/g, '.trae/rules/');
|
|
7482
|
-
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Trae');
|
|
7483
|
-
fs.writeFileSync(destPath, jsContent);
|
|
7484
|
-
} else if (isCline && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7485
|
-
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
7486
|
-
jsContent = jsContent.replace(/\.claude\/skills\//g, '.cline/skills/');
|
|
7487
|
-
jsContent = jsContent.replace(/CLAUDE\.md/g, '.clinerules');
|
|
7488
|
-
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cline');
|
|
7489
|
-
fs.writeFileSync(destPath, jsContent);
|
|
7490
|
-
} else if (isQwen && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7491
|
-
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
7492
|
-
jsContent = jsContent.replace(/\.claude\/skills\//g, '.qwen/skills/');
|
|
7493
|
-
jsContent = jsContent.replace(/\.claude\//g, '.qwen/');
|
|
7494
|
-
jsContent = jsContent.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
7495
|
-
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
7496
|
-
fs.writeFileSync(destPath, jsContent);
|
|
7497
|
-
} else if (isHermes && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
|
7498
|
-
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
|
7499
|
-
jsContent = jsContent.replace(/\.claude\/skills\//g, '.hermes/skills/');
|
|
7500
|
-
jsContent = jsContent.replace(/\.claude\//g, '.hermes/');
|
|
7501
|
-
jsContent = jsContent.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
7502
|
-
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
7503
|
-
fs.writeFileSync(destPath, jsContent);
|
|
7504
6749
|
} else {
|
|
7505
6750
|
fs.copyFileSync(srcPath, destPath);
|
|
7506
6751
|
}
|
|
@@ -7674,19 +6919,7 @@ const GSD_UNINSTALL_HOOKS = [
|
|
|
7674
6919
|
* @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex', 'copilot')
|
|
7675
6920
|
*/
|
|
7676
6921
|
function uninstall(isGlobal, runtime = 'claude') {
|
|
7677
|
-
const isOpencode = runtime
|
|
7678
|
-
const isKilo = runtime === 'kilo';
|
|
7679
|
-
const isGemini = runtime === 'gemini';
|
|
7680
|
-
const isCodex = runtime === 'codex';
|
|
7681
|
-
const isCopilot = runtime === 'copilot';
|
|
7682
|
-
const isAntigravity = runtime === 'antigravity';
|
|
7683
|
-
const isCursor = runtime === 'cursor';
|
|
7684
|
-
const isWindsurf = runtime === 'windsurf';
|
|
7685
|
-
const isAugment = runtime === 'augment';
|
|
7686
|
-
const isTrae = runtime === 'trae';
|
|
7687
|
-
const isQwen = runtime === 'qwen';
|
|
7688
|
-
const isHermes = runtime === 'hermes';
|
|
7689
|
-
const isCodebuddy = runtime === 'codebuddy';
|
|
6922
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
7690
6923
|
const dirName = getDirName(runtime);
|
|
7691
6924
|
|
|
7692
6925
|
// Get the target directory based on runtime and install type. Cline local
|
|
@@ -7702,21 +6935,9 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7702
6935
|
? targetDir.replace(os.homedir(), '~')
|
|
7703
6936
|
: targetDir.replace(process.cwd(), '.');
|
|
7704
6937
|
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
if (runtime === 'kilo') runtimeLabel = 'Kilo';
|
|
7709
|
-
if (runtime === 'codex') runtimeLabel = 'Codex';
|
|
7710
|
-
if (runtime === 'copilot') runtimeLabel = 'Copilot';
|
|
7711
|
-
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
|
|
7712
|
-
if (runtime === 'cursor') runtimeLabel = 'Cursor';
|
|
7713
|
-
if (runtime === 'windsurf') runtimeLabel = 'Windsurf';
|
|
7714
|
-
if (runtime === 'augment') runtimeLabel = 'Augment';
|
|
7715
|
-
if (runtime === 'trae') runtimeLabel = 'Trae';
|
|
7716
|
-
if (runtime === 'qwen') runtimeLabel = 'Qwen Code';
|
|
7717
|
-
if (runtime === 'hermes') runtimeLabel = 'Hermes Agent';
|
|
7718
|
-
if (runtime === 'kimi') runtimeLabel = 'Kimi CLI';
|
|
7719
|
-
if (runtime === 'codebuddy') runtimeLabel = 'CodeBuddy';
|
|
6938
|
+
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
|
6939
|
+
// Phase B / #1679) — collapses the prior 15-line assignment chain.
|
|
6940
|
+
const runtimeLabel = getRuntimeLabel(runtime);
|
|
7720
6941
|
|
|
7721
6942
|
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
7722
6943
|
|
|
@@ -8096,6 +7317,22 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
8096
7317
|
}
|
|
8097
7318
|
}
|
|
8098
7319
|
|
|
7320
|
+
// 4z. Remove the OpenCode native plugin adapter (#1914). Only GSD's own
|
|
7321
|
+
// plugin file is removed; the plugins/ dir is pruned only if it becomes
|
|
7322
|
+
// empty, preserving any user-authored OpenCode plugins.
|
|
7323
|
+
if (isOpencode) {
|
|
7324
|
+
const pluginsDir = path.join(targetDir, 'plugins');
|
|
7325
|
+
const pluginPath = path.join(pluginsDir, 'gsd-core.js');
|
|
7326
|
+
if (fs.existsSync(pluginPath)) {
|
|
7327
|
+
try {
|
|
7328
|
+
fs.unlinkSync(pluginPath);
|
|
7329
|
+
removedCount++;
|
|
7330
|
+
console.log(` ${green}✓${reset} Removed OpenCode plugin`);
|
|
7331
|
+
} catch (_) { /* best-effort */ }
|
|
7332
|
+
try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ }
|
|
7333
|
+
}
|
|
7334
|
+
}
|
|
7335
|
+
|
|
8099
7336
|
// 4a. Remove scripts/changeset/ and scripts/lib/ (#935)
|
|
8100
7337
|
// GSD-managed files only: enumerate the exact set the installer writes.
|
|
8101
7338
|
// Any file NOT in this set is user-owned and must survive uninstall.
|
|
@@ -8140,6 +7377,11 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
8140
7377
|
const fixSlashUninstallPath = path.join(targetDir, 'scripts', 'fix-slash-commands.cjs');
|
|
8141
7378
|
try { fs.unlinkSync(fixSlashUninstallPath); } catch (_) { /* best-effort */ }
|
|
8142
7379
|
|
|
7380
|
+
// Remove the capability registry generator scripts (#1920) — before the scripts/ rmdir
|
|
7381
|
+
for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) {
|
|
7382
|
+
try { fs.unlinkSync(path.join(targetDir, 'scripts', gen)); } catch (_) { /* best-effort */ }
|
|
7383
|
+
}
|
|
7384
|
+
|
|
8143
7385
|
// If scripts/ dir is now empty, remove it too
|
|
8144
7386
|
const scriptsUninstallDir = path.join(targetDir, 'scripts');
|
|
8145
7387
|
if (fs.existsSync(scriptsUninstallDir)) {
|
|
@@ -8482,7 +7724,7 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
8482
7724
|
modified = true;
|
|
8483
7725
|
}
|
|
8484
7726
|
|
|
8485
|
-
// Configure external_directory permission (the safety guard for paths outside
|
|
7727
|
+
// Configure external_directory permission (the safety guard for paths outside)
|
|
8486
7728
|
if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
|
|
8487
7729
|
config.permission.external_directory = {};
|
|
8488
7730
|
}
|
|
@@ -8491,6 +7733,25 @@ function configureOpencodePermissions(isGlobal = true, configDir = null) {
|
|
|
8491
7733
|
modified = true;
|
|
8492
7734
|
}
|
|
8493
7735
|
|
|
7736
|
+
// ADR-1239 Phase D / #1682 — register the companion MCP server (Phase 4) so
|
|
7737
|
+
// OpenCode connects to GSD's command (point 1) + state-IO (point 5) surface
|
|
7738
|
+
// with NO bespoke plugin. Idempotent + non-clobbering: only added when
|
|
7739
|
+
// `mcp.gsd` is absent (a user-defined `mcp.gsd` is respected — Hyrum's Law).
|
|
7740
|
+
// Local-stdio schema per OpenCode config (packages/core/src/config/mcp.ts).
|
|
7741
|
+
// `-p @opengsd/gsd-core` resolves the `gsd-mcp-server` bin from this package
|
|
7742
|
+
// (bin name != package name) regardless of global-install state.
|
|
7743
|
+
if (!config.mcp || typeof config.mcp !== 'object') {
|
|
7744
|
+
config.mcp = {};
|
|
7745
|
+
}
|
|
7746
|
+
if (config.mcp.gsd === undefined) {
|
|
7747
|
+
config.mcp.gsd = {
|
|
7748
|
+
type: 'local',
|
|
7749
|
+
command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
|
|
7750
|
+
enabled: true,
|
|
7751
|
+
};
|
|
7752
|
+
modified = true;
|
|
7753
|
+
}
|
|
7754
|
+
|
|
8494
7755
|
if (!modified) {
|
|
8495
7756
|
return; // Already configured
|
|
8496
7757
|
}
|
|
@@ -8676,45 +7937,14 @@ function resolveInstallRelativePath(baseDir, relPath) {
|
|
|
8676
7937
|
return { relPath: normalized, fullPath };
|
|
8677
7938
|
}
|
|
8678
7939
|
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
const resolvedFullPath = path.resolve(fullPath);
|
|
8682
|
-
if (resolvedFullPath !== resolvedRoot && !resolvedFullPath.startsWith(resolvedRoot + path.sep)) {
|
|
8683
|
-
return true;
|
|
8684
|
-
}
|
|
8685
|
-
|
|
8686
|
-
let cursor = resolvedRoot;
|
|
8687
|
-
if (fs.existsSync(cursor) && fs.lstatSync(cursor).isSymbolicLink()) {
|
|
8688
|
-
return true;
|
|
8689
|
-
}
|
|
8690
|
-
|
|
8691
|
-
const relative = path.relative(resolvedRoot, resolvedFullPath);
|
|
8692
|
-
for (const segment of relative.split(path.sep)) {
|
|
8693
|
-
if (!segment) continue;
|
|
8694
|
-
cursor = path.join(cursor, segment);
|
|
8695
|
-
if (!fs.existsSync(cursor)) return false;
|
|
8696
|
-
if (fs.lstatSync(cursor).isSymbolicLink()) return true;
|
|
8697
|
-
}
|
|
8698
|
-
|
|
8699
|
-
return false;
|
|
8700
|
-
}
|
|
7940
|
+
// hasExistingSymlinkBetween: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
7941
|
+
// Imported from installEngine above.
|
|
8701
7942
|
|
|
8702
7943
|
/**
|
|
8703
7944
|
* Write file manifest after installation for future modification detection
|
|
8704
7945
|
*/
|
|
8705
7946
|
function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
8706
|
-
const isOpencode = runtime
|
|
8707
|
-
const isKilo = runtime === 'kilo';
|
|
8708
|
-
const isGemini = runtime === 'gemini';
|
|
8709
|
-
const isCodex = runtime === 'codex';
|
|
8710
|
-
const isCopilot = runtime === 'copilot';
|
|
8711
|
-
const isAntigravity = runtime === 'antigravity';
|
|
8712
|
-
const isCursor = runtime === 'cursor';
|
|
8713
|
-
const isWindsurf = runtime === 'windsurf';
|
|
8714
|
-
const isTrae = runtime === 'trae';
|
|
8715
|
-
const isCline = runtime === 'cline';
|
|
8716
|
-
const isKimi = runtime === 'kimi';
|
|
8717
|
-
const isHermes = runtime === 'hermes';
|
|
7947
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
8718
7948
|
const gsdDir = path.join(configDir, 'gsd-core');
|
|
8719
7949
|
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
|
|
8720
7950
|
// commandsDir points to the old location for Gemini (which still uses commands/gsd/).
|
|
@@ -8875,6 +8105,24 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
8875
8105
|
manifest.files['scripts/fix-slash-commands.cjs'] = fileHash(fixSlashInstallPath);
|
|
8876
8106
|
}
|
|
8877
8107
|
|
|
8108
|
+
// Track the capability registry generator scripts (#1920) — top-level scripts/ files
|
|
8109
|
+
// not covered by the changeset/lib loops.
|
|
8110
|
+
for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) {
|
|
8111
|
+
const genInstallPath = path.join(configDir, 'scripts', gen);
|
|
8112
|
+
if (fs.existsSync(genInstallPath)) {
|
|
8113
|
+
manifest.files['scripts/' + gen] = fileHash(genInstallPath);
|
|
8114
|
+
}
|
|
8115
|
+
}
|
|
8116
|
+
|
|
8117
|
+
// Track the OpenCode native plugin adapter (#1914) so update/drift detection
|
|
8118
|
+
// and uninstall can account for it.
|
|
8119
|
+
if (isOpencode) {
|
|
8120
|
+
const pluginInstallPath = path.join(configDir, 'plugins', 'gsd-core.js');
|
|
8121
|
+
if (fs.existsSync(pluginInstallPath)) {
|
|
8122
|
+
manifest.files['plugins/gsd-core.js'] = fileHash(pluginInstallPath);
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
|
|
8878
8126
|
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
|
|
8879
8127
|
return manifest;
|
|
8880
8128
|
}
|
|
@@ -8943,7 +8191,7 @@ function populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathP
|
|
|
8943
8191
|
const srcDir = path.join(packageSrc, top);
|
|
8944
8192
|
const stageDir = path.join(stageRoot, top);
|
|
8945
8193
|
if (!fs.existsSync(srcDir)) continue;
|
|
8946
|
-
copyWithPathReplacement(srcDir, stageDir, pathPrefix, runtime, false, isGlobal);
|
|
8194
|
+
copyWithPathReplacement(srcDir, stageDir, pathPrefix, runtime, false, isGlobal, stageRoot);
|
|
8947
8195
|
}
|
|
8948
8196
|
|
|
8949
8197
|
for (const relPath of safeModified) {
|
|
@@ -9201,21 +8449,7 @@ function reportInstallerMigrationResult(result) {
|
|
|
9201
8449
|
}
|
|
9202
8450
|
|
|
9203
8451
|
function install(isGlobal, runtime = 'claude', options = {}) {
|
|
9204
|
-
const isOpencode = runtime
|
|
9205
|
-
const isGemini = runtime === 'gemini';
|
|
9206
|
-
const isKilo = runtime === 'kilo';
|
|
9207
|
-
const isKimi = runtime === 'kimi';
|
|
9208
|
-
const isCodex = runtime === 'codex';
|
|
9209
|
-
const isCopilot = runtime === 'copilot';
|
|
9210
|
-
const isAntigravity = runtime === 'antigravity';
|
|
9211
|
-
const isCursor = runtime === 'cursor';
|
|
9212
|
-
const isWindsurf = runtime === 'windsurf';
|
|
9213
|
-
const isAugment = runtime === 'augment';
|
|
9214
|
-
const isTrae = runtime === 'trae';
|
|
9215
|
-
const isQwen = runtime === 'qwen';
|
|
9216
|
-
const isHermes = runtime === 'hermes';
|
|
9217
|
-
const isCodebuddy = runtime === 'codebuddy';
|
|
9218
|
-
const isCline = runtime === 'cline';
|
|
8452
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
9219
8453
|
const plan = resolveInstallPlan(runtime);
|
|
9220
8454
|
const dirName = getDirName(runtime);
|
|
9221
8455
|
const src = path.join(__dirname, '..');
|
|
@@ -9361,22 +8595,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9361
8595
|
homeDir,
|
|
9362
8596
|
});
|
|
9363
8597
|
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
if (isKilo) runtimeLabel = 'Kilo';
|
|
9368
|
-
if (isCodex) runtimeLabel = 'Codex';
|
|
9369
|
-
if (isCopilot) runtimeLabel = 'Copilot';
|
|
9370
|
-
if (isAntigravity) runtimeLabel = 'Antigravity';
|
|
9371
|
-
if (isCursor) runtimeLabel = 'Cursor';
|
|
9372
|
-
if (isWindsurf) runtimeLabel = 'Windsurf';
|
|
9373
|
-
if (isAugment) runtimeLabel = 'Augment';
|
|
9374
|
-
if (isTrae) runtimeLabel = 'Trae';
|
|
9375
|
-
if (isQwen) runtimeLabel = 'Qwen Code';
|
|
9376
|
-
if (isHermes) runtimeLabel = 'Hermes Agent';
|
|
9377
|
-
if (isKimi) runtimeLabel = 'Kimi';
|
|
9378
|
-
if (isCodebuddy) runtimeLabel = 'CodeBuddy';
|
|
9379
|
-
if (isCline) runtimeLabel = 'Cline';
|
|
8598
|
+
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
|
8599
|
+
// Phase B / #1679) — collapses the prior 16-line assignment chain.
|
|
8600
|
+
const runtimeLabel = getRuntimeLabel(runtime);
|
|
9380
8601
|
|
|
9381
8602
|
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
9382
8603
|
|
|
@@ -9671,7 +8892,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9671
8892
|
if (_isSkillsRuntime) {
|
|
9672
8893
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
9673
8894
|
const scope = isGlobal ? 'global' : 'local';
|
|
9674
|
-
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile);
|
|
8895
|
+
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution);
|
|
9675
8896
|
|
|
9676
8897
|
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
9677
8898
|
// gsd-* skill dirs. Prior installs wrote these files so Codex would show a
|
|
@@ -9831,12 +9052,45 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9831
9052
|
// Kilo support native, on-demand skills in addition to flat commands — see
|
|
9832
9053
|
// resolveRuntimeArtifactLayout's opencode/kilo entries. Derive skills from
|
|
9833
9054
|
// the SAME staged command set (gsdSrc) so both surfaces match exactly. (#784)
|
|
9834
|
-
const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix);
|
|
9055
|
+
const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix, getCommitAttribution);
|
|
9835
9056
|
if (_skillCount > 0) {
|
|
9836
9057
|
console.log(` ${green}✓${reset} Installed ${_skillCount} skills to skills/`);
|
|
9837
9058
|
} else {
|
|
9838
9059
|
failures.push('skills/gsd-*');
|
|
9839
9060
|
}
|
|
9061
|
+
|
|
9062
|
+
// OpenCode-only: install the native plugin adapter (#1914). OpenCode
|
|
9063
|
+
// declares hooksSurface: 'none', so GSD's lifecycle hooks are never
|
|
9064
|
+
// registered as settings.json hooks the way Claude Code does — the hook
|
|
9065
|
+
// *scripts* ship to <configDir>/hooks/ but nothing invokes them. This
|
|
9066
|
+
// plugin bridges OpenCode's event bus onto those existing hook scripts
|
|
9067
|
+
// (prompt guard, read guard, injection scanner, context monitor, ...),
|
|
9068
|
+
// spawning them as subprocesses. OpenCode auto-discovers plugin files under
|
|
9069
|
+
// <configDir>/plugins/ at startup — no opencode.json registration needed
|
|
9070
|
+
// (its `plugin` array is for npm packages, not local file paths).
|
|
9071
|
+
//
|
|
9072
|
+
// The file MUST land as `.js`: OpenCode's loader globs
|
|
9073
|
+
// `{plugin,plugins}/*.{ts,js}` (verified against its source) — a `.cjs`
|
|
9074
|
+
// extension would never be discovered. The config dir carries a
|
|
9075
|
+
// `{"type":"commonjs"}` package.json (written above), so the `.js` file is
|
|
9076
|
+
// interpreted as CommonJS, matching the adapter's module.exports/require.
|
|
9077
|
+
// Kilo has no plugin surface, so this is gated to OpenCode only.
|
|
9078
|
+
if (isOpencode) {
|
|
9079
|
+
const pluginSrc = path.join(src, '.opencode', 'plugins', 'gsd-core.js');
|
|
9080
|
+
const pluginDestDir = path.join(targetDir, 'plugins');
|
|
9081
|
+
const pluginDest = path.join(pluginDestDir, 'gsd-core.js');
|
|
9082
|
+
if (fs.existsSync(pluginSrc)) {
|
|
9083
|
+
fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
9084
|
+
fs.copyFileSync(pluginSrc, pluginDest);
|
|
9085
|
+
if (fs.existsSync(pluginDest)) {
|
|
9086
|
+
console.log(` ${green}✓${reset} Installed OpenCode plugin (bridges GSD hooks)`);
|
|
9087
|
+
} else {
|
|
9088
|
+
failures.push('plugins/gsd-core.js');
|
|
9089
|
+
}
|
|
9090
|
+
} else {
|
|
9091
|
+
failures.push('plugins/gsd-core.js');
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9840
9094
|
} else if (isCline) {
|
|
9841
9095
|
// Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
|
|
9842
9096
|
// No skills/commands directory needed for local installs.
|
|
@@ -9886,7 +9140,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9886
9140
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
9887
9141
|
const gsdSrc = _stageSkills(_commandsDir);
|
|
9888
9142
|
const gsdDest = path.join(commandsDir, 'gsd');
|
|
9889
|
-
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
|
|
9143
|
+
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal, targetDir);
|
|
9890
9144
|
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
|
9891
9145
|
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
|
9892
9146
|
} else {
|
|
@@ -9968,7 +9222,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9968
9222
|
const skillSrc = path.join(src, 'gsd-core');
|
|
9969
9223
|
const skillDest = path.join(targetDir, 'gsd-core');
|
|
9970
9224
|
const savedGsdArtifacts = preserveUserArtifacts(skillDest, USER_OWNED_ARTIFACTS);
|
|
9971
|
-
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal);
|
|
9225
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
9972
9226
|
restoreUserArtifacts(skillDest, savedGsdArtifacts);
|
|
9973
9227
|
if (verifyInstalled(skillDest, 'gsd-core')) {
|
|
9974
9228
|
console.log(` ${green}✓${reset} Installed workflow assets`);
|
|
@@ -9988,7 +9242,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9988
9242
|
const commandsSrc = path.join(src, 'commands', 'gsd');
|
|
9989
9243
|
const commandsDest = path.join(skillDest, 'commands', 'gsd');
|
|
9990
9244
|
if (fs.existsSync(commandsSrc)) {
|
|
9991
|
-
copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal);
|
|
9245
|
+
copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal, targetDir);
|
|
9992
9246
|
console.log(` ${green}✓${reset} Installed command bodies to gsd-core/commands/gsd/ (workflow delegation targets)`);
|
|
9993
9247
|
}
|
|
9994
9248
|
}
|
|
@@ -10030,11 +9284,24 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10030
9284
|
agentsSrc = _stageAgents(path.join(src, 'agents'));
|
|
10031
9285
|
const agentsDest = path.join(targetDir, 'agents');
|
|
10032
9286
|
|
|
9287
|
+
// ADR-1235 §1: runtimes that have been migrated to the descriptor-driven agent
|
|
9288
|
+
// path (installRuntimeArtifacts → convertedAgentsKind). The descriptor path
|
|
9289
|
+
// applies path-rewrite + attribution + converter + normalize via
|
|
9290
|
+
// stageAgentsForRuntimeWithConverter (with agentCtx pre-converter threading) in
|
|
9291
|
+
// createRuntimeArtifactInstallPlan. Their agents are already written ABOVE
|
|
9292
|
+
// (by installRuntimeArtifacts at line 8912), which also performs its own
|
|
9293
|
+
// stale-file prune pass. The inline stale-removal + inline loop both skip them.
|
|
9294
|
+
// Trivial group (cursor/windsurf/augment/trae/codebuddy) cut over together.
|
|
9295
|
+
// cline is excluded: it takes a rules-only local branch and has a local/global
|
|
9296
|
+
// complication that the descriptor-driven path does not handle correctly.
|
|
9297
|
+
const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy']);
|
|
9298
|
+
|
|
10033
9299
|
// Always remove stale gsd-* agents first so re-installing with
|
|
10034
9300
|
// `--minimal` actually shrinks a previously-full install.
|
|
10035
9301
|
// For Codex this also covers per-agent `.toml` files alongside the `.md`
|
|
10036
9302
|
// sources so a full → minimal switch doesn't leave stale registrations.
|
|
10037
|
-
|
|
9303
|
+
// Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes).
|
|
9304
|
+
if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && fs.existsSync(agentsDest)) {
|
|
10038
9305
|
for (const file of fs.readdirSync(agentsDest)) {
|
|
10039
9306
|
if (
|
|
10040
9307
|
file.startsWith('gsd-') &&
|
|
@@ -10047,6 +9314,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10047
9314
|
|
|
10048
9315
|
if (isKimi) {
|
|
10049
9316
|
console.log(` ${dim}↳${reset} Kimi custom agent YAML/prompt artifacts were installed via runtime artifact layout`);
|
|
9317
|
+
} else if (_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime)) {
|
|
9318
|
+
// installRuntimeArtifacts already wrote agents + handles stale-file cleanup
|
|
9319
|
+
// via its own prune pass. No further action needed.
|
|
9320
|
+
console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
|
|
10050
9321
|
} else if (isMinimalMode(_effectiveInstallMode)) {
|
|
10051
9322
|
// Codex registers agents in `config.toml` via `[agents.gsd-*]` sections.
|
|
10052
9323
|
// Without stripping them here, a full → minimal reinstall would leave the
|
|
@@ -10372,6 +9643,31 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10372
9643
|
}
|
|
10373
9644
|
}
|
|
10374
9645
|
|
|
9646
|
+
// Copy scripts/gen-capability-registry.cjs + scripts/gen-loop-host-contract.cjs —
|
|
9647
|
+
// required by gsd-core/bin/lib/capability-loader.cjs at overlay-composition time via
|
|
9648
|
+
// require('../../../scripts/gen-capability-registry.cjs') (which itself requires
|
|
9649
|
+
// gen-loop-host-contract.cjs). Without these, the loader's never-crash invariant
|
|
9650
|
+
// discards EVERY third-party capability overlay and silently falls back to the frozen
|
|
9651
|
+
// first-party registry, so installed capabilities are inert (#1920). Same class of
|
|
9652
|
+
// gap as #1223 (fix-slash-commands.cjs) and copied unconditionally for the same reason:
|
|
9653
|
+
// any runtime that installs gsd-core/ needs the capability system to compose.
|
|
9654
|
+
{
|
|
9655
|
+
const capGenDestDir = path.join(targetDir, 'scripts');
|
|
9656
|
+
fs.mkdirSync(capGenDestDir, { recursive: true });
|
|
9657
|
+
for (const gen of ['gen-capability-registry.cjs', 'gen-loop-host-contract.cjs']) {
|
|
9658
|
+
const genSrc = path.join(src, 'scripts', gen);
|
|
9659
|
+
const genDest = path.join(capGenDestDir, gen);
|
|
9660
|
+
if (!fs.existsSync(genSrc)) {
|
|
9661
|
+
failures.push(`scripts/${gen} (source missing from package — reinstall from npm)`);
|
|
9662
|
+
} else {
|
|
9663
|
+
fs.copyFileSync(genSrc, genDest);
|
|
9664
|
+
if (!verifyFileInstalled(genDest, `scripts/${gen}`)) {
|
|
9665
|
+
failures.push(`scripts/${gen}`);
|
|
9666
|
+
}
|
|
9667
|
+
}
|
|
9668
|
+
}
|
|
9669
|
+
}
|
|
9670
|
+
|
|
10375
9671
|
// Remove legacy get-shit-done-cc artifacts and stale update caches (#607).
|
|
10376
9672
|
// cleanupLegacyGsdCc handles both the legacy shared cache and the per-package
|
|
10377
9673
|
// cache (formerly an inline unlinkSync here). A cleanup failure must never
|
|
@@ -11217,14 +10513,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
11217
10513
|
* Apply statusline config, then print completion message
|
|
11218
10514
|
*/
|
|
11219
10515
|
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true, configDir = null, bannerOpts = {}) {
|
|
11220
|
-
const isOpencode = runtime
|
|
11221
|
-
const isKilo = runtime === 'kilo';
|
|
11222
|
-
const isCodex = runtime === 'codex';
|
|
11223
|
-
const isCopilot = runtime === 'copilot';
|
|
11224
|
-
const isCursor = runtime === 'cursor';
|
|
11225
|
-
const isWindsurf = runtime === 'windsurf';
|
|
11226
|
-
const isTrae = runtime === 'trae';
|
|
11227
|
-
const isCline = runtime === 'cline';
|
|
10516
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
11228
10517
|
const plan = resolveInstallPlan(runtime);
|
|
11229
10518
|
|
|
11230
10519
|
if (shouldInstallStatusline && plan.writesSharedSettings && !isOpencode) {
|
|
@@ -11347,37 +10636,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
11347
10636
|
}
|
|
11348
10637
|
}
|
|
11349
10638
|
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
11353
|
-
|
|
11354
|
-
|
|
11355
|
-
if (runtime === 'copilot') program = 'Copilot';
|
|
11356
|
-
if (runtime === 'antigravity') program = 'Antigravity';
|
|
11357
|
-
if (runtime === 'cursor') program = 'Cursor';
|
|
11358
|
-
if (runtime === 'windsurf') program = 'Windsurf';
|
|
11359
|
-
if (runtime === 'augment') program = 'Augment';
|
|
11360
|
-
if (runtime === 'trae') program = 'Trae';
|
|
11361
|
-
if (runtime === 'cline') program = 'Cline';
|
|
11362
|
-
if (runtime === 'qwen') program = 'Qwen Code';
|
|
11363
|
-
if (runtime === 'hermes') program = 'Hermes Agent';
|
|
11364
|
-
if (runtime === 'kimi') program = 'Kimi CLI';
|
|
11365
|
-
|
|
11366
|
-
let command = '/gsd-new-project';
|
|
11367
|
-
if (runtime === 'opencode') command = '/gsd-new-project';
|
|
11368
|
-
if (runtime === 'kilo') command = '/gsd-new-project';
|
|
11369
|
-
if (runtime === 'gemini') command = '/gsd:new-project';
|
|
11370
|
-
if (runtime === 'codex') command = '$gsd-new-project';
|
|
11371
|
-
if (runtime === 'copilot') command = '/gsd-new-project';
|
|
11372
|
-
if (runtime === 'antigravity') command = '/gsd-new-project';
|
|
11373
|
-
if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)';
|
|
11374
|
-
if (runtime === 'windsurf') command = '/gsd-new-project';
|
|
11375
|
-
if (runtime === 'augment') command = '/gsd-new-project';
|
|
11376
|
-
if (runtime === 'trae') command = '/gsd-new-project';
|
|
11377
|
-
if (runtime === 'cline') command = '/gsd-new-project';
|
|
11378
|
-
if (runtime === 'qwen') command = '/gsd-new-project';
|
|
11379
|
-
if (runtime === 'hermes') command = '/gsd-new-project';
|
|
11380
|
-
if (runtime === 'kimi') command = '/skill:gsd-new-project';
|
|
10639
|
+
// program + command are now single-source lookups (ADR-1239 Phase B / #1679):
|
|
10640
|
+
// program is the runtime display label; command is the per-host /gsd-new-project
|
|
10641
|
+
// invocation syntax.
|
|
10642
|
+
const program = getRuntimeLabel(runtime);
|
|
10643
|
+
const command = getRuntimeNewProjectCommand(runtime);
|
|
11381
10644
|
|
|
11382
10645
|
// Claude Code global installs use the skills/ format (CC 2.1.88+).
|
|
11383
10646
|
// Restart is required for CC to pick up newly-installed skills, and the
|
|
@@ -12404,6 +11667,8 @@ module.exports = {
|
|
|
12404
11667
|
// runtimeArtifactConversion spread (#1559).
|
|
12405
11668
|
processAttribution,
|
|
12406
11669
|
applyRuntimeContentRewritesForCommandsInPlace,
|
|
11670
|
+
_copyStaged,
|
|
11671
|
+
copyWithPathReplacement,
|
|
12407
11672
|
};
|
|
12408
11673
|
|
|
12409
11674
|
// Main logic — only run when not loaded as a module for testing
|