@opengsd/gsd-core 1.5.0 → 1.6.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-plan-checker.md +34 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-roadmapper.md +6 -0
- package/bin/install.js +199 -365
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +695 -5
- package/gsd-core/bin/lib/adr-parser.cjs +45 -23
- package/gsd-core/bin/lib/audit.cjs +2 -2
- package/gsd-core/bin/lib/capability-consent.cjs +763 -0
- package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
- package/gsd-core/bin/lib/capability-loader.cjs +764 -0
- package/gsd-core/bin/lib/capability-lock.cjs +553 -0
- package/gsd-core/bin/lib/capability-registry.cjs +198 -4
- package/gsd-core/bin/lib/capability-source.cjs +1242 -0
- package/gsd-core/bin/lib/capability-state.cjs +9 -6
- package/gsd-core/bin/lib/capability-trust.cjs +550 -0
- package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
- package/gsd-core/bin/lib/capability-writer.cjs +14 -5
- package/gsd-core/bin/lib/check-command-router.cjs +69 -18
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +98 -84
- package/gsd-core/bin/lib/config-schema.cjs +26 -7
- package/gsd-core/bin/lib/config.cjs +7 -1
- package/gsd-core/bin/lib/decisions.cjs +149 -60
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/gap-checker.cjs +126 -11
- package/gsd-core/bin/lib/init.cjs +91 -22
- package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
- package/gsd-core/bin/lib/milestone.cjs +41 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
- package/gsd-core/bin/lib/phase.cjs +33 -4
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/project-root.cjs +89 -2
- package/gsd-core/bin/lib/resolution.cjs +26 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +73 -106
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
- package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
- package/gsd-core/bin/lib/semver-compare.cjs +127 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/state-document.cjs +4 -2
- package/gsd-core/bin/lib/state.cjs +317 -161
- package/gsd-core/bin/lib/surface.cjs +12 -19
- package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
- package/gsd-core/bin/lib/uat.cjs +39 -26
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +40 -15
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +6 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -1
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/execute-phase-wave-guard.md +33 -0
- package/gsd-core/references/planner-antipatterns.md +48 -0
- package/gsd-core/references/planning-config.md +4 -0
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/references/scout-codebase.md +2 -2
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -2
- package/gsd-core/workflows/execute-phase.md +12 -12
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/package.json +6 -3
- package/scripts/gen-capability-matrix.cjs +284 -0
- package/scripts/gen-capability-registry.cjs +96 -1853
- package/scripts/lint-regression-test-names.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.cjs +192 -0
- package/scripts/lint-test-file-count.allowlist.json +9 -0
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/run-tests.cjs +14 -0
- package/scripts/sync-manifest-versions.cjs +77 -5
package/bin/install.js
CHANGED
|
@@ -33,6 +33,11 @@ const {
|
|
|
33
33
|
getGlobalConfigDir,
|
|
34
34
|
getGlobalSkillsBase,
|
|
35
35
|
} = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
36
|
+
// getDirName (runtime -> local config dir name) is relocated out of this
|
|
37
|
+
// installer to the runtime-name-policy leaf (ADR-1508 / #1510 Phase 1) so the
|
|
38
|
+
// conversion module's rewrite engine can consume it without importing
|
|
39
|
+
// bin/install.js. Re-exported below for back-compat consumers/tests.
|
|
40
|
+
const { getDirName } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
36
41
|
const {
|
|
37
42
|
applyWorktreeBaseRef,
|
|
38
43
|
readBaseRefFromSettings,
|
|
@@ -358,6 +363,10 @@ const {
|
|
|
358
363
|
const {
|
|
359
364
|
resolveRuntimeArtifactLayout,
|
|
360
365
|
} = require(path.join(_gsdLibDir, 'runtime-artifact-layout.cjs'));
|
|
366
|
+
const {
|
|
367
|
+
createRuntimeArtifactInstallPlan,
|
|
368
|
+
createRuntimeArtifactUninstallPlan,
|
|
369
|
+
} = require(path.join(_gsdLibDir, 'runtime-artifact-install-plan.cjs'));
|
|
361
370
|
const {
|
|
362
371
|
planLegacyCleanup,
|
|
363
372
|
applyLegacyCleanup,
|
|
@@ -461,25 +470,8 @@ Then re-run: npx ${pkg.name}@latest
|
|
|
461
470
|
}
|
|
462
471
|
}
|
|
463
472
|
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
if (runtime === 'copilot') return '.github';
|
|
467
|
-
if (runtime === 'opencode') return '.opencode';
|
|
468
|
-
if (runtime === 'gemini') return '.gemini';
|
|
469
|
-
if (runtime === 'kilo') return '.kilo';
|
|
470
|
-
if (runtime === 'codex') return '.codex';
|
|
471
|
-
if (runtime === 'antigravity') return '.agents';
|
|
472
|
-
if (runtime === 'cursor') return '.cursor';
|
|
473
|
-
if (runtime === 'windsurf') return '.devin';
|
|
474
|
-
if (runtime === 'augment') return '.augment';
|
|
475
|
-
if (runtime === 'trae') return '.trae';
|
|
476
|
-
if (runtime === 'qwen') return '.qwen';
|
|
477
|
-
if (runtime === 'hermes') return '.hermes';
|
|
478
|
-
if (runtime === 'kimi') return '.kimi-code';
|
|
479
|
-
if (runtime === 'codebuddy') return '.codebuddy';
|
|
480
|
-
if (runtime === 'cline') return '.cline';
|
|
481
|
-
return '.claude';
|
|
482
|
-
}
|
|
473
|
+
// getDirName (runtime -> local config dir name) now lives in
|
|
474
|
+
// runtime-name-policy.cjs (ADR-1508 / #1510 Phase 1); imported + re-exported.
|
|
483
475
|
|
|
484
476
|
/**
|
|
485
477
|
* Get the config directory path relative to home directory for a runtime
|
|
@@ -605,30 +597,12 @@ if (hasHelp) {
|
|
|
605
597
|
process.exit(0);
|
|
606
598
|
}
|
|
607
599
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
* `@$HOME/...` is treated as a path relative to the config command/ dir, which
|
|
615
|
-
* resolves to `command/$HOME/...` (file not found). For OpenCode we always emit
|
|
616
|
-
* the absolute resolved path. (#2376 Windows, #2831 macOS/Linux.)
|
|
617
|
-
*
|
|
618
|
-
* @param {object} args
|
|
619
|
-
* @param {boolean} args.isGlobal - Global runtime install vs local project
|
|
620
|
-
* @param {boolean} args.isOpencode - Whether the runtime is OpenCode
|
|
621
|
-
* @param {boolean} args.isWindowsHost - process.platform === 'win32'
|
|
622
|
-
* @param {string} args.resolvedTarget - Absolute target dir, forward-slashed
|
|
623
|
-
* @param {string} args.homeDir - User home dir, forward-slashed
|
|
624
|
-
* @returns {string} pathPrefix ending with '/'
|
|
625
|
-
*/
|
|
626
|
-
function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost, resolvedTarget, homeDir }) {
|
|
627
|
-
if (isGlobal && resolvedTarget.startsWith(homeDir) && !isOpencode) {
|
|
628
|
-
return '$HOME' + resolvedTarget.slice(homeDir.length) + '/';
|
|
629
|
-
}
|
|
630
|
-
return `${resolvedTarget}/`;
|
|
631
|
-
}
|
|
600
|
+
// computePathPrefix: implementation moved to runtimeArtifactConversion._computePathPrefix
|
|
601
|
+
// (ADR-1508 / #1511 Phase 2 — single owner). The const binding above (~line 638)
|
|
602
|
+
// re-exports it here for call sites and module.exports.
|
|
603
|
+
// Original doc: Compute the path prefix used for `@file` references in installed
|
|
604
|
+
// command/skill markdown. For global installs under $HOME uses $HOME/... form;
|
|
605
|
+
// OpenCode always uses the absolute path (#2376 Windows, #2831 macOS/Linux).
|
|
632
606
|
|
|
633
607
|
// normalizeNodePath, resolveNodeRunner, resolveBashRunner, referencesHook are
|
|
634
608
|
// now owned by the runtime-hooks-surface module. Import them here so
|
|
@@ -643,6 +617,19 @@ const referencesHook = hooksSurface.referencesHook;
|
|
|
643
617
|
// applySettingsJsonHooks: mutates settings.hooks.* in place with all GSD-managed
|
|
644
618
|
// hook registrations for settings.json-surface runtimes (ADR-857 phase 5f-1b).
|
|
645
619
|
const applySettingsJsonHooks = hooksSurface.applySettingsJsonHooks;
|
|
620
|
+
// processAttribution: pure Co-Authored-By content transform, relocated to the
|
|
621
|
+
// conversion module (ADR-1508 / #1510 Phase 1). Bound here so install.js
|
|
622
|
+
// callers continue to work and there is a single implementation. (All call
|
|
623
|
+
// sites are below this line, so the const binding has no TDZ hazard.)
|
|
624
|
+
const processAttribution = runtimeArtifactConversion.processAttribution;
|
|
625
|
+
// computePathPrefix / applyRuntimeContentRewritesInPlace / applyRuntimeContentRewritesForCommandsInPlace:
|
|
626
|
+
// Single implementations now live in runtimeArtifactConversion (ADR-1508 / #1511 Phase 2).
|
|
627
|
+
// Re-bound here so install.js call sites and exports continue to work unchanged.
|
|
628
|
+
// Local bodies replaced by breadcrumb comments at their original locations.
|
|
629
|
+
// All call sites are below this line → no TDZ hazard.
|
|
630
|
+
const computePathPrefix = runtimeArtifactConversion._computePathPrefix;
|
|
631
|
+
const applyRuntimeContentRewritesInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesInPlace;
|
|
632
|
+
const applyRuntimeContentRewritesForCommandsInPlace = runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace;
|
|
646
633
|
|
|
647
634
|
function rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts) {
|
|
648
635
|
return hooksSurface.rewriteLegacyManagedNodeHookCommands(settings, absoluteRunner, opts);
|
|
@@ -1417,24 +1404,9 @@ function getCommitAttribution(runtime) {
|
|
|
1417
1404
|
return result;
|
|
1418
1405
|
}
|
|
1419
1406
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
* @param {null|undefined|string} attribution - null=remove, undefined=keep, string=replace
|
|
1424
|
-
* @returns {string} Processed content
|
|
1425
|
-
*/
|
|
1426
|
-
function processAttribution(content, attribution) {
|
|
1427
|
-
if (attribution === null) {
|
|
1428
|
-
// Remove Co-Authored-By lines and the preceding blank line
|
|
1429
|
-
return content.replace(/(\r?\n){2}Co-Authored-By:.*$/gim, '');
|
|
1430
|
-
}
|
|
1431
|
-
if (attribution === undefined) {
|
|
1432
|
-
return content;
|
|
1433
|
-
}
|
|
1434
|
-
// Replace with custom attribution (escape $ to prevent backreference injection)
|
|
1435
|
-
const safeAttribution = attribution.replace(/\$/g, '$$$$');
|
|
1436
|
-
return content.replace(/Co-Authored-By:.*$/gim, `Co-Authored-By: ${safeAttribution}`);
|
|
1437
|
-
}
|
|
1407
|
+
// processAttribution (pure Co-Authored-By content transform) relocated to
|
|
1408
|
+
// runtime-artifact-conversion.cjs (ADR-1508 / #1510 Phase 1); bound above.
|
|
1409
|
+
// getCommitAttribution stays here — it is impure install-time config I/O.
|
|
1438
1410
|
|
|
1439
1411
|
/**
|
|
1440
1412
|
* Convert Claude Code frontmatter to opencode format
|
|
@@ -6689,24 +6661,10 @@ function migrateLegacyDevPreferencesToSkill(targetDir, saved, runtime, scope = '
|
|
|
6689
6661
|
* @param {string} pathPrefix e.g. "~/.codex/" — trailing-slash string
|
|
6690
6662
|
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6691
6663
|
*/
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
const walkAndRewrite = (dir) => {
|
|
6697
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
6698
|
-
const fullPath = path.join(dir, entry.name);
|
|
6699
|
-
if (entry.isDirectory()) {
|
|
6700
|
-
walkAndRewrite(fullPath);
|
|
6701
|
-
} else if (entry.name.endsWith('.md')) {
|
|
6702
|
-
let content = fs.readFileSync(fullPath, 'utf8');
|
|
6703
|
-
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
|
|
6704
|
-
fs.writeFileSync(fullPath, content);
|
|
6705
|
-
}
|
|
6706
|
-
}
|
|
6707
|
-
};
|
|
6708
|
-
walkAndRewrite(stagedDir);
|
|
6709
|
-
}
|
|
6664
|
+
// applyRuntimeContentRewritesInPlace: walk loop is now owned by
|
|
6665
|
+
// runtimeArtifactConversion.applyRuntimeContentRewritesInPlace (ADR-1508 / #1511 Phase 2).
|
|
6666
|
+
// The const binding above (~line 629) delegates here. Call sites in installRuntimeArtifacts
|
|
6667
|
+
// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
|
|
6710
6668
|
|
|
6711
6669
|
/**
|
|
6712
6670
|
* Apply per-runtime content rewrites to flat .md files in a staged commands dir.
|
|
@@ -6724,30 +6682,10 @@ function applyRuntimeContentRewritesInPlace(stagedDir, runtime, pathPrefix, isGl
|
|
|
6724
6682
|
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6725
6683
|
* @returns {string} path to a temp dir with rewritten files (caller is responsible for cleanup)
|
|
6726
6684
|
*/
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
// package source. A temp copy is unconditional to keep the code simple and safe.
|
|
6732
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-cmd-rewrites-'));
|
|
6733
|
-
try {
|
|
6734
|
-
for (const entry of fs.readdirSync(stagedDir, { withFileTypes: true })) {
|
|
6735
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
6736
|
-
let content = fs.readFileSync(path.join(stagedDir, entry.name), 'utf8');
|
|
6737
|
-
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal);
|
|
6738
|
-
// For augment commands, apply the markdown conversion so tool references
|
|
6739
|
-
// and skill paths use Augment equivalents.
|
|
6740
|
-
if (runtime === 'augment') {
|
|
6741
|
-
content = convertClaudeToAugmentMarkdown(content);
|
|
6742
|
-
}
|
|
6743
|
-
fs.writeFileSync(path.join(tempDir, entry.name), content);
|
|
6744
|
-
}
|
|
6745
|
-
} catch (err) {
|
|
6746
|
-
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
6747
|
-
throw err;
|
|
6748
|
-
}
|
|
6749
|
-
return tempDir;
|
|
6750
|
-
}
|
|
6685
|
+
// applyRuntimeContentRewritesForCommandsInPlace: copy+rewrite loop is now owned by
|
|
6686
|
+
// runtimeArtifactConversion.applyRuntimeContentRewritesForCommandsInPlace (ADR-1508 / #1511 Phase 2).
|
|
6687
|
+
// The const binding above (~line 630) delegates here. Call sites in installRuntimeArtifacts
|
|
6688
|
+
// pass attribution as the 5th arg (getCommitAttribution(runtime)) per the new contract.
|
|
6751
6689
|
|
|
6752
6690
|
/**
|
|
6753
6691
|
* Apply the per-runtime rewrite table to a single content string.
|
|
@@ -6759,196 +6697,12 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
|
|
|
6759
6697
|
* @param {boolean} [isGlobal=false] true when the install is a global (home-dir) install
|
|
6760
6698
|
* @returns {string}
|
|
6761
6699
|
*/
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6769
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6770
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6771
|
-
content = content.replace(/~\/\.codex\//g, pathPrefix);
|
|
6772
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6773
|
-
break;
|
|
6774
|
-
|
|
6775
|
-
case 'cline':
|
|
6776
|
-
// Slash forms: both the original ~/.claude/ (safety net) and the stage-time
|
|
6777
|
-
// converted ~/.cline/ (from convertClaudeToCliineMarkdown) → pathPrefix
|
|
6778
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6779
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6780
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6781
|
-
content = content.replace(/~\/\.cline\//g, pathPrefix);
|
|
6782
|
-
content = content.replace(/\$HOME\/\.cline\//g, pathPrefix);
|
|
6783
|
-
// Bare forms (no trailing slash)
|
|
6784
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6785
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6786
|
-
content = content.replace(/~\/\.cline\b/g, normalizedPathPrefix);
|
|
6787
|
-
content = content.replace(/\$HOME\/\.cline\b/g, normalizedPathPrefix);
|
|
6788
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6789
|
-
break;
|
|
6790
|
-
|
|
6791
|
-
case 'cursor':
|
|
6792
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6793
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6794
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6795
|
-
// Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
|
|
6796
|
-
// .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
|
|
6797
|
-
// fires between 'e' and '-', which rewrites .claude-plugin → .cursor-plugin).
|
|
6798
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6799
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6800
|
-
content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
|
|
6801
|
-
content = content.replace(/~\/\.cursor\//g, pathPrefix);
|
|
6802
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6803
|
-
break;
|
|
6804
|
-
|
|
6805
|
-
case 'windsurf': {
|
|
6806
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6807
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6808
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6809
|
-
// Bare forms (no trailing slash) — use (?![\w-]) instead of \b so that
|
|
6810
|
-
// .claude-plugin / .claudeignore are NOT corrupted (the \b word-boundary
|
|
6811
|
-
// fires between 'e' and '-', which rewrites .claude-plugin → .devin-plugin).
|
|
6812
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6813
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6814
|
-
content = content.replace(/~\/\.codeium\/windsurf\//g, pathPrefix);
|
|
6815
|
-
// Stage-1 converter rewrites .claude/skills/ → .devin/skills/ (workspace-relative
|
|
6816
|
-
// form). For global installs the real path is pathPrefix + skills/, so fix that up
|
|
6817
|
-
// here using the real isGlobal flag (threaded from installRuntimeArtifacts scope,
|
|
6818
|
-
// not derived from pathPrefix substring which misclassifies custom config dirs).
|
|
6819
|
-
// For local installs, the relative .devin/ form is correct — leave it. (#1085)
|
|
6820
|
-
if (isGlobal) {
|
|
6821
|
-
content = content.replace(/\.devin\/skills\//g, `${pathPrefix}skills/`);
|
|
6822
|
-
content = content.replace(/\.\/\.devin\//g, pathPrefix);
|
|
6823
|
-
content = content.replace(/~\/\.devin(?![\w-])/g, normalizedPathPrefix);
|
|
6824
|
-
content = content.replace(/\$HOME\/\.devin(?![\w-])/g, normalizedPathPrefix);
|
|
6825
|
-
}
|
|
6826
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6827
|
-
break;
|
|
6828
|
-
}
|
|
6829
|
-
|
|
6830
|
-
case 'augment':
|
|
6831
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6832
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6833
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6834
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6835
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6836
|
-
content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
|
|
6837
|
-
content = content.replace(/~\/\.augment\//g, pathPrefix);
|
|
6838
|
-
content = content.replace(/\$HOME\/\.augment\//g, pathPrefix);
|
|
6839
|
-
content = content.replace(/~\/\.augment(?![\w-])/g, normalizedPathPrefix);
|
|
6840
|
-
content = content.replace(/\$HOME\/\.augment(?![\w-])/g, normalizedPathPrefix);
|
|
6841
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6842
|
-
break;
|
|
6843
|
-
|
|
6844
|
-
case 'trae':
|
|
6845
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6846
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6847
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6848
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6849
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6850
|
-
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
6851
|
-
content = content.replace(/~\/\.trae\//g, pathPrefix);
|
|
6852
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6853
|
-
break;
|
|
6854
|
-
|
|
6855
|
-
case 'codebuddy':
|
|
6856
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6857
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6858
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6859
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6860
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6861
|
-
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
6862
|
-
// The codebuddy converter rewrites `.claude/` → `.codebuddy/` at stage
|
|
6863
|
-
// time, so `$HOME/.claude/...` arrives here as `$HOME/.codebuddy/...`.
|
|
6864
|
-
// Normalize BOTH the `~/` and `$HOME/` forms (slash + bare) to the install
|
|
6865
|
-
// target so `--config-dir`/local installs don't leak the default home.
|
|
6866
|
-
content = content.replace(/~\/\.codebuddy\//g, pathPrefix);
|
|
6867
|
-
content = content.replace(/\$HOME\/\.codebuddy\//g, pathPrefix);
|
|
6868
|
-
content = content.replace(/~\/\.codebuddy\b/g, normalizedPathPrefix);
|
|
6869
|
-
content = content.replace(/\$HOME\/\.codebuddy\b/g, normalizedPathPrefix);
|
|
6870
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6871
|
-
break;
|
|
6872
|
-
|
|
6873
|
-
case 'copilot':
|
|
6874
|
-
// Copilot converter handles path rewrites; only attribution here
|
|
6875
|
-
content = processAttribution(content, getCommitAttribution('copilot'));
|
|
6876
|
-
break;
|
|
6877
|
-
|
|
6878
|
-
case 'antigravity':
|
|
6879
|
-
// Antigravity converter handles path rewrites; only attribution here
|
|
6880
|
-
content = processAttribution(content, getCommitAttribution('antigravity'));
|
|
6881
|
-
break;
|
|
6882
|
-
|
|
6883
|
-
case 'claude':
|
|
6884
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6885
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6886
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6887
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6888
|
-
break;
|
|
6889
|
-
|
|
6890
|
-
case 'qwen':
|
|
6891
|
-
// Branding rewrites run before path rewrites to avoid consuming
|
|
6892
|
-
// patterns that the path step would also match.
|
|
6893
|
-
content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
|
|
6894
|
-
content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
|
|
6895
|
-
// Base path rewrites (use ~/ and $HOME/ slash forms first — most specific)
|
|
6896
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6897
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6898
|
-
content = content.replace(/~\/\.qwen\//g, pathPrefix);
|
|
6899
|
-
content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
|
|
6900
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6901
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6902
|
-
content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
|
|
6903
|
-
content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
|
|
6904
|
-
// Bare relative .claude/ → .qwen/ (residual refs not matched above)
|
|
6905
|
-
content = content.replace(/\.claude\//g, '.qwen/');
|
|
6906
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6907
|
-
content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
|
|
6908
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6909
|
-
break;
|
|
6910
|
-
|
|
6911
|
-
case 'hermes':
|
|
6912
|
-
// Branding rewrites run before path rewrites (same rationale as qwen)
|
|
6913
|
-
content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
|
|
6914
|
-
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
|
6915
|
-
// Base path rewrites
|
|
6916
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6917
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6918
|
-
content = content.replace(/~\/\.hermes\//g, pathPrefix);
|
|
6919
|
-
content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
|
|
6920
|
-
content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6921
|
-
content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
|
|
6922
|
-
content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
|
|
6923
|
-
content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
|
|
6924
|
-
// Bare relative .claude/ → .hermes/ (residual refs)
|
|
6925
|
-
content = content.replace(/\.claude\//g, '.hermes/');
|
|
6926
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6927
|
-
content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
|
|
6928
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6929
|
-
break;
|
|
6930
|
-
|
|
6931
|
-
case 'kimi':
|
|
6932
|
-
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
|
6933
|
-
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
|
6934
|
-
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
|
6935
|
-
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
|
|
6936
|
-
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
|
|
6937
|
-
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
|
|
6938
|
-
content = processAttribution(content, getCommitAttribution(runtime));
|
|
6939
|
-
break;
|
|
6940
|
-
|
|
6941
|
-
default:
|
|
6942
|
-
// Unknown runtime — no rewrites.
|
|
6943
|
-
// OpenCode/Kilo are intentionally absent: their skills are written by
|
|
6944
|
-
// installOpencodeFamilySkills, which applies pathPrefix BEFORE the
|
|
6945
|
-
// command→skill conversion (mirroring copyFlattenedCommands) rather than
|
|
6946
|
-
// rewriting already-converted SKILL.md bodies. See #784.
|
|
6947
|
-
break;
|
|
6948
|
-
}
|
|
6949
|
-
|
|
6950
|
-
return content;
|
|
6951
|
-
}
|
|
6700
|
+
// _applyRuntimeRewrites: single implementation lives in runtimeArtifactConversion
|
|
6701
|
+
// (ADR-1508 / #1511 Phase 2). Bound here so install.js call sites and exports are
|
|
6702
|
+
// reference-identical to the conversion module (consistent with the walkers above).
|
|
6703
|
+
// All call sites are below this line → no TDZ hazard.
|
|
6704
|
+
const _applyRuntimeRewrites = runtimeArtifactConversion._applyRuntimeRewrites;
|
|
6705
|
+
const _stampNonClaudeRuntimeDefaults = runtimeArtifactConversion._stampNonClaudeRuntimeDefaults;
|
|
6952
6706
|
|
|
6953
6707
|
/**
|
|
6954
6708
|
* Copy a staged directory's contents into destDir.
|
|
@@ -7118,18 +6872,23 @@ function _runLegacyInstallMigrations(runtime, configDir, scope = 'global') {
|
|
|
7118
6872
|
* @param {'global'|'local'} [scope]
|
|
7119
6873
|
*/
|
|
7120
6874
|
function _runLegacyUninstallCleanup(runtime, configDir, scope = 'global') {
|
|
7121
|
-
//
|
|
7122
|
-
//
|
|
7123
|
-
// Claude
|
|
7124
|
-
//
|
|
6875
|
+
// commands/gsd/ is a legacy location for Qwen, Hermes, and all Claude installs.
|
|
6876
|
+
// Prior to #1367 fix, Claude-local used commands/gsd/<cmd>.md (colon-namespaced).
|
|
6877
|
+
// After #1367, Claude-local uses flat commands/gsd-<cmd>.md. The inline uninstall
|
|
6878
|
+
// block (1c) handles removal of flat files; this function handles the legacy
|
|
6879
|
+
// commands/gsd/ directory for all Claude scopes (global was already included,
|
|
6880
|
+
// local is now added since that layout is also legacy post-#1367).
|
|
7125
6881
|
// #2973 / Codex review (bd1f06c9): preserve user-owned dev-preferences.md
|
|
7126
6882
|
// before destructive wipe. Migration to skills/gsd-dev-preferences/SKILL.md
|
|
7127
6883
|
// is deferred and returned so the caller can apply it AFTER layout-driven
|
|
7128
6884
|
// removal — this prevents the layout's gsd-* prefix removal from wiping the
|
|
7129
6885
|
// freshly created skill dir (same pattern as _runLegacyInstallMigrations).
|
|
7130
6886
|
let savedLegacyArtifacts = null;
|
|
7131
|
-
// commands/gsd/ is a legacy location for Qwen, Hermes, and Claude
|
|
7132
|
-
// Claude
|
|
6887
|
+
// commands/gsd/ is a legacy location for Qwen, Hermes, and Claude global.
|
|
6888
|
+
// Claude local is intentionally excluded: the inline uninstall block (1c) handles
|
|
6889
|
+
// commands/gsd/ for claude local, preserving dev-preferences.md by restoring it
|
|
6890
|
+
// to the same location (#1423). Using migrateLegacyDevPreferencesToSkill here
|
|
6891
|
+
// (which would redirect to skills/) conflicts with the test contract for local installs.
|
|
7133
6892
|
const isLegacyCommandsGsd = runtime === 'qwen' || runtime === 'hermes' || (runtime === 'claude' && scope === 'global');
|
|
7134
6893
|
if (isLegacyCommandsGsd) {
|
|
7135
6894
|
const legacyCommandsGsd = path.join(configDir, 'commands', 'gsd');
|
|
@@ -7253,36 +7012,25 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
|
|
7253
7012
|
_runLegacyInstallMigrations(runtime, configDir, scope);
|
|
7254
7013
|
|
|
7255
7014
|
const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
isGlobal: scope === 'global',
|
|
7263
|
-
isOpencode: runtime === 'opencode',
|
|
7264
|
-
isWindowsHost: process.platform === 'win32',
|
|
7265
|
-
resolvedTarget: _resolvedTarget,
|
|
7266
|
-
homeDir: _homeDir,
|
|
7015
|
+
const planResult = createRuntimeArtifactInstallPlan({
|
|
7016
|
+
layout,
|
|
7017
|
+
resolvedProfile,
|
|
7018
|
+
homedir: () => os.homedir(),
|
|
7019
|
+
platform: process.platform,
|
|
7020
|
+
resolveAttribution: getCommitAttribution,
|
|
7267
7021
|
});
|
|
7268
7022
|
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
}
|
|
7281
|
-
// applyRuntimeContentRewritesForCommandsInPlace() returns a fresh mkdtemp dir under
|
|
7282
|
-
// os.tmpdir() (gsd-cmd-rewrites-*); remove it once copied so it does not accumulate (#856).
|
|
7283
|
-
const tempToClean = stagedForCopy !== staged ? stagedForCopy : null;
|
|
7284
|
-
try {
|
|
7285
|
-
const dest = path.join(layout.configDir, kind.destSubpath);
|
|
7023
|
+
const cleanupDirs = planResult.ok ? planResult.plan.cleanupDirs : planResult.cleanupDirs;
|
|
7024
|
+
try {
|
|
7025
|
+
if (!planResult.ok) {
|
|
7026
|
+
throw new Error(planResult.message);
|
|
7027
|
+
}
|
|
7028
|
+
|
|
7029
|
+
const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
|
|
7030
|
+
for (const item of planResult.plan.items) {
|
|
7031
|
+
const kind = kindsByName.get(item.kind);
|
|
7032
|
+
if (!kind) throw new Error(`Install plan returned unknown artifact kind: ${item.kind}`);
|
|
7033
|
+
const dest = item.destDir;
|
|
7286
7034
|
fs.mkdirSync(dest, { recursive: true });
|
|
7287
7035
|
if (kind.kind === 'skills' && fs.existsSync(dest)) {
|
|
7288
7036
|
// Pre-prune: snapshot user-owned content before _removeGsdEntries wipes it,
|
|
@@ -7309,7 +7057,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
|
|
7309
7057
|
}
|
|
7310
7058
|
|
|
7311
7059
|
_removeGsdEntries(dest, kind);
|
|
7312
|
-
_copyStaged(
|
|
7060
|
+
_copyStaged(item.sourceDir, dest, kind);
|
|
7313
7061
|
|
|
7314
7062
|
// Restore user-owned dirs after the prune+copy
|
|
7315
7063
|
for (const [dirName, snap] of toPreserve) {
|
|
@@ -7319,13 +7067,13 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile) {
|
|
|
7319
7067
|
// For non-skills kinds (commands, agents): no user content to preserve;
|
|
7320
7068
|
// just prune stale gsd-* entries and copy new ones.
|
|
7321
7069
|
_removeGsdEntries(dest, kind);
|
|
7322
|
-
_copyStaged(
|
|
7323
|
-
}
|
|
7324
|
-
} finally {
|
|
7325
|
-
if (tempToClean) {
|
|
7326
|
-
try { fs.rmSync(tempToClean, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
7070
|
+
_copyStaged(item.sourceDir, dest, kind);
|
|
7327
7071
|
}
|
|
7328
7072
|
}
|
|
7073
|
+
} finally {
|
|
7074
|
+
for (const dir of cleanupDirs) {
|
|
7075
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
7076
|
+
}
|
|
7329
7077
|
}
|
|
7330
7078
|
|
|
7331
7079
|
// Hermes: after the install loop has written all gsd-<stem>/ dirs to
|
|
@@ -7442,9 +7190,14 @@ function uninstallRuntimeArtifacts(runtime, configDir, scope) {
|
|
|
7442
7190
|
const savedLegacyArtifacts = _runLegacyUninstallCleanup(runtime, configDir, scope);
|
|
7443
7191
|
|
|
7444
7192
|
const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope);
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7193
|
+
const plan = createRuntimeArtifactUninstallPlan(layout);
|
|
7194
|
+
const kindsByName = new Map(layout.kinds.map((kind) => [kind.kind, kind]));
|
|
7195
|
+
for (const item of plan.items) {
|
|
7196
|
+
const kind = kindsByName.get(item.kind);
|
|
7197
|
+
if (!kind) {
|
|
7198
|
+
throw new Error(`Runtime artifact uninstall plan referenced unknown kind: ${item.kind}`);
|
|
7199
|
+
}
|
|
7200
|
+
_removeGsdEntries(item.destDir, kind);
|
|
7448
7201
|
}
|
|
7449
7202
|
|
|
7450
7203
|
// Hermes: after removing gsd-* skill dirs from skills/gsd/, also remove
|
|
@@ -7535,6 +7288,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
|
|
7535
7288
|
}
|
|
7536
7289
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
7537
7290
|
|
|
7291
|
+
// #1521: stamp the workflow runtime-resolution block so every non-Claude
|
|
7292
|
+
// install resolves its own runtime identity and defaults use_worktrees=false.
|
|
7293
|
+
// copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
|
|
7294
|
+
// _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
|
|
7295
|
+
// live in real installs (it is a no-op for files without those lines).
|
|
7296
|
+
if (runtime !== 'claude') {
|
|
7297
|
+
content = _stampNonClaudeRuntimeDefaults(content, runtime);
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7538
7300
|
// #3683 — normalize /gsd:<cmd> → /gsd-<cmd> in any body passing through
|
|
7539
7301
|
// copyWithPathReplacement for runtimes that register commands under the
|
|
7540
7302
|
// hyphen form; normalizeAgentBodyForRuntime self-gates on
|
|
@@ -8066,23 +7828,37 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
8066
7828
|
} catch { /* best-effort */ }
|
|
8067
7829
|
}
|
|
8068
7830
|
|
|
8069
|
-
// 1c. Claude local: remove commands/
|
|
8070
|
-
//
|
|
8071
|
-
// flat command dirs (OpenCode/Kilo). Claude local files use no prefix inside
|
|
8072
|
-
// the namespaced directory, so layout does not remove them. Handle inline.
|
|
8073
|
-
// Preserve dev-preferences.md across the wipe (#1423).
|
|
7831
|
+
// 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
|
|
7832
|
+
// #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
|
|
8074
7833
|
if (!isGlobal && runtime === 'claude') {
|
|
8075
|
-
const
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
fs.
|
|
7834
|
+
const commandsDir = path.join(targetDir, 'commands');
|
|
7835
|
+
// Remove flat gsd-*.md files (current layout after #1367 fix)
|
|
7836
|
+
if (fs.existsSync(commandsDir)) {
|
|
7837
|
+
let removed = 0;
|
|
7838
|
+
for (const f of fs.readdirSync(commandsDir)) {
|
|
7839
|
+
if (f.startsWith('gsd-') && f.endsWith('.md')) {
|
|
7840
|
+
fs.rmSync(path.join(commandsDir, f), { force: true });
|
|
7841
|
+
removed++;
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
if (removed > 0) {
|
|
7845
|
+
removedCount++;
|
|
7846
|
+
console.log(` ${green}✓${reset} Removed ${removed} flat gsd-*.md commands from commands/`);
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7849
|
+
// Remove legacy commands/gsd/ subdirectory if it still exists (pre-#1367 layout).
|
|
7850
|
+
// Preserve user-owned dev-preferences.md if present (#1423 parity).
|
|
7851
|
+
const legacyGsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
|
7852
|
+
if (fs.existsSync(legacyGsdCommandsDir)) {
|
|
7853
|
+
const legacyDevPrefsPath = path.join(legacyGsdCommandsDir, 'dev-preferences.md');
|
|
7854
|
+
const savedDevPrefs = fs.existsSync(legacyDevPrefsPath) ? fs.readFileSync(legacyDevPrefsPath, 'utf-8') : null;
|
|
7855
|
+
fs.rmSync(legacyGsdCommandsDir, { recursive: true });
|
|
8080
7856
|
removedCount++;
|
|
8081
|
-
console.log(` ${green}✓${reset} Removed commands/gsd/`);
|
|
8082
|
-
if (
|
|
7857
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
|
|
7858
|
+
if (savedDevPrefs) {
|
|
8083
7859
|
try {
|
|
8084
|
-
fs.mkdirSync(
|
|
8085
|
-
fs.writeFileSync(
|
|
7860
|
+
fs.mkdirSync(legacyGsdCommandsDir, { recursive: true });
|
|
7861
|
+
fs.writeFileSync(legacyDevPrefsPath, savedDevPrefs);
|
|
8086
7862
|
console.log(` ${green}✓${reset} Preserved commands/gsd/dev-preferences.md`);
|
|
8087
7863
|
} catch (err) {
|
|
8088
7864
|
console.error(` ${red}✗${reset} Failed to restore dev-preferences.md: ${err.message}`);
|
|
@@ -8849,7 +8625,11 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
8849
8625
|
const isKimi = runtime === 'kimi';
|
|
8850
8626
|
const isHermes = runtime === 'hermes';
|
|
8851
8627
|
const gsdDir = path.join(configDir, 'gsd-core');
|
|
8628
|
+
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
|
|
8629
|
+
// commandsDir points to the old location for Gemini (which still uses commands/gsd/).
|
|
8630
|
+
// Claude local uses flatCommandsDir instead for manifest recording.
|
|
8852
8631
|
const commandsDir = path.join(configDir, 'commands', 'gsd');
|
|
8632
|
+
const flatCommandsDir = path.join(configDir, 'commands');
|
|
8853
8633
|
const opencodeCommandDir = path.join(configDir, 'command');
|
|
8854
8634
|
// Hermes nests GSD skills under skills/gsd/ as a single category (#2841).
|
|
8855
8635
|
// All other runtimes that use the Codex-style skills layout use a flat skills/ root.
|
|
@@ -8875,17 +8655,27 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
|
|
8875
8655
|
if (USER_OWNED_ARTIFACTS.includes(rel)) continue;
|
|
8876
8656
|
manifest.files['gsd-core/' + rel] = hash;
|
|
8877
8657
|
}
|
|
8878
|
-
// Record commands
|
|
8879
|
-
//
|
|
8880
|
-
//
|
|
8881
|
-
//
|
|
8882
|
-
//
|
|
8883
|
-
if (fs.existsSync(commandsDir)) {
|
|
8658
|
+
// Record commands surface for runtimes that emit it:
|
|
8659
|
+
// Gemini: commands/gsd/<cmd>.toml (nested, colon-namespaced)
|
|
8660
|
+
// Claude local (#1367 fix): flat gsd-<cmd>.md at commands/ level
|
|
8661
|
+
// Manifest must reflect everything on disk so saveLocalPatches() can detect
|
|
8662
|
+
// user edits and per-runtime minimal-mode assertions can read manifest.files.
|
|
8663
|
+
if (isGemini && fs.existsSync(commandsDir)) {
|
|
8884
8664
|
const cmdHashes = generateManifest(commandsDir);
|
|
8885
8665
|
for (const [rel, hash] of Object.entries(cmdHashes)) {
|
|
8886
8666
|
manifest.files['commands/gsd/' + rel] = hash;
|
|
8887
8667
|
}
|
|
8888
8668
|
}
|
|
8669
|
+
// Claude local (#1367): flat gsd-*.md files at commands/ level.
|
|
8670
|
+
// Only claude local writes gsd-*.md here; global installs don't emit commands,
|
|
8671
|
+
// so this branch is a no-op for global (no matching files to find).
|
|
8672
|
+
if (runtime === 'claude' && fs.existsSync(flatCommandsDir)) {
|
|
8673
|
+
for (const file of fs.readdirSync(flatCommandsDir)) {
|
|
8674
|
+
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
8675
|
+
manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
|
|
8676
|
+
}
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8889
8679
|
if ((isOpencode || isKilo) && fs.existsSync(opencodeCommandDir)) {
|
|
8890
8680
|
for (const file of fs.readdirSync(opencodeCommandDir)) {
|
|
8891
8681
|
if (file.startsWith('gsd-') && file.endsWith('.md')) {
|
|
@@ -9985,18 +9775,59 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
|
|
9985
9775
|
}
|
|
9986
9776
|
}
|
|
9987
9777
|
} else {
|
|
9988
|
-
// Claude Code local:
|
|
9989
|
-
// commands from .claude/commands/
|
|
9778
|
+
// Claude Code local: flat gsd-<cmd>.md layout — Claude Code registers
|
|
9779
|
+
// commands from .claude/commands/ using the filename stem as the command
|
|
9780
|
+
// name, so gsd-<cmd>.md produces the /gsd-<cmd> hyphen form used everywhere
|
|
9781
|
+
// in the framework. The old commands/gsd/<cmd>.md subdirectory layout caused
|
|
9782
|
+
// Claude Code to namespace commands as /gsd:<cmd> (colon form). (#1367)
|
|
9990
9783
|
const commandsDir = path.join(targetDir, 'commands');
|
|
9991
9784
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
9992
9785
|
const gsdSrc = _stageSkills(_commandsDir);
|
|
9993
|
-
const
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9786
|
+
const cmdNames = readGsdCommandNames();
|
|
9787
|
+
|
|
9788
|
+
// Remove stale gsd-*.md files before writing new ones (clean install)
|
|
9789
|
+
if (fs.existsSync(commandsDir)) {
|
|
9790
|
+
for (const f of fs.readdirSync(commandsDir)) {
|
|
9791
|
+
if (f.startsWith('gsd-') && f.endsWith('.md')) {
|
|
9792
|
+
fs.unlinkSync(path.join(commandsDir, f));
|
|
9793
|
+
}
|
|
9794
|
+
}
|
|
9795
|
+
}
|
|
9796
|
+
|
|
9797
|
+
// Write each command as gsd-<stem>.md (flat, hyphen-prefixed)
|
|
9798
|
+
let cmdCount = 0;
|
|
9799
|
+
if (fs.existsSync(gsdSrc)) {
|
|
9800
|
+
for (const entry of fs.readdirSync(gsdSrc, { withFileTypes: true })) {
|
|
9801
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
9802
|
+
const stem = entry.name.slice(0, -3);
|
|
9803
|
+
let content = fs.readFileSync(path.join(gsdSrc, entry.name), 'utf8');
|
|
9804
|
+
content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, getCommitAttribution(runtime));
|
|
9805
|
+
content = normalizeAgentBodyForRuntime(content, runtime, cmdNames);
|
|
9806
|
+
fs.writeFileSync(path.join(commandsDir, `gsd-${stem}.md`), content);
|
|
9807
|
+
cmdCount++;
|
|
9808
|
+
}
|
|
9809
|
+
}
|
|
9810
|
+
|
|
9811
|
+
if (cmdCount > 0) {
|
|
9812
|
+
console.log(` ${green}✓${reset} Installed ${cmdCount} commands to commands/ (gsd-<cmd>.md flat form)`);
|
|
9998
9813
|
} else {
|
|
9999
|
-
failures.push('commands/gsd');
|
|
9814
|
+
failures.push('commands/gsd-*');
|
|
9815
|
+
}
|
|
9816
|
+
|
|
9817
|
+
// Legacy cleanup: remove old commands/gsd/ subdirectory from prior installs
|
|
9818
|
+
// that used the namespaced layout (wrote bare-name files under commands/gsd/).
|
|
9819
|
+
const legacyGsdDir = path.join(commandsDir, 'gsd');
|
|
9820
|
+
if (fs.existsSync(legacyGsdDir)) {
|
|
9821
|
+
// Preserve user-owned dev-preferences.md before wiping
|
|
9822
|
+
const devPrefsPath = path.join(legacyGsdDir, 'dev-preferences.md');
|
|
9823
|
+
const preservedDevPrefs = fs.existsSync(devPrefsPath) ? fs.readFileSync(devPrefsPath, 'utf-8') : null;
|
|
9824
|
+
fs.rmSync(legacyGsdDir, { recursive: true });
|
|
9825
|
+
console.log(` ${green}✓${reset} Removed legacy commands/gsd/ (migrated to flat gsd-<cmd>.md layout)`);
|
|
9826
|
+
if (preservedDevPrefs) {
|
|
9827
|
+
// Migrate dev-preferences to the new flat form
|
|
9828
|
+
fs.writeFileSync(path.join(commandsDir, 'gsd-dev-preferences.md'), preservedDevPrefs);
|
|
9829
|
+
console.log(` ${green}✓${reset} Migrated dev-preferences.md to commands/gsd-dev-preferences.md`);
|
|
9830
|
+
}
|
|
10000
9831
|
}
|
|
10001
9832
|
|
|
10002
9833
|
// Clean up any stale skills/ from a previous local install
|
|
@@ -12217,7 +12048,10 @@ module.exports = {
|
|
|
12217
12048
|
// #1191 — exported so tests exercise the REAL readSettings, not a replica
|
|
12218
12049
|
readSettings,
|
|
12219
12050
|
stripJsonComments,
|
|
12220
|
-
|
|
12051
|
+
// Compatibility relays retained after auditing the former broad
|
|
12052
|
+
// runtimeArtifactConversion spread (#1559).
|
|
12053
|
+
processAttribution,
|
|
12054
|
+
applyRuntimeContentRewritesForCommandsInPlace,
|
|
12221
12055
|
};
|
|
12222
12056
|
|
|
12223
12057
|
// Main logic — only run when not loaded as a module for testing
|