@opengsd/gsd-core 1.6.0 → 1.7.0-rc.1
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/plugin.json +1 -1
- package/agents/gsd-verifier.md +1 -0
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +293 -1145
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +116 -1
- 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 +18 -0
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +889 -82
- package/gsd-core/bin/lib/capability-source.cjs +4 -4
- package/gsd-core/bin/lib/capability-validator.cjs +198 -0
- 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/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/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration.cjs +408 -0
- package/gsd-core/bin/lib/init.cjs +1 -1
- 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 +27 -30
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase.cjs +41 -72
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +91 -1
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +17 -3
- 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 +37 -1
- 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 +1588 -0
- package/gsd-core/bin/lib/state.cjs +306 -681
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +9 -9
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/review.md +26 -0
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +4 -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/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'));
|
|
@@ -375,6 +376,31 @@ const {
|
|
|
375
376
|
updateCacheFileName,
|
|
376
377
|
} = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'package-identity.cjs'));
|
|
377
378
|
|
|
379
|
+
// ADR-1239 Phase B: runtime-artifact install cluster extracted to install-engine.cjs.
|
|
380
|
+
// getCommitAttribution STAYS here (impure install-time config I/O); it is injected
|
|
381
|
+
// into the engine functions via the resolveAttribution parameter at each call site.
|
|
382
|
+
const installEngine = require(path.join(_gsdLibDir, 'install-engine.cjs'));
|
|
383
|
+
const {
|
|
384
|
+
installRuntimeArtifacts,
|
|
385
|
+
uninstallRuntimeArtifacts,
|
|
386
|
+
installOpencodeFamilySkills,
|
|
387
|
+
_copyStaged,
|
|
388
|
+
hasExistingSymlinkBetween,
|
|
389
|
+
preserveUserArtifacts,
|
|
390
|
+
restoreUserArtifacts,
|
|
391
|
+
migrateLegacyDevPreferencesToSkill,
|
|
392
|
+
applyOpencodeFamilyPathPrefix,
|
|
393
|
+
convertClaudeCommandToOpencodeSkill,
|
|
394
|
+
convertClaudeCommandToKiloSkill,
|
|
395
|
+
USER_OWNED_ARTIFACTS,
|
|
396
|
+
_runLegacyInstallMigrations,
|
|
397
|
+
_runLegacyUninstallCleanup,
|
|
398
|
+
_removeGsdEntries,
|
|
399
|
+
_snapshotDir,
|
|
400
|
+
_restoreDir,
|
|
401
|
+
_removeHermesBareStemDirs,
|
|
402
|
+
} = installEngine;
|
|
403
|
+
|
|
378
404
|
// Parse args
|
|
379
405
|
const args = process.argv.slice(2);
|
|
380
406
|
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
@@ -484,18 +510,11 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
|
|
484
510
|
// Local installs use the same dir name pattern
|
|
485
511
|
return `'${getDirName(runtime)}'`;
|
|
486
512
|
}
|
|
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'";
|
|
513
|
+
// Global installs. antigravity's home is resolved dynamically (env-overridable,
|
|
514
|
+
// multi-segment via resolveAntigravityGlobalDir + path.relative) — not a table
|
|
515
|
+
// entry. (The prior inner `if (!isGlobal) return "'.agents'"` was unreachable:
|
|
516
|
+
// !isGlobal returns at the top of this function.)
|
|
497
517
|
if (runtime === 'antigravity') {
|
|
498
|
-
if (!isGlobal) return "'.agents'";
|
|
499
518
|
const antigravityDir = resolveAntigravityGlobalDir();
|
|
500
519
|
const rel = path.relative(os.homedir(), antigravityDir);
|
|
501
520
|
const segments = rel.split(path.sep).filter(Boolean);
|
|
@@ -506,16 +525,9 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
|
|
506
525
|
// stable legacy template so generated path.join() calls remain valid.
|
|
507
526
|
return "'.gemini', 'antigravity'";
|
|
508
527
|
}
|
|
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'";
|
|
528
|
+
// All other runtimes: single source-of-truth fragment table (ADR-1239 Phase B,
|
|
529
|
+
// #1679). claude/unknown fall through to the table's default '.claude'.
|
|
530
|
+
return getGlobalConfigHomeFragment(runtime);
|
|
519
531
|
}
|
|
520
532
|
|
|
521
533
|
/**
|
|
@@ -630,6 +642,15 @@ const processAttribution = runtimeArtifactConversion.processAttribution;
|
|
|
630
642
|
const computePathPrefix = runtimeArtifactConversion._computePathPrefix;
|
|
631
643
|
const applyRuntimeContentRewritesInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesInPlace;
|
|
632
644
|
const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace;
|
|
645
|
+
// #1675 (ADR-1508): the augment converter family is single-sourced in the
|
|
646
|
+
// conversion module. install.js re-binds (does not re-define) these so there
|
|
647
|
+
// is exactly one body — the generative-drift hazard the dedup removes. The two
|
|
648
|
+
// private helpers (getAugmentSkillAdapterHeader, convertSlashCommandsToAugmentSkillMentions)
|
|
649
|
+
// live only in the conversion module now; they are no longer duplicated here.
|
|
650
|
+
// (All call sites are below this line → no TDZ hazard.)
|
|
651
|
+
const convertClaudeToAugmentMarkdown = runtimeArtifactConversion.convertClaudeToAugmentMarkdown;
|
|
652
|
+
const convertClaudeCommandToAugmentSkill = runtimeArtifactConversion.convertClaudeCommandToAugmentSkill;
|
|
653
|
+
const convertClaudeAgentToAugmentAgent = runtimeArtifactConversion.convertClaudeAgentToAugmentAgent;
|
|
633
654
|
|
|
634
655
|
function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
635
656
|
return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
|
|
@@ -2580,105 +2601,14 @@ const claudeToAugmentTools = {
|
|
|
2580
2601
|
TodoWrite: 'add_tasks',
|
|
2581
2602
|
};
|
|
2582
2603
|
|
|
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
|
-
*/
|
|
2604
|
+
// #1675 (ADR-1508): the augment converter family below was a byte-identical
|
|
2605
|
+
// duplicate of runtime-artifact-conversion.cjs:
|
|
2606
|
+
// convertSlashCommandsToAugmentSkillMentions, convertClaudeToAugmentMarkdown,
|
|
2607
|
+
// getAugmentSkillAdapterHeader, convertClaudeCommandToAugmentSkill,
|
|
2608
|
+
// convertClaudeAgentToAugmentAgent
|
|
2609
|
+
// Deleted here and bound from runtimeArtifactConversion above (single source).
|
|
2610
|
+
// The DEFECT.GENERATIVE-FIX parity guard in
|
|
2611
|
+
// tests/enh-1511-rewrite-engine-relocation.test.cjs asserts reference identity.
|
|
2682
2612
|
|
|
2683
2613
|
function convertSlashCommandsToTraeSkillMentions(content) {
|
|
2684
2614
|
return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
@@ -5710,8 +5640,21 @@ function writeCopilotHookConfig(targetDir) {
|
|
|
5710
5640
|
* Reads agent .md files from source, extracts metadata, writes .toml configs.
|
|
5711
5641
|
*/
|
|
5712
5642
|
function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-sandbox') {
|
|
5713
|
-
|
|
5714
|
-
const
|
|
5643
|
+
// ADR-1239 Phase B write-confinement: every Codex config write stays under targetDir.
|
|
5644
|
+
const configPath = assertDestWithinConfigHome(targetDir, 'config.toml');
|
|
5645
|
+
const agentsTomlDir = assertDestWithinConfigHome(targetDir, 'agents');
|
|
5646
|
+
const resolvedTargetRoot = path.resolve(targetDir);
|
|
5647
|
+
// Symlink-escape guard (parity with _copyStaged / copyWithPathReplacement): the
|
|
5648
|
+
// lexical gate above does not resolve symlinks, so a pre-existing config.toml or
|
|
5649
|
+
// agents/ symlink could redirect writes outside targetDir. Reject those.
|
|
5650
|
+
if (
|
|
5651
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, configPath) ||
|
|
5652
|
+
hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir))
|
|
5653
|
+
) {
|
|
5654
|
+
throw new Error(
|
|
5655
|
+
`installCodexConfig: a Codex config path under "${targetDir}" contains a symlink escaping the install root — refusing to write`,
|
|
5656
|
+
);
|
|
5657
|
+
}
|
|
5715
5658
|
fs.mkdirSync(agentsTomlDir, { recursive: true });
|
|
5716
5659
|
|
|
5717
5660
|
const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
|
|
@@ -5756,7 +5699,16 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
|
|
|
5756
5699
|
// follows the same config-driven precedence as the Claude .md effort key.
|
|
5757
5700
|
const effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
|
5758
5701
|
const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg, sandboxTier);
|
|
5759
|
-
|
|
5702
|
+
// Confine the per-agent write to the agents/ dir itself: a crafted agent
|
|
5703
|
+
// `name` containing path separators must not escape agents/ (which would let
|
|
5704
|
+
// it clobber config.toml or write elsewhere under the configHome).
|
|
5705
|
+
const agentTomlPath = assertDestWithinConfigHome(agentsTomlDir, `${name}.toml`);
|
|
5706
|
+
if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath)) {
|
|
5707
|
+
throw new Error(
|
|
5708
|
+
`installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink escaping the install root — refusing to write`,
|
|
5709
|
+
);
|
|
5710
|
+
}
|
|
5711
|
+
fs.writeFileSync(agentTomlPath, tomlContent);
|
|
5760
5712
|
}
|
|
5761
5713
|
|
|
5762
5714
|
const gsdBlock = generateCodexConfigBlock(agents, targetDir);
|
|
@@ -6326,69 +6278,9 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
|
|
|
6326
6278
|
return `---\n${newFrontmatter}\n---${body}`;
|
|
6327
6279
|
}
|
|
6328
6280
|
|
|
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
|
-
}
|
|
6281
|
+
// convertClaudeCommandToOpencodeFamilySkill, convertClaudeCommandToOpencodeSkill,
|
|
6282
|
+
// convertClaudeCommandToKiloSkill: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6283
|
+
// Imported from installEngine above.
|
|
6392
6284
|
|
|
6393
6285
|
/**
|
|
6394
6286
|
* Convert Claude Code markdown command to Gemini TOML format
|
|
@@ -6471,30 +6363,8 @@ function convertClaudeToGeminiToml(content, { commandName = null } = {}) {
|
|
|
6471
6363
|
* @param {string} pathPrefix - Path prefix for file references
|
|
6472
6364
|
* @param {string} runtime - Target runtime ('claude', 'opencode', or 'kilo')
|
|
6473
6365
|
*/
|
|
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
|
-
}
|
|
6366
|
+
// applyOpencodeFamilyPathPrefix: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6367
|
+
// Imported from installEngine above.
|
|
6498
6368
|
|
|
6499
6369
|
function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
6500
6370
|
if (!fs.existsSync(srcDir)) {
|
|
@@ -6633,688 +6503,140 @@ function writeHermesCategoryDescription(categoryDir) {
|
|
|
6633
6503
|
* @param {boolean} isGlobal - Whether this is a global install
|
|
6634
6504
|
*/
|
|
6635
6505
|
|
|
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
|
-
}
|
|
6506
|
+
// USER_OWNED_ARTIFACTS, preserveUserArtifacts, restoreUserArtifacts,
|
|
6507
|
+
// migrateLegacyDevPreferencesToSkill, _copyStaged, _removeGsdEntries,
|
|
6508
|
+
// _runLegacyInstallMigrations, _runLegacyUninstallCleanup, _snapshotDir,
|
|
6509
|
+
// _restoreDir, _removeHermesBareStemDirs, installRuntimeArtifacts,
|
|
6510
|
+
// installOpencodeFamilySkills, uninstallRuntimeArtifacts:
|
|
6511
|
+
// ALL moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
6512
|
+
// Imported from installEngine above.
|
|
6733
6513
|
|
|
6734
6514
|
// ---------------------------------------------------------------------------
|
|
6735
|
-
// Phase 2 — Layout-driven install/uninstall orchestrators
|
|
6515
|
+
// Phase 2 — Layout-driven install/uninstall orchestrators (moved to engine)
|
|
6516
|
+
// _applyRuntimeRewrites / _stampNonClaudeRuntimeDefaults remain here for
|
|
6517
|
+
// call sites in copyWithPathReplacement (not moved).
|
|
6736
6518
|
// ---------------------------------------------------------------------------
|
|
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
6519
|
const _applyRuntimeRewrites = runtimeArtifactConversion._applyRuntimeRewrites;
|
|
6796
6520
|
const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaudeRuntimeDefaults;
|
|
6797
6521
|
|
|
6798
6522
|
/**
|
|
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
|
-
}
|
|
6523
|
+
* Data-driven dispatch table for copyWithPathReplacement (ADR-1239 Phase B).
|
|
6524
|
+
* Keyed by runtime id. Each entry declares ONLY what that runtime does differently.
|
|
6525
|
+
* The DEFAULT (no entry, or entry with no md/js key) = identity transform after
|
|
6526
|
+
* the uniform steps — covers claude, augment, codebuddy, kimi, etc.
|
|
6527
|
+
*
|
|
6528
|
+
* Entry shape:
|
|
6529
|
+
* mdSkipGenericRewrite?: boolean — skip the ~/.claude/ rewrite block (copilot, antigravity)
|
|
6530
|
+
* md?: (content, ctx) => string — per-runtime .md transform
|
|
6531
|
+
* mdReattributeAfter?: boolean — re-run processAttribution after md() (copilot, antigravity)
|
|
6532
|
+
* mdTomlRenameOnCommand?: boolean — when isCommand, rename dest .md → .toml (gemini)
|
|
6533
|
+
* js?: (content, ctx) => string — per-runtime .cjs/.js transform (absent = plain copyFileSync)
|
|
6534
|
+
*
|
|
6535
|
+
* ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName, runtime }
|
|
6536
|
+
*/
|
|
6537
|
+
const RUNTIME_CONTENT_DISPATCH = {
|
|
6538
|
+
opencode: {
|
|
6539
|
+
md: (content) => convertClaudeToOpencodeFrontmatter(content),
|
|
6540
|
+
},
|
|
6541
|
+
kilo: {
|
|
6542
|
+
md: (content) => convertClaudeToKiloFrontmatter(content),
|
|
6543
|
+
},
|
|
6544
|
+
gemini: {
|
|
6545
|
+
md: (content, ctx) =>
|
|
6546
|
+
convertClaudeToGeminiMarkdown(content, {
|
|
6547
|
+
isCommand: ctx.isCommand,
|
|
6548
|
+
commandName: ctx.isCommand ? ctx.entryName.replace(/\.md$/, '') : null,
|
|
6549
|
+
}),
|
|
6550
|
+
mdTomlRenameOnCommand: true,
|
|
6551
|
+
},
|
|
6552
|
+
codex: {
|
|
6553
|
+
md: (content) => convertClaudeToCodexMarkdown(content),
|
|
6554
|
+
},
|
|
6555
|
+
copilot: {
|
|
6556
|
+
mdSkipGenericRewrite: true,
|
|
6557
|
+
md: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal),
|
|
6558
|
+
mdReattributeAfter: true,
|
|
6559
|
+
js: (content, ctx) => convertClaudeToCopilotContent(content, ctx.isGlobal),
|
|
6560
|
+
},
|
|
6561
|
+
antigravity: {
|
|
6562
|
+
mdSkipGenericRewrite: true,
|
|
6563
|
+
md: (content, ctx) => convertClaudeToAntigravityContent(content, ctx.isGlobal),
|
|
6564
|
+
mdReattributeAfter: true,
|
|
6565
|
+
js: (content, ctx) => convertClaudeToAntigravityContent(content, ctx.isGlobal),
|
|
6566
|
+
},
|
|
6567
|
+
cursor: {
|
|
6568
|
+
md: (content) => convertClaudeToCursorMarkdown(content),
|
|
6569
|
+
js: (content) => {
|
|
6570
|
+
content = content.replace(/gsd:/gi, 'gsd-');
|
|
6571
|
+
content = content.replace(/\.claude\/skills\//g, '.cursor/skills/');
|
|
6572
|
+
content = content.replace(/CLAUDE\.md/g, '.cursor/rules/');
|
|
6573
|
+
content = content.replace(/\bClaude Code\b/g, 'Cursor');
|
|
6574
|
+
return content;
|
|
6575
|
+
},
|
|
6576
|
+
},
|
|
6577
|
+
windsurf: {
|
|
6578
|
+
md: (content) => convertClaudeToWindsurfMarkdown(content),
|
|
6579
|
+
js: (content) => {
|
|
6580
|
+
// Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085).
|
|
6581
|
+
content = content.replace(/gsd:/gi, 'gsd-');
|
|
6582
|
+
content = content.replace(/\.claude\/skills\//g, '.devin/skills/');
|
|
6583
|
+
content = content.replace(/CLAUDE\.md/g, '.devin/rules');
|
|
6584
|
+
content = content.replace(/\bClaude Code\b/g, 'Windsurf');
|
|
6585
|
+
return content;
|
|
6586
|
+
},
|
|
6587
|
+
},
|
|
6588
|
+
trae: {
|
|
6589
|
+
md: (content) => convertClaudeToTraeMarkdown(content),
|
|
6590
|
+
js: (content) => {
|
|
6591
|
+
content = content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
|
|
6592
|
+
return `/gsd-${commandName}`;
|
|
6593
|
+
});
|
|
6594
|
+
content = content.replace(/\.claude\/skills\//g, '.trae/skills/');
|
|
6595
|
+
content = content.replace(/CLAUDE\.md/g, '.trae/rules/');
|
|
6596
|
+
content = content.replace(/\bClaude Code\b/g, 'Trae');
|
|
6597
|
+
return content;
|
|
6598
|
+
},
|
|
6599
|
+
},
|
|
6600
|
+
cline: {
|
|
6601
|
+
md: (content) => convertClaudeToCliineMarkdown(content),
|
|
6602
|
+
js: (content) => {
|
|
6603
|
+
content = content.replace(/\.claude\/skills\//g, '.cline/skills/');
|
|
6604
|
+
content = content.replace(/CLAUDE\.md/g, '.clinerules');
|
|
6605
|
+
content = content.replace(/\bClaude Code\b/g, 'Cline');
|
|
6606
|
+
return content;
|
|
6607
|
+
},
|
|
6608
|
+
},
|
|
6609
|
+
qwen: {
|
|
6610
|
+
md: (content) => {
|
|
6611
|
+
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
6612
|
+
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
6613
|
+
content = content.replace(/\.claude\//g, '.qwen/');
|
|
6614
|
+
return content;
|
|
6615
|
+
},
|
|
6616
|
+
js: (content) => {
|
|
6617
|
+
content = content.replace(/\.claude\/skills\//g, '.qwen/skills/');
|
|
6618
|
+
content = content.replace(/\.claude\//g, '.qwen/');
|
|
6619
|
+
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
6620
|
+
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
6621
|
+
return content;
|
|
6622
|
+
},
|
|
6623
|
+
},
|
|
6624
|
+
hermes: {
|
|
6625
|
+
md: (content) => {
|
|
6626
|
+
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
6627
|
+
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
6628
|
+
content = content.replace(/\.claude\//g, '.hermes/');
|
|
6629
|
+
return content;
|
|
6630
|
+
},
|
|
6631
|
+
js: (content) => {
|
|
6632
|
+
content = content.replace(/\.claude\/skills\//g, '.hermes/skills/');
|
|
6633
|
+
content = content.replace(/\.claude\//g, '.hermes/');
|
|
6634
|
+
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
6635
|
+
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
6636
|
+
return content;
|
|
6637
|
+
},
|
|
6638
|
+
},
|
|
6639
|
+
};
|
|
7318
6640
|
|
|
7319
6641
|
/**
|
|
7320
6642
|
* Recursively copy directory, replacing paths in .md files
|
|
@@ -7326,22 +6648,28 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) {
|
|
|
7326
6648
|
* @param {boolean} isCommand - Whether the source is a command directory
|
|
7327
6649
|
* @param {boolean} isGlobal - Whether the install is global
|
|
7328
6650
|
*/
|
|
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';
|
|
6651
|
+
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand = false, isGlobal = false, confinementRoot) {
|
|
7343
6652
|
const dirName = getDirName(runtime);
|
|
7344
6653
|
|
|
6654
|
+
// ADR-1239 Phase B write-confinement: refuse to wipe/write a destDir that
|
|
6655
|
+
// escapes the caller-declared install root. Runs BEFORE the rmSync below so a
|
|
6656
|
+
// crafted destDir can never delete or write outside confinementRoot.
|
|
6657
|
+
if (confinementRoot === undefined) {
|
|
6658
|
+
throw new Error(
|
|
6659
|
+
'copyWithPathReplacement: confinementRoot is required to confine writes to the install root — refusing to write',
|
|
6660
|
+
);
|
|
6661
|
+
}
|
|
6662
|
+
const resolvedConfinementRoot = path.resolve(confinementRoot);
|
|
6663
|
+
const resolvedDestDir = assertDestWithinConfigHome(confinementRoot, destDir);
|
|
6664
|
+
if (hasExistingSymlinkBetween(resolvedConfinementRoot, resolvedDestDir)) {
|
|
6665
|
+
throw new Error(
|
|
6666
|
+
`copyWithPathReplacement: destDir "${destDir}" contains a symlink escaping the install root "${confinementRoot}" — refusing to write`,
|
|
6667
|
+
);
|
|
6668
|
+
}
|
|
6669
|
+
// Use the validated absolute path for all writes below so the gate validates
|
|
6670
|
+
// exactly what is written (a relative destDir would otherwise resolve to cwd).
|
|
6671
|
+
destDir = resolvedDestDir;
|
|
6672
|
+
|
|
7345
6673
|
// Clean install: remove existing destination to prevent orphaned files
|
|
7346
6674
|
if (fs.existsSync(destDir)) {
|
|
7347
6675
|
fs.rmSync(destDir, { recursive: true });
|
|
@@ -7355,12 +6683,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7355
6683
|
const destPath = path.join(destDir, entry.name);
|
|
7356
6684
|
|
|
7357
6685
|
if (entry.isDirectory()) {
|
|
7358
|
-
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal);
|
|
6686
|
+
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime, isCommand, isGlobal, confinementRoot);
|
|
7359
6687
|
} else if (entry.name.endsWith('.md')) {
|
|
6688
|
+
const dispatch = RUNTIME_CONTENT_DISPATCH[runtime] || {};
|
|
6689
|
+
const ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName: entry.name, runtime };
|
|
6690
|
+
|
|
7360
6691
|
// Replace ~/.claude/ and $HOME/.claude/ and ./.claude/ with runtime-appropriate paths
|
|
7361
|
-
// Skip generic replacement for Copilot —
|
|
6692
|
+
// Skip generic replacement for Copilot/Antigravity — their converters handle all paths
|
|
7362
6693
|
let content = fs.readFileSync(srcPath, 'utf8');
|
|
7363
|
-
if (!
|
|
6694
|
+
if (!dispatch.mdSkipGenericRewrite) {
|
|
7364
6695
|
const globalClaudeRegex = /~\/\.claude\//g;
|
|
7365
6696
|
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
|
7366
6697
|
const localClaudeRegex = /\.\/\.claude\//g;
|
|
@@ -7395,112 +6726,25 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7395
6726
|
// colon-canonical runtimes (Gemini).
|
|
7396
6727
|
content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
|
|
7397
6728
|
|
|
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/');
|
|
6729
|
+
// Apply per-runtime .md converter (if any)
|
|
6730
|
+
if (dispatch.md) content = dispatch.md(content, ctx);
|
|
6731
|
+
|
|
6732
|
+
// Re-run attribution after converter for runtimes that need it (copilot, antigravity)
|
|
6733
|
+
if (dispatch.mdReattributeAfter) content = processAttribution(content, getCommitAttribution(runtime));
|
|
6734
|
+
|
|
6735
|
+
// Gemini: rename .md → .toml for command files
|
|
6736
|
+
const finalPath = (dispatch.mdTomlRenameOnCommand && isCommand) ? destPath.replace(/\.md$/, '.toml') : destPath;
|
|
6737
|
+
fs.writeFileSync(finalPath, content);
|
|
6738
|
+
} else if (entry.name.endsWith('.cjs') || entry.name.endsWith('.js')) {
|
|
6739
|
+
const dispatch = RUNTIME_CONTENT_DISPATCH[runtime] || {};
|
|
6740
|
+
if (dispatch.js) {
|
|
6741
|
+
const ctx = { isCommand, isGlobal, dirName, pathPrefix, entryName: entry.name, runtime };
|
|
6742
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
6743
|
+
content = dispatch.js(content, ctx);
|
|
7444
6744
|
fs.writeFileSync(destPath, content);
|
|
7445
6745
|
} else {
|
|
7446
|
-
fs.
|
|
6746
|
+
fs.copyFileSync(srcPath, destPath);
|
|
7447
6747
|
}
|
|
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
6748
|
} else {
|
|
7505
6749
|
fs.copyFileSync(srcPath, destPath);
|
|
7506
6750
|
}
|
|
@@ -7674,19 +6918,7 @@ const GSD_UNINSTALL_HOOKS = [
|
|
|
7674
6918
|
* @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini', 'codex', 'copilot')
|
|
7675
6919
|
*/
|
|
7676
6920
|
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';
|
|
6921
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
7690
6922
|
const dirName = getDirName(runtime);
|
|
7691
6923
|
|
|
7692
6924
|
// Get the target directory based on runtime and install type. Cline local
|
|
@@ -7702,21 +6934,9 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
7702
6934
|
? targetDir.replace(os.homedir(), '~')
|
|
7703
6935
|
: targetDir.replace(process.cwd(), '.');
|
|
7704
6936
|
|
|
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';
|
|
6937
|
+
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
|
6938
|
+
// Phase B / #1679) — collapses the prior 15-line assignment chain.
|
|
6939
|
+
const runtimeLabel = getRuntimeLabel(runtime);
|
|
7720
6940
|
|
|
7721
6941
|
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
7722
6942
|
|
|
@@ -8676,45 +7896,14 @@ function resolveInstallRelativePath(baseDir, relPath) {
|
|
|
8676
7896
|
return { relPath: normalized, fullPath };
|
|
8677
7897
|
}
|
|
8678
7898
|
|
|
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
|
-
}
|
|
7899
|
+
// hasExistingSymlinkBetween: moved to src/install-engine.cts (ADR-1239 Phase B).
|
|
7900
|
+
// Imported from installEngine above.
|
|
8701
7901
|
|
|
8702
7902
|
/**
|
|
8703
7903
|
* Write file manifest after installation for future modification detection
|
|
8704
7904
|
*/
|
|
8705
7905
|
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';
|
|
7906
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
8718
7907
|
const gsdDir = path.join(configDir, 'gsd-core');
|
|
8719
7908
|
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
|
|
8720
7909
|
// commandsDir points to the old location for Gemini (which still uses commands/gsd/).
|
|
@@ -8943,7 +8132,7 @@ function populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathP
|
|
|
8943
8132
|
const srcDir = path.join(packageSrc, top);
|
|
8944
8133
|
const stageDir = path.join(stageRoot, top);
|
|
8945
8134
|
if (!fs.existsSync(srcDir)) continue;
|
|
8946
|
-
copyWithPathReplacement(srcDir, stageDir, pathPrefix, runtime, false, isGlobal);
|
|
8135
|
+
copyWithPathReplacement(srcDir, stageDir, pathPrefix, runtime, false, isGlobal, stageRoot);
|
|
8947
8136
|
}
|
|
8948
8137
|
|
|
8949
8138
|
for (const relPath of safeModified) {
|
|
@@ -9201,21 +8390,7 @@ function reportInstallerMigrationResult(result) {
|
|
|
9201
8390
|
}
|
|
9202
8391
|
|
|
9203
8392
|
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';
|
|
8393
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
9219
8394
|
const plan = resolveInstallPlan(runtime);
|
|
9220
8395
|
const dirName = getDirName(runtime);
|
|
9221
8396
|
const src = path.join(__dirname, '..');
|
|
@@ -9361,22 +8536,9 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9361
8536
|
homeDir,
|
|
9362
8537
|
});
|
|
9363
8538
|
|
|
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';
|
|
8539
|
+
// runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
|
|
8540
|
+
// Phase B / #1679) — collapses the prior 16-line assignment chain.
|
|
8541
|
+
const runtimeLabel = getRuntimeLabel(runtime);
|
|
9380
8542
|
|
|
9381
8543
|
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
9382
8544
|
|
|
@@ -9671,7 +8833,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9671
8833
|
if (_isSkillsRuntime) {
|
|
9672
8834
|
// Layout-driven install for skills-based runtimes (full and minimal modes)
|
|
9673
8835
|
const scope = isGlobal ? 'global' : 'local';
|
|
9674
|
-
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile);
|
|
8836
|
+
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution);
|
|
9675
8837
|
|
|
9676
8838
|
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
|
|
9677
8839
|
// gsd-* skill dirs. Prior installs wrote these files so Codex would show a
|
|
@@ -9831,7 +8993,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9831
8993
|
// Kilo support native, on-demand skills in addition to flat commands — see
|
|
9832
8994
|
// resolveRuntimeArtifactLayout's opencode/kilo entries. Derive skills from
|
|
9833
8995
|
// the SAME staged command set (gsdSrc) so both surfaces match exactly. (#784)
|
|
9834
|
-
const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix);
|
|
8996
|
+
const _skillCount = installOpencodeFamilySkills(runtime, targetDir, gsdSrc, pathPrefix, getCommitAttribution);
|
|
9835
8997
|
if (_skillCount > 0) {
|
|
9836
8998
|
console.log(` ${green}✓${reset} Installed ${_skillCount} skills to skills/`);
|
|
9837
8999
|
} else {
|
|
@@ -9886,7 +9048,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9886
9048
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
9887
9049
|
const gsdSrc = _stageSkills(_commandsDir);
|
|
9888
9050
|
const gsdDest = path.join(commandsDir, 'gsd');
|
|
9889
|
-
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
|
|
9051
|
+
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal, targetDir);
|
|
9890
9052
|
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
|
9891
9053
|
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
|
9892
9054
|
} else {
|
|
@@ -9968,7 +9130,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9968
9130
|
const skillSrc = path.join(src, 'gsd-core');
|
|
9969
9131
|
const skillDest = path.join(targetDir, 'gsd-core');
|
|
9970
9132
|
const savedGsdArtifacts = preserveUserArtifacts(skillDest, USER_OWNED_ARTIFACTS);
|
|
9971
|
-
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal);
|
|
9133
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime, false, isGlobal, targetDir);
|
|
9972
9134
|
restoreUserArtifacts(skillDest, savedGsdArtifacts);
|
|
9973
9135
|
if (verifyInstalled(skillDest, 'gsd-core')) {
|
|
9974
9136
|
console.log(` ${green}✓${reset} Installed workflow assets`);
|
|
@@ -9988,7 +9150,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9988
9150
|
const commandsSrc = path.join(src, 'commands', 'gsd');
|
|
9989
9151
|
const commandsDest = path.join(skillDest, 'commands', 'gsd');
|
|
9990
9152
|
if (fs.existsSync(commandsSrc)) {
|
|
9991
|
-
copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal);
|
|
9153
|
+
copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal, targetDir);
|
|
9992
9154
|
console.log(` ${green}✓${reset} Installed command bodies to gsd-core/commands/gsd/ (workflow delegation targets)`);
|
|
9993
9155
|
}
|
|
9994
9156
|
}
|
|
@@ -10030,11 +9192,24 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10030
9192
|
agentsSrc = _stageAgents(path.join(src, 'agents'));
|
|
10031
9193
|
const agentsDest = path.join(targetDir, 'agents');
|
|
10032
9194
|
|
|
9195
|
+
// ADR-1235 §1: runtimes that have been migrated to the descriptor-driven agent
|
|
9196
|
+
// path (installRuntimeArtifacts → convertedAgentsKind). The descriptor path
|
|
9197
|
+
// applies path-rewrite + attribution + converter + normalize via
|
|
9198
|
+
// stageAgentsForRuntimeWithConverter (with agentCtx pre-converter threading) in
|
|
9199
|
+
// createRuntimeArtifactInstallPlan. Their agents are already written ABOVE
|
|
9200
|
+
// (by installRuntimeArtifacts at line 8912), which also performs its own
|
|
9201
|
+
// stale-file prune pass. The inline stale-removal + inline loop both skip them.
|
|
9202
|
+
// Trivial group (cursor/windsurf/augment/trae/codebuddy) cut over together.
|
|
9203
|
+
// cline is excluded: it takes a rules-only local branch and has a local/global
|
|
9204
|
+
// complication that the descriptor-driven path does not handle correctly.
|
|
9205
|
+
const _DESCRIPTOR_AGENTS_RUNTIMES = new Set(['cursor', 'windsurf', 'augment', 'trae', 'codebuddy']);
|
|
9206
|
+
|
|
10033
9207
|
// Always remove stale gsd-* agents first so re-installing with
|
|
10034
9208
|
// `--minimal` actually shrinks a previously-full install.
|
|
10035
9209
|
// For Codex this also covers per-agent `.toml` files alongside the `.md`
|
|
10036
9210
|
// sources so a full → minimal switch doesn't leave stale registrations.
|
|
10037
|
-
|
|
9211
|
+
// Skipped for descriptor-agent runtimes (installRuntimeArtifacts prunes).
|
|
9212
|
+
if (!_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime) && fs.existsSync(agentsDest)) {
|
|
10038
9213
|
for (const file of fs.readdirSync(agentsDest)) {
|
|
10039
9214
|
if (
|
|
10040
9215
|
file.startsWith('gsd-') &&
|
|
@@ -10047,6 +9222,10 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
10047
9222
|
|
|
10048
9223
|
if (isKimi) {
|
|
10049
9224
|
console.log(` ${dim}↳${reset} Kimi custom agent YAML/prompt artifacts were installed via runtime artifact layout`);
|
|
9225
|
+
} else if (_DESCRIPTOR_AGENTS_RUNTIMES.has(runtime)) {
|
|
9226
|
+
// installRuntimeArtifacts already wrote agents + handles stale-file cleanup
|
|
9227
|
+
// via its own prune pass. No further action needed.
|
|
9228
|
+
console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
|
|
10050
9229
|
} else if (isMinimalMode(_effectiveInstallMode)) {
|
|
10051
9230
|
// Codex registers agents in `config.toml` via `[agents.gsd-*]` sections.
|
|
10052
9231
|
// Without stripping them here, a full → minimal reinstall would leave the
|
|
@@ -11217,14 +10396,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
11217
10396
|
* Apply statusline config, then print completion message
|
|
11218
10397
|
*/
|
|
11219
10398
|
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';
|
|
10399
|
+
const { isOpencode, isKilo, isGemini, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
|
|
11228
10400
|
const plan = resolveInstallPlan(runtime);
|
|
11229
10401
|
|
|
11230
10402
|
if (shouldInstallStatusline && plan.writesSharedSettings && !isOpencode) {
|
|
@@ -11347,37 +10519,11 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
11347
10519
|
}
|
|
11348
10520
|
}
|
|
11349
10521
|
|
|
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';
|
|
10522
|
+
// program + command are now single-source lookups (ADR-1239 Phase B / #1679):
|
|
10523
|
+
// program is the runtime display label; command is the per-host /gsd-new-project
|
|
10524
|
+
// invocation syntax.
|
|
10525
|
+
const program = getRuntimeLabel(runtime);
|
|
10526
|
+
const command = getRuntimeNewProjectCommand(runtime);
|
|
11381
10527
|
|
|
11382
10528
|
// Claude Code global installs use the skills/ format (CC 2.1.88+).
|
|
11383
10529
|
// Restart is required for CC to pick up newly-installed skills, and the
|
|
@@ -12404,6 +11550,8 @@ module.exports = {
|
|
|
12404
11550
|
// runtimeArtifactConversion spread (#1559).
|
|
12405
11551
|
processAttribution,
|
|
12406
11552
|
applyRuntimeContentRewritesForCommandsInPlace,
|
|
11553
|
+
_copyStaged,
|
|
11554
|
+
copyWithPathReplacement,
|
|
12407
11555
|
};
|
|
12408
11556
|
|
|
12409
11557
|
// Main logic — only run when not loaded as a module for testing
|