@opengsd/gsd-core 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -11,6 +11,7 @@ const _require = require;
|
|
|
11
11
|
const path = _require('node:path');
|
|
12
12
|
const { tryWithinRootLexical } = _require('./security.cjs');
|
|
13
13
|
const { hostBehaviorsFor } = _require('./runtime-name-policy.cjs');
|
|
14
|
+
const { isMinimalMode } = _require('./install-profiles.cjs');
|
|
14
15
|
// #2870: InstallScope is owned by install-scope.cts, not re-declared here.
|
|
15
16
|
// `isGlobalScope` centralizes the `scope === 'global'` boolean projection
|
|
16
17
|
// this module needs at `_computePathPrefix`'s `isGlobal: boolean` boundary
|
|
@@ -49,6 +50,64 @@ function assertDestWithinConfigHome(configDir, destSubpath) {
|
|
|
49
50
|
}
|
|
50
51
|
return contained;
|
|
51
52
|
}
|
|
53
|
+
function resolveRuntimeArtifactDestination(configDir, kind) {
|
|
54
|
+
return assertDestWithinConfigHome(resolveRuntimeArtifactInstallRoot(configDir, kind), kind.destSubpath);
|
|
55
|
+
}
|
|
56
|
+
function resolveRuntimeArtifactInstallRoot(configDir, kind) {
|
|
57
|
+
if (kind?.home != null && typeof kind.home !== 'string') {
|
|
58
|
+
throw new TypeError('Artifact home must be a string, null, or undefined');
|
|
59
|
+
}
|
|
60
|
+
return (kind && typeof kind.home === 'string' && kind.home !== '') ? kind.home : configDir;
|
|
61
|
+
}
|
|
62
|
+
function shouldInstallCombinedFamily(behaviors) {
|
|
63
|
+
return Boolean(behaviors.combinedFamilyInstall);
|
|
64
|
+
}
|
|
65
|
+
function isLegacyFlatLocalInstall(behaviors, scope) {
|
|
66
|
+
return scope === 'local' && behaviors.localInstallStyle === 'legacy-flat';
|
|
67
|
+
}
|
|
68
|
+
function shouldInstallStandaloneAgents(pluginOnlyInstall, skillsRuntime) {
|
|
69
|
+
return !pluginOnlyInstall && !skillsRuntime;
|
|
70
|
+
}
|
|
71
|
+
function requiredRuntimeSurfaceSources(layout, scope) {
|
|
72
|
+
const required = new Set();
|
|
73
|
+
if (!(0, install_scope_cjs_1.isGlobalScope)(scope))
|
|
74
|
+
return required;
|
|
75
|
+
for (const kind of layout.kinds) {
|
|
76
|
+
if (kind.kind === 'commands' || kind.kind === 'skills')
|
|
77
|
+
required.add('commands');
|
|
78
|
+
if (kind.kind === 'agents' || kind.kind === 'kimi-agents')
|
|
79
|
+
required.add('agents');
|
|
80
|
+
}
|
|
81
|
+
return required;
|
|
82
|
+
}
|
|
83
|
+
function codexAgentRolesEnabled(installMode, resolvedProfile) {
|
|
84
|
+
return !isMinimalMode(installMode) || (resolvedProfile.agents instanceof Set && resolvedProfile.agents.size > 0);
|
|
85
|
+
}
|
|
86
|
+
function plannedActionsMutateCodexHooksInputs(actions) {
|
|
87
|
+
return actions.some((action) => (action.relPath === 'hooks.json' || action.relPath === 'config.toml') &&
|
|
88
|
+
['rewrite-json', 'remove-managed', 'backup-and-remove'].includes(action.type));
|
|
89
|
+
}
|
|
90
|
+
function copyWithPathReplacementSymlinkRefusalMessage(destDir, installRoot) {
|
|
91
|
+
return `copyWithPathReplacement: destDir "${destDir}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
|
|
92
|
+
}
|
|
93
|
+
function installRuntimeArtifactsSymlinkRefusalMessage(dest, installRoot) {
|
|
94
|
+
return `installRuntimeArtifacts: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to create. If this is an intentional user-owned symlink layout (e.g. externalized skills/hooks dir, multi-account configHome, or a dotfiles-managed configHome), re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
|
|
95
|
+
}
|
|
96
|
+
function installOpencodeFamilySkillsSymlinkRefusalMessage(dest, installRoot) {
|
|
97
|
+
return `installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
|
|
98
|
+
}
|
|
99
|
+
function installAgentsKindStandaloneSymlinkRefusalMessage(dest, installRoot) {
|
|
100
|
+
return `installAgentsKindStandalone: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
|
|
101
|
+
}
|
|
102
|
+
function syncRuntimeSurfaceCorpusSymlinkRefusalMessage(destination, installRoot) {
|
|
103
|
+
return `syncRuntimeSurfaceCorpus: destination "${destination}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write.`;
|
|
104
|
+
}
|
|
105
|
+
function installCodexConfigSymlinkRefusalMessage(targetDir) {
|
|
106
|
+
return `installCodexConfig: a Codex config path under "${targetDir}" contains a symlink the install root does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
|
|
107
|
+
}
|
|
108
|
+
function installCodexAgentTomlSymlinkRefusalMessage(agentTomlPath) {
|
|
109
|
+
return `installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink the install root does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
|
|
110
|
+
}
|
|
52
111
|
function errorMessage(err) {
|
|
53
112
|
if (err instanceof Error)
|
|
54
113
|
return err.message;
|
|
@@ -135,7 +194,7 @@ function createRuntimeArtifactInstallPlan(args) {
|
|
|
135
194
|
items.push({
|
|
136
195
|
kind: kind.kind,
|
|
137
196
|
sourceDir,
|
|
138
|
-
destDir:
|
|
197
|
+
destDir: resolveRuntimeArtifactDestination(layout.configDir, kind),
|
|
139
198
|
});
|
|
140
199
|
}
|
|
141
200
|
return { ok: true, plan: { items, cleanupDirs } };
|
|
@@ -144,8 +203,27 @@ function createRuntimeArtifactUninstallPlan(layout) {
|
|
|
144
203
|
return {
|
|
145
204
|
items: layout.kinds.map((kind) => ({
|
|
146
205
|
kind: kind.kind,
|
|
147
|
-
destDir:
|
|
206
|
+
destDir: resolveRuntimeArtifactDestination(layout.configDir, kind),
|
|
148
207
|
})),
|
|
149
208
|
};
|
|
150
209
|
}
|
|
151
|
-
module.exports = {
|
|
210
|
+
module.exports = {
|
|
211
|
+
assertDestWithinConfigHome,
|
|
212
|
+
resolveRuntimeArtifactDestination,
|
|
213
|
+
resolveRuntimeArtifactInstallRoot,
|
|
214
|
+
shouldInstallCombinedFamily,
|
|
215
|
+
isLegacyFlatLocalInstall,
|
|
216
|
+
shouldInstallStandaloneAgents,
|
|
217
|
+
requiredRuntimeSurfaceSources,
|
|
218
|
+
codexAgentRolesEnabled,
|
|
219
|
+
plannedActionsMutateCodexHooksInputs,
|
|
220
|
+
copyWithPathReplacementSymlinkRefusalMessage,
|
|
221
|
+
installRuntimeArtifactsSymlinkRefusalMessage,
|
|
222
|
+
installOpencodeFamilySkillsSymlinkRefusalMessage,
|
|
223
|
+
installAgentsKindStandaloneSymlinkRefusalMessage,
|
|
224
|
+
syncRuntimeSurfaceCorpusSymlinkRefusalMessage,
|
|
225
|
+
installCodexConfigSymlinkRefusalMessage,
|
|
226
|
+
installCodexAgentTomlSymlinkRefusalMessage,
|
|
227
|
+
createRuntimeArtifactInstallPlan,
|
|
228
|
+
createRuntimeArtifactUninstallPlan,
|
|
229
|
+
};
|
|
@@ -565,6 +565,12 @@ function convertedAgentsKind(destSubpath, prefix, converterName, configDir, sour
|
|
|
565
565
|
// (content) ignore the extra positional arg. Mirrors skillsKind's scope
|
|
566
566
|
// threading (#1173).
|
|
567
567
|
converter = (content) => rawConverter(content, (0, install_scope_cjs_1.isGlobalScope)(scope));
|
|
568
|
+
if (converterName === 'convertClaudeAgentToTraeAgent') {
|
|
569
|
+
// #5715: the Trae agent converter needs the prefix the path-rewrite step
|
|
570
|
+
// already emitted, so it can leave it alone.
|
|
571
|
+
const traeConverter = rawConverter;
|
|
572
|
+
converter = (content, _isGlobal, meta) => traeConverter(content, (0, install_scope_cjs_1.isGlobalScope)(scope), meta);
|
|
573
|
+
}
|
|
568
574
|
}
|
|
569
575
|
// ADR-1235 §1: when agentCtx is provided (by createRuntimeArtifactInstallPlan
|
|
570
576
|
// for descriptor-driven runtimes), thread it through so stageAgentsForRuntimeWithConverter
|
|
@@ -31,6 +31,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
31
31
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
32
32
|
const node_path_1 = __importDefault(require("node:path"));
|
|
33
33
|
const node_os_1 = __importDefault(require("node:os"));
|
|
34
|
+
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
34
35
|
// #2544: the single source of truth for the CommonJS module-type marker. The
|
|
35
36
|
// two helpers below are thin boolean-returning shims over these — see the
|
|
36
37
|
// marker section for why this file no longer carries its own copy.
|
|
@@ -615,6 +616,182 @@ function resolveBashRunner(opts) {
|
|
|
615
616
|
? JSON.stringify(executable)
|
|
616
617
|
: executable;
|
|
617
618
|
}
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// #5850 — PowerShell hook runner (Claude Code on Windows)
|
|
621
|
+
// ---------------------------------------------------------------------------
|
|
622
|
+
/**
|
|
623
|
+
* #5850 (owner ruling: always PowerShell on win32): every managed JS hook command
|
|
624
|
+
* Claude gets on Windows is a PowerShell command carrying an explicit
|
|
625
|
+
* `shell: "powershell"` (see projectPowerShellHookShell), whether or not Git Bash
|
|
626
|
+
* is discoverable. The POSIX `$(for ... done)` runner chain does not parse when
|
|
627
|
+
* Claude Code hosts the hook in PowerShell, and a blocking PreToolUse guard that
|
|
628
|
+
* cannot start blocks every tool call. An explicit `hookShell: 'bash'` still
|
|
629
|
+
* selects the chain (a test/projection seam, never set by the installer).
|
|
630
|
+
*/
|
|
631
|
+
function usesPowerShellHookShell(opts) {
|
|
632
|
+
if (opts && opts.hookShell === 'bash')
|
|
633
|
+
return false;
|
|
634
|
+
const platform = (opts && opts.platform) || process.platform;
|
|
635
|
+
const runtime = (opts && opts.runtime) || 'generic';
|
|
636
|
+
return platform === 'win32' && (0, runtime_name_policy_cjs_1.hostBehaviorsFor)(runtime).powershellHooksOnWindows === true;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* A PowerShell single-quoted string literal. PowerShell treats U+2018..U+201B
|
|
640
|
+
* as single-quote delimiters as well as the ASCII quote, and each is escaped by
|
|
641
|
+
* doubling itself.
|
|
642
|
+
*/
|
|
643
|
+
function powerShellQuote(value) {
|
|
644
|
+
return `'${value.replace(/['\u2018\u2019\u201A\u201B]/g, (q) => q + q)}'`;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* A hook script path as a PowerShell argument. A path anchored on
|
|
648
|
+
* `"$CLAUDE_PROJECT_DIR"/...` (local installs) or `$HOME/...` (portable installs)
|
|
649
|
+
* becomes an expanding PowerShell expression; any other path is a single-quoted
|
|
650
|
+
* literal. A path carrying some OTHER shell variable (`$FOO`) is a POSIX token
|
|
651
|
+
* with no PowerShell spelling here: null, and the caller leaves the entry alone.
|
|
652
|
+
*/
|
|
653
|
+
function powerShellScriptArg(scriptPath, opts = {}) {
|
|
654
|
+
const projectDir = scriptPath.match(/^"\$CLAUDE_PROJECT_DIR"(\/.*)$/);
|
|
655
|
+
const homeDir = scriptPath.match(/^\$HOME(\/.*)$/);
|
|
656
|
+
const anchored = projectDir || homeDir;
|
|
657
|
+
if (anchored) {
|
|
658
|
+
const variable = projectDir ? '$env:CLAUDE_PROJECT_DIR' : '$HOME';
|
|
659
|
+
const suffix = anchored[1];
|
|
660
|
+
// Plain suffixes keep the readable double-quoted expansion; any character
|
|
661
|
+
// PowerShell treats specially inside double quotes (including the curly
|
|
662
|
+
// double quotes) moves the suffix into a single-quoted literal.
|
|
663
|
+
if (!/["`$\u201C\u201D\u201E]/.test(suffix))
|
|
664
|
+
return `"${variable}${suffix}"`;
|
|
665
|
+
return `(${variable} + ${powerShellQuote(suffix)})`;
|
|
666
|
+
}
|
|
667
|
+
// A real filesystem path (the installer's own configDir) may contain `$`; a
|
|
668
|
+
// token read back from settings.json may not (it would never expand).
|
|
669
|
+
if (!opts.literal && scriptPath.includes('$'))
|
|
670
|
+
return null;
|
|
671
|
+
return powerShellQuote(shellCmdProjection.posixNormalize(scriptPath));
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* The PowerShell counterpart of buildNodeRunnerChainToken: the baked install-time
|
|
675
|
+
* node first (the minimal-PATH guarantee), then node on PATH, resolved when the
|
|
676
|
+
* hook fires. `scriptToken` is an already-quoted PowerShell argument.
|
|
677
|
+
*/
|
|
678
|
+
function buildPowerShellHookCommand(scriptToken, opts) {
|
|
679
|
+
const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
|
|
680
|
+
if (!execPath)
|
|
681
|
+
return null;
|
|
682
|
+
const baked = powerShellQuote(shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts)));
|
|
683
|
+
// The `exit` tail: `powershell -Command` collapses a native exit code to 0/1
|
|
684
|
+
// unless the script exits with it, and a blocking guard denies with exit 2. A
|
|
685
|
+
// runner that never started leaves $LASTEXITCODE null and exits 1, not 0.
|
|
686
|
+
return `& ((@(${baked}) + @(Get-Command node -CommandType Application -ErrorAction SilentlyContinue | Where-Object { $_.Source -like '*.exe' } | ForEach-Object { $_.Source })) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1) ${scriptToken}; exit $(if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 1 })`;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Local installs anchor the script under `"$CLAUDE_PROJECT_DIR"/<dir>`; the
|
|
690
|
+
* PowerShell spelling of that variable is `$env:CLAUDE_PROJECT_DIR`.
|
|
691
|
+
*/
|
|
692
|
+
function buildPowerShellLocalHookCommand(opts) {
|
|
693
|
+
if (!opts.localPrefix || !opts.hookFile)
|
|
694
|
+
return null;
|
|
695
|
+
const arg = powerShellScriptArg(`${opts.localPrefix}/hooks/${opts.hookFile}`);
|
|
696
|
+
return arg === null ? null : buildPowerShellHookCommand(arg, opts);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* The statusLine spelling. Claude Code runs a statusLine command through Git Bash
|
|
700
|
+
* when it is installed and through PowerShell when it is not, and a statusLine has
|
|
701
|
+
* no `shell` key to pin that. One command line that both hosts run is
|
|
702
|
+
* `powershell -NoProfile -Command "..."` with a body free of `$`, backticks and
|
|
703
|
+
* backslashes (all special inside bash double quotes). null when a path carries
|
|
704
|
+
* one of those, so the statusLine is skipped rather than written broken.
|
|
705
|
+
*/
|
|
706
|
+
function buildPowerShellStatusLineCommand(scriptPath, opts) {
|
|
707
|
+
const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
|
|
708
|
+
if (!execPath)
|
|
709
|
+
return null;
|
|
710
|
+
const baked = shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts));
|
|
711
|
+
const script = shellCmdProjection.posixNormalize(scriptPath);
|
|
712
|
+
if ([baked, script].some((p) => /["`$\\]/.test(p)))
|
|
713
|
+
return null;
|
|
714
|
+
const b = powerShellQuote(baked);
|
|
715
|
+
const sc = powerShellQuote(script);
|
|
716
|
+
return `powershell -NoProfile -Command "if (Test-Path -LiteralPath ${b}) { & ${b} ${sc} } else { & node ${sc} }"`;
|
|
717
|
+
}
|
|
718
|
+
const POWERSHELL_STATUSLINE_COMMAND = /^powershell -NoProfile -Command "if \(Test-Path -LiteralPath '(?:[^']|'')*'\) \{ & '(?:[^']|'')*' '((?:[^']|'')*)' \} else \{ & node '((?:[^']|'')*)' \}"$/;
|
|
719
|
+
function unquotePowerShell(value) {
|
|
720
|
+
return value.replace(/(['\u2018\u2019\u201A\u201B])\1/g, '$1');
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Read the script back out of a PowerShell runner or statusLine command, as the
|
|
724
|
+
* POSIX path/token pair projectLegacySettingsHookCommand takes. null when the
|
|
725
|
+
* command is not one of GSD's PowerShell shapes.
|
|
726
|
+
*/
|
|
727
|
+
function parsePowerShellScript(command) {
|
|
728
|
+
// A path bash would expand or re-quote (`$`, backtick, `"`) has no faithful POSIX
|
|
729
|
+
// spelling here: refuse, and the entry stays as written. (A portable install under
|
|
730
|
+
// a directory named with one of them therefore stays PowerShell on a POSIX host.)
|
|
731
|
+
const posixUnsafe = /["`$]/;
|
|
732
|
+
const status = command.match(POWERSHELL_STATUSLINE_COMMAND);
|
|
733
|
+
if (status) {
|
|
734
|
+
const scriptPath = unquotePowerShell(status[2]);
|
|
735
|
+
return posixUnsafe.test(scriptPath) ? null : { scriptPath, scriptToken: JSON.stringify(scriptPath) };
|
|
736
|
+
}
|
|
737
|
+
const runner = command.replace(/^&\s+/, '').trim().match(/^\(\(@\('(?:[^']|'')*'\) \+ @\(Get-Command node [\s\S]*?Select-Object -First 1\)\s+(.+?); exit \$\(if [\s\S]*\)$/);
|
|
738
|
+
if (!runner)
|
|
739
|
+
return null;
|
|
740
|
+
const arg = runner[1];
|
|
741
|
+
let m = arg.match(/^'((?:[^']|'')*)'$/);
|
|
742
|
+
if (m) {
|
|
743
|
+
const scriptPath = unquotePowerShell(m[1]);
|
|
744
|
+
return posixUnsafe.test(scriptPath) ? null : { scriptPath, scriptToken: JSON.stringify(scriptPath) };
|
|
745
|
+
}
|
|
746
|
+
m = arg.match(/^"\$env:CLAUDE_PROJECT_DIR(\/[^"]*)"$/) || arg.match(/^\(\$env:CLAUDE_PROJECT_DIR \+ '((?:[^']|'')*)'\)$/);
|
|
747
|
+
if (m) {
|
|
748
|
+
const rel = unquotePowerShell(m[1]);
|
|
749
|
+
if (posixUnsafe.test(rel))
|
|
750
|
+
return null;
|
|
751
|
+
const token = `"$CLAUDE_PROJECT_DIR"${rel}`;
|
|
752
|
+
return { scriptPath: token, scriptToken: token };
|
|
753
|
+
}
|
|
754
|
+
m = arg.match(/^"\$HOME(\/[^"]*)"$/) || arg.match(/^\(\$HOME \+ '((?:[^']|'')*)'\)$/);
|
|
755
|
+
if (m) {
|
|
756
|
+
const rel = unquotePowerShell(m[1]);
|
|
757
|
+
if (posixUnsafe.test(rel))
|
|
758
|
+
return null;
|
|
759
|
+
return { scriptPath: `$HOME${rel}`, scriptToken: `"$HOME${rel}"` };
|
|
760
|
+
}
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
// Recognizes buildPowerShellHookCommand's output once the `& ` call operator is stripped.
|
|
764
|
+
const POWERSHELL_RUNNER_COMMAND = /^\(\(@\('(?:[^']|'')*'\) \+ @\(Get-Command node [\s\S]*?Select-Object -First 1\)\s+\S/;
|
|
765
|
+
function isPowerShellRunnerCommand(command) {
|
|
766
|
+
return typeof command === 'string' && /^&\s+/.test(command) && POWERSHELL_RUNNER_COMMAND.test(command.replace(/^&\s+/, '').trim());
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Set the explicit `shell: "powershell"` on every hook whose command is the
|
|
770
|
+
* PowerShell runner, so Claude Code does not depend on its own Git Bash
|
|
771
|
+
* detection to pick the shell. Returns true when a hook changed. Idempotent.
|
|
772
|
+
*/
|
|
773
|
+
function projectPowerShellHookShell(settings) {
|
|
774
|
+
if (!settings || !settings.hooks)
|
|
775
|
+
return false;
|
|
776
|
+
let changed = false;
|
|
777
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
778
|
+
if (!Array.isArray(entries))
|
|
779
|
+
continue;
|
|
780
|
+
for (const entry of entries) {
|
|
781
|
+
if (!entry || !Array.isArray(entry.hooks))
|
|
782
|
+
continue;
|
|
783
|
+
for (const h of entry.hooks) {
|
|
784
|
+
if (!h || !isPowerShellRunnerCommand(h.command) || !isManagedHookCommand(h.command, { surface: 'settings-json' }))
|
|
785
|
+
continue;
|
|
786
|
+
if (h.shell === 'powershell')
|
|
787
|
+
continue;
|
|
788
|
+
h.shell = 'powershell';
|
|
789
|
+
changed = true;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return changed;
|
|
794
|
+
}
|
|
618
795
|
// #3662 — recognize the two runtime-resolving command shapes the installer
|
|
619
796
|
// emits, so the rewriter never churns (or un-does) an entry that already
|
|
620
797
|
// works in every environment sharing the config root.
|
|
@@ -626,8 +803,31 @@ function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
|
|
|
626
803
|
if (!opts)
|
|
627
804
|
opts = {};
|
|
628
805
|
const platform = opts.platform || process.platform;
|
|
806
|
+
const usePowerShell = usesPowerShellHookShell({ platform, runtime: opts.runtime });
|
|
629
807
|
let changed = false;
|
|
630
|
-
|
|
808
|
+
// #5850: the managed statusLine is a command like a hook. It is converted to the
|
|
809
|
+
// shell-neutral PowerShell spelling on the PowerShell path, and back to the POSIX
|
|
810
|
+
// chain when a PowerShell statusLine reaches a POSIX host (shared config root).
|
|
811
|
+
const statusLine = settings.statusLine;
|
|
812
|
+
const statusLineGroups = statusLine && typeof statusLine.command === 'string'
|
|
813
|
+
&& (usePowerShell || POWERSHELL_STATUSLINE_COMMAND.test(statusLine.command))
|
|
814
|
+
? [{ hooks: [statusLine] }]
|
|
815
|
+
: [];
|
|
816
|
+
const toPowerShell = (h, scriptPath) => (h === statusLine
|
|
817
|
+
? buildPowerShellStatusLineCommand(scriptPath, opts)
|
|
818
|
+
: (() => {
|
|
819
|
+
const arg = powerShellScriptArg(scriptPath);
|
|
820
|
+
return arg === null ? null : buildPowerShellHookCommand(arg, opts);
|
|
821
|
+
})());
|
|
822
|
+
const assign = (h, command) => {
|
|
823
|
+
if (!command || h.command === command)
|
|
824
|
+
return;
|
|
825
|
+
h.command = command;
|
|
826
|
+
if (h.shell === 'powershell' && !usePowerShell)
|
|
827
|
+
delete h.shell;
|
|
828
|
+
changed = true;
|
|
829
|
+
};
|
|
830
|
+
for (const entries of [...Object.values(settings.hooks), statusLineGroups]) {
|
|
631
831
|
if (!Array.isArray(entries))
|
|
632
832
|
continue;
|
|
633
833
|
for (const entry of entries) {
|
|
@@ -638,12 +838,53 @@ function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
|
|
|
638
838
|
continue;
|
|
639
839
|
if (Array.isArray(h.args) && h.args.length > 0)
|
|
640
840
|
continue;
|
|
841
|
+
if (!usePowerShell && platform !== 'win32') {
|
|
842
|
+
// #5850: a PowerShell entry on a POSIX host (a config root shared with
|
|
843
|
+
// Windows) is a syntax error in bash; converge it back to the POSIX chain.
|
|
844
|
+
const ps = parsePowerShellScript(h.command);
|
|
845
|
+
if (ps) {
|
|
846
|
+
if (!isManagedHookBasename(ps.scriptPath, { surface: 'settings-json' }))
|
|
847
|
+
continue;
|
|
848
|
+
assign(h, projectLegacySettingsHookCommand({
|
|
849
|
+
runnerToken,
|
|
850
|
+
scriptPath: ps.scriptPath,
|
|
851
|
+
scriptToken: ps.scriptToken,
|
|
852
|
+
runtime: opts.runtime || 'generic',
|
|
853
|
+
platform,
|
|
854
|
+
}));
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
641
858
|
let trimmed = h.command.trim();
|
|
642
859
|
const hadPowerShellCallOperator = platform === 'win32' && /^&\s+/.test(trimmed);
|
|
643
860
|
if (hadPowerShellCallOperator) {
|
|
644
861
|
trimmed = trimmed.replace(/^&\s+/, '').trim();
|
|
645
862
|
}
|
|
646
|
-
if (
|
|
863
|
+
if (usePowerShell) {
|
|
864
|
+
// #5850: already the PowerShell runner: nothing to do. A POSIX chain
|
|
865
|
+
// (or a legacy bare/foreign-node entry) is re-projected below.
|
|
866
|
+
if (POWERSHELL_RUNNER_COMMAND.test(trimmed) || POWERSHELL_STATUSLINE_COMMAND.test(trimmed))
|
|
867
|
+
continue;
|
|
868
|
+
const chain = trimmed.match(/^"\$\(for n in [\s\S]*?printf '%s' "\$n" && break; done\)"\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/);
|
|
869
|
+
if (chain) {
|
|
870
|
+
const chainScript = chain[1] || chain[2] || chain[3] || '';
|
|
871
|
+
if (!isManagedHookBasename(chainScript, { surface: 'settings-json' }))
|
|
872
|
+
continue;
|
|
873
|
+
assign(h, toPowerShell(h, chainScript));
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
// The #3662 bash resolver shape needs bash too; its script is a plain
|
|
877
|
+
// or `$HOME`-relative path, both spelled for PowerShell.
|
|
878
|
+
const resolver = trimmed.match(/^(?:"[^"]*bash(?:\.exe)?"|bash)\s+"[^"]*gsd-node-runner\.sh"\s+"[^"]*"\s+(?:"([^"]+)"|(\S+))\s*$/);
|
|
879
|
+
if (resolver) {
|
|
880
|
+
const resolverScript = resolver[1] || resolver[2] || '';
|
|
881
|
+
if (!isManagedHookBasename(resolverScript, { surface: 'settings-json' }))
|
|
882
|
+
continue;
|
|
883
|
+
assign(h, toPowerShell(h, resolverScript));
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (CHAIN_RUNNER_COMMAND.test(trimmed) || RESOLVER_RUNNER_COMMAND.test(trimmed) || POWERSHELL_RUNNER_COMMAND.test(trimmed))
|
|
647
888
|
continue;
|
|
648
889
|
const m = trimmed.match(/^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/) ||
|
|
649
890
|
trimmed.match(/^("([^"]+)"|'([^']+)'|(\S+))\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/);
|
|
@@ -666,6 +907,10 @@ function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
|
|
|
666
907
|
}
|
|
667
908
|
if (!isManagedHookBasename(scriptPath, { surface: 'settings-json' }))
|
|
668
909
|
continue;
|
|
910
|
+
if (usePowerShell) {
|
|
911
|
+
assign(h, toPowerShell(h, scriptPath));
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
669
914
|
const projectedCommand = projectLegacySettingsHookCommand({
|
|
670
915
|
runnerToken,
|
|
671
916
|
scriptPath,
|
|
@@ -831,12 +1076,9 @@ function _installEngineSymlinkGuard() {
|
|
|
831
1076
|
const mod = require('./install-engine.cjs');
|
|
832
1077
|
return mod;
|
|
833
1078
|
}
|
|
834
|
-
|
|
1079
|
+
// Shared by install preflight and the hooks writer; resolving never writes.
|
|
1080
|
+
function resolveCodexHooksJsonDestination(targetDir) {
|
|
835
1081
|
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
836
|
-
const managedCommand = typeof opts.managedCommand === 'string' ? opts.managedCommand : null;
|
|
837
|
-
const commandWindows = typeof opts.commandWindows === 'string' ? opts.commandWindows : null;
|
|
838
|
-
const matcher = typeof opts.matcher === 'string' ? opts.matcher : undefined;
|
|
839
|
-
const timeout = typeof opts.timeout === 'number' ? opts.timeout : undefined;
|
|
840
1082
|
// #2586 Major 2: every Codex hooks.json writer funnels through this one
|
|
841
1083
|
// function, and atomicWriteFileSync's final step is a rename(2) onto
|
|
842
1084
|
// `hooksJsonPath` — which, when that path is a symlink, REPLACES the
|
|
@@ -868,6 +1110,15 @@ function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
|
868
1110
|
// is supposed to mean.
|
|
869
1111
|
effectiveHooksJsonPath = node_fs_1.default.realpathSync(hooksJsonPath);
|
|
870
1112
|
}
|
|
1113
|
+
return effectiveHooksJsonPath;
|
|
1114
|
+
}
|
|
1115
|
+
function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
|
|
1116
|
+
const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
|
|
1117
|
+
const managedCommand = typeof opts.managedCommand === 'string' ? opts.managedCommand : null;
|
|
1118
|
+
const commandWindows = typeof opts.commandWindows === 'string' ? opts.commandWindows : null;
|
|
1119
|
+
const matcher = typeof opts.matcher === 'string' ? opts.matcher : undefined;
|
|
1120
|
+
const timeout = typeof opts.timeout === 'number' ? opts.timeout : undefined;
|
|
1121
|
+
const effectiveHooksJsonPath = resolveCodexHooksJsonDestination(targetDir);
|
|
871
1122
|
let parsed = {};
|
|
872
1123
|
let currentContent = null;
|
|
873
1124
|
if (node_fs_1.default.existsSync(effectiveHooksJsonPath)) {
|
|
@@ -1280,7 +1531,8 @@ function configuredEntrypointsForHook(configDir, hookName, opts) {
|
|
|
1280
1531
|
'/usr/local/bin/node',
|
|
1281
1532
|
'/usr/bin/node',
|
|
1282
1533
|
].filter((candidate) => Boolean(candidate));
|
|
1283
|
-
|
|
1534
|
+
// #5850: the PowerShell runner needs no bash resolver, portable or not.
|
|
1535
|
+
if (!opts.portableHooks || usesPowerShellHookShell({ platform, runtime, hookShell: opts.hookShell })) {
|
|
1284
1536
|
return [{ ...target, interpreterCandidates: nodeCandidates }];
|
|
1285
1537
|
}
|
|
1286
1538
|
const runner = {
|
|
@@ -1363,6 +1615,20 @@ function buildHookCommand(configDir, hookName, opts) {
|
|
|
1363
1615
|
// baked as a bare absolute path — an install-environment absolute path is
|
|
1364
1616
|
// exactly what breaks with exit 127 when the config root is shared across
|
|
1365
1617
|
// environments with different node layouts.
|
|
1618
|
+
// #5850: Claude on win32 always gets the PowerShell runner (owner ruling),
|
|
1619
|
+
// portable installs included: the `$HOME`-relative script path is spelled for
|
|
1620
|
+
// PowerShell and needs no bash resolver.
|
|
1621
|
+
if (usesPowerShellHookShell({ platform, runtime, hookShell })) {
|
|
1622
|
+
const scriptPath = opts.portableHooks
|
|
1623
|
+
? `${projectPortableHookBaseDir({ configDir, homeDir: node_os_1.default.homedir() })}/hooks/${hookName}`
|
|
1624
|
+
: shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
|
|
1625
|
+
// A statusLine is not shared across hosts and has no `$`-free `$HOME` spelling:
|
|
1626
|
+
// it always carries the real absolute path, portable or not.
|
|
1627
|
+
if (opts.statusLine) {
|
|
1628
|
+
return track(buildPowerShellStatusLineCommand(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName, opts));
|
|
1629
|
+
}
|
|
1630
|
+
return track(buildPowerShellHookCommand(powerShellScriptArg(scriptPath, { literal: true }), opts));
|
|
1631
|
+
}
|
|
1366
1632
|
if (opts.portableHooks) {
|
|
1367
1633
|
// Portable installs route through the staged resolver: the baked absolute
|
|
1368
1634
|
// path travels as the resolver's FIRST argument (tried first, so the
|
|
@@ -2852,6 +3118,7 @@ module.exports = {
|
|
|
2852
3118
|
writeCopilotHookConfig,
|
|
2853
3119
|
GSD_COPILOT_HOOK_FILE,
|
|
2854
3120
|
// Codex hooks.json
|
|
3121
|
+
resolveCodexHooksJsonDestination,
|
|
2855
3122
|
reconcileCodexHooksJsonEvent,
|
|
2856
3123
|
reconcileCodexHooksJsonSessionStart,
|
|
2857
3124
|
ensureCodexHooksJsonSessionStart,
|
|
@@ -2892,6 +3159,10 @@ module.exports = {
|
|
|
2892
3159
|
normalizeNodePath,
|
|
2893
3160
|
resolveNodeRunner,
|
|
2894
3161
|
buildNodeRunnerChainToken,
|
|
3162
|
+
usesPowerShellHookShell,
|
|
3163
|
+
buildPowerShellHookCommand,
|
|
3164
|
+
buildPowerShellLocalHookCommand,
|
|
3165
|
+
projectPowerShellHookShell,
|
|
2895
3166
|
resolveBashExecutable,
|
|
2896
3167
|
resolveBashRunner,
|
|
2897
3168
|
NODE_RUNNER_RESOLVER_HOOK,
|
|
@@ -40,6 +40,9 @@ exports.runtimeCommand = runtimeCommand;
|
|
|
40
40
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
42
42
|
const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
|
|
44
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
45
|
+
const { readConfigFileBounded } = configFileReadMod;
|
|
43
46
|
function formatGsdSlash(commandName, runtime) {
|
|
44
47
|
if (typeof commandName !== 'string')
|
|
45
48
|
return commandName;
|
|
@@ -171,9 +174,11 @@ function readProjectConfig(projectDir) {
|
|
|
171
174
|
return null;
|
|
172
175
|
try {
|
|
173
176
|
const configPath = node_path_1.default.join(projectDir, '.planning', 'config.json');
|
|
174
|
-
|
|
177
|
+
// Bounded read (#5759): null is an absent file; a FIFO/device/oversized one throws and degrades to null.
|
|
178
|
+
const raw = readConfigFileBounded(configPath);
|
|
179
|
+
if (raw === null)
|
|
175
180
|
return null;
|
|
176
|
-
const parsed = JSON.parse(
|
|
181
|
+
const parsed = JSON.parse(raw);
|
|
177
182
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
178
183
|
}
|
|
179
184
|
catch {
|