@opengsd/gsd-core 1.20.1 → 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.
Files changed (135) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +234 -126
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/gsd-core/bin/gsd-tools.cjs +450 -753
  11. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  12. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  13. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  14. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  15. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  16. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  17. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  18. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  19. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  20. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  21. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  22. package/gsd-core/bin/lib/commands.cjs +80 -2
  23. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  24. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  25. package/gsd-core/bin/lib/config.cjs +139 -101
  26. package/gsd-core/bin/lib/configuration.cjs +41 -2
  27. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  28. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  29. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  30. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  33. package/gsd-core/bin/lib/graphify.cjs +19 -2
  34. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  35. package/gsd-core/bin/lib/init.cjs +13 -11
  36. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  37. package/gsd-core/bin/lib/install-engine.cjs +11 -19
  38. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  39. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  40. package/gsd-core/bin/lib/io.cjs +6 -0
  41. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  42. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  43. package/gsd-core/bin/lib/milestone.cjs +8 -6
  44. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  45. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  46. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  47. package/gsd-core/bin/lib/phase.cjs +107 -31
  48. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  49. package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
  50. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  51. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  52. package/gsd-core/bin/lib/project-context.cjs +439 -0
  53. package/gsd-core/bin/lib/project-root.cjs +9 -230
  54. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  55. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  56. package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
  57. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  58. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  59. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
  60. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  61. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  62. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  63. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  64. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  65. package/gsd-core/bin/lib/state-transition.cjs +91 -41
  66. package/gsd-core/bin/lib/state.cjs +446 -61
  67. package/gsd-core/bin/lib/surface.cjs +4 -7
  68. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  69. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  70. package/gsd-core/bin/lib/verify.cjs +49 -6
  71. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  73. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  74. package/gsd-core/references/checkpoints.md +1 -1
  75. package/gsd-core/references/planner-guidance.md +11 -0
  76. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  77. package/gsd-core/references/planner-reversibility.md +2 -0
  78. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  79. package/gsd-core/workflows/autonomous.md +4 -4
  80. package/gsd-core/workflows/code-review-fix.md +5 -7
  81. package/gsd-core/workflows/execute-phase.md +21 -7
  82. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  83. package/gsd-core/workflows/plan-phase.md +18 -19
  84. package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
  85. package/hooks/dist/gsd-config-reload.js +2 -2
  86. package/hooks/dist/gsd-context-monitor.js +2 -1
  87. package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
  88. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  89. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-workflow-guard.js +2 -1
  92. package/hooks/dist/lib/config-read.js +49 -0
  93. package/hooks/dist/lib/git-cmd.js +3 -1
  94. package/hooks/dist/lib/git-head-tree.js +474 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +97 -17
  96. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  97. package/hooks/gsd-agent-isolation-guard.js +3 -2
  98. package/hooks/gsd-config-reload.js +2 -2
  99. package/hooks/gsd-context-monitor.js +2 -1
  100. package/hooks/gsd-cursor-subagent-start.js +8 -5
  101. package/hooks/gsd-read-injection-scanner.js +2 -2
  102. package/hooks/gsd-secret-read-guard.js +141 -6
  103. package/hooks/gsd-statusline.js +3 -2
  104. package/hooks/gsd-workflow-guard.js +2 -1
  105. package/hooks/lib/config-read.js +49 -0
  106. package/hooks/lib/git-cmd.js +3 -1
  107. package/hooks/lib/git-head-tree.js +474 -0
  108. package/hooks/lib/isolation-sentinel.js +97 -17
  109. package/hooks/lib/secret-read-exemptions.js +161 -0
  110. package/package.json +7 -2
  111. package/pi/package-agents/gsd-executor.md +1 -1
  112. package/pi/package-agents/gsd-plan-checker.md +1 -1
  113. package/pi/package-agents/gsd-planner.md +3 -3
  114. package/pi/package-agents/gsd-verifier.md +2 -2
  115. package/pi/package-agents/manifest.json +4 -4
  116. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  117. package/scripts/changeset/cli.cjs +2 -2
  118. package/scripts/changeset/serialize.cjs +12 -2
  119. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  120. package/scripts/ci-fix-next-rerun.cjs +149 -0
  121. package/scripts/ci-next-health.cjs +52 -9
  122. package/scripts/ci-test-scope.cjs +77 -71
  123. package/scripts/command-contract-helpers.cjs +56 -7
  124. package/scripts/docs-guard-registry.cjs +3 -0
  125. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  126. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  127. package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
  128. package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
  129. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  130. package/scripts/lint-removed-but-needed.cjs +562 -50
  131. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  132. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  133. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  134. package/skills/gsd-autonomous/SKILL.md +1 -1
  135. package/vscode/package.json +1 -1
@@ -14,6 +14,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
15
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
16
16
  const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
18
+ const configFileReadMod = require("./config-file-read.cjs");
19
+ const { readConfigFileBounded } = configFileReadMod;
17
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports
18
21
  const planningWorkspace = require("./planning-workspace.cjs");
19
22
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -882,7 +885,10 @@ function computeBracketPlan(cwd) {
882
885
  };
883
886
  let configData = {};
884
887
  try {
885
- configData = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
888
+ // Bounded read (#5759): a FIFO/device/oversized config is treated like an absent one.
889
+ const rawConfig = readConfigFileBounded(configPath);
890
+ if (rawConfig !== null)
891
+ configData = JSON.parse(rawConfig);
886
892
  }
887
893
  catch { /* config may not exist */ }
888
894
  const projectCode = typeof configData['project_code'] === 'string' && configData['project_code'].length > 0
@@ -1086,7 +1092,7 @@ function computeBracketPlan(cwd) {
1086
1092
  // never `null` (unlimited). Comparing under a DIFFERENT rule than the one
1087
1093
  // that created the directory refused two-milestone roadmaps the
1088
1094
  // tooling's own commands produced.
1089
- const slugify = (text) => (coreUtilsMod.generateSlugInternal(text.replace(/\(INSERTED\)/i, '').trim()) ?? '');
1095
+ const slugify = (text) => (coreUtilsMod.generateSlugInternal((0, phase_entry_grammar_cjs_1.stripInsertedMarker)(text)) ?? '');
1090
1096
  // #4144 round 7 W2, corrected round 8 B1: a plan-level ambiguity check
1091
1097
  // that runs BEFORE the resolution loop below and never touches its
1092
1098
  // (unmodified, round-6) algorithm. For every directory that structurally
@@ -1621,7 +1627,10 @@ function computeMigrationPlan(cwd, options = {}) {
1621
1627
  // ── Check config for existing convention ─────────────────────────────────
1622
1628
  let configData = {};
1623
1629
  try {
1624
- configData = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
1630
+ // Bounded read (#5759): a FIFO/device/oversized config is treated like an absent one.
1631
+ const rawConfig = readConfigFileBounded(configPath);
1632
+ if (rawConfig !== null)
1633
+ configData = JSON.parse(rawConfig);
1625
1634
  }
1626
1635
  catch { /* config may not exist */ }
1627
1636
  if (configData['phase_id_convention'] === 'milestone-prefixed') {
@@ -2110,9 +2119,19 @@ function applyMigration(cwd, plan, options = {}) {
2110
2119
  if (plan.phases.length > 0 || plan.roadmapEdits.length > 0) {
2111
2120
  let configData = {};
2112
2121
  try {
2113
- configData = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
2122
+ // Bounded read (#5759): the stamp below is written to configPath, so a FIFO/device/
2123
+ // oversized config is refused (and rolled back) rather than read or written blindly.
2124
+ const rawConfig = readConfigFileBounded(configPath);
2125
+ if (rawConfig !== null)
2126
+ configData = JSON.parse(rawConfig);
2127
+ }
2128
+ catch (err) {
2129
+ const code = err.code;
2130
+ if (code === 'ENXIO' || code === 'EISDIR' || code === 'EFBIG') {
2131
+ throw new Error(`config.json cannot be updated: it is not a regular file within the size limit (${code})`);
2132
+ }
2133
+ /* config may not exist yet, or may not parse */
2114
2134
  }
2115
- catch { /* config may not exist yet */ }
2116
2135
  configData['phase_id_convention'] = plan.targetConvention ?? 'milestone-prefixed';
2117
2136
  snapshotFile(configPath);
2118
2137
  try {
@@ -171,7 +171,7 @@ function searchPhaseInContent(content, escapedPhase, phaseNum, convention) {
171
171
  }
172
172
  return null;
173
173
  }
174
- const phaseName = headerMatch[1].trim();
174
+ const phaseName = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(headerMatch[1]);
175
175
  const headerIndex = headings[headingIndex].offset;
176
176
  const currentHeading = headings[headingIndex];
177
177
  const nextHeading = headings
@@ -395,7 +395,7 @@ function collectAnalyzePhases(cwd, content, phasesDir, phaseDirNames, convention
395
395
  if (isSentinelPhase(phaseNum, bracketId))
396
396
  continue;
397
397
  detailKeys.add(occurrenceKey(phaseNum, bracketId));
398
- const phaseName = match[2 + G].replace(/\(INSERTED\)/i, '').trim();
398
+ const phaseName = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(match[2 + G]);
399
399
  // Extract goal from the section
400
400
  const sectionStart = match.index;
401
401
  const restOfContent = content.slice(sectionStart);
@@ -2779,9 +2779,10 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
2779
2779
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
2780
2780
  const description = extractFrontmatterField(frontmatter, 'description') || '';
2781
2781
  const color = extractFrontmatterField(frontmatter, 'color');
2782
- const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
2783
- // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array
2784
- const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
2782
+ // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array.
2783
+ // parseFrontmatterTools reads both the inline comma list and the YAML block
2784
+ // list (#5790).
2785
+ const claudeTools = parseFrontmatterTools(frontmatter);
2785
2786
  const mappedTools = claudeTools.map(t => convertCopilotToolName(t));
2786
2787
  const uniqueTools = [...new Set(mappedTools)];
2787
2788
  const toolsArray = uniqueTools.length > 0
@@ -3187,7 +3188,9 @@ function convertClaudeAgentToCodexAgent(content) {
3187
3188
  return converted;
3188
3189
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
3189
3190
  const description = extractFrontmatterField(frontmatter, 'description') || '';
3190
- const tools = extractFrontmatterField(frontmatter, 'tools') || '';
3191
+ // parseFrontmatterTools reads both the inline comma list and the YAML block
3192
+ // list (#5790).
3193
+ const tools = parseFrontmatterTools(frontmatter).join(', ');
3191
3194
  const roleHeader = `<codex_agent_role>
3192
3195
  role: ${name}
3193
3196
  tools: ${tools}
@@ -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: assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath),
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: assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath),
206
+ destDir: resolveRuntimeArtifactDestination(layout.configDir, kind),
148
207
  })),
149
208
  };
150
209
  }
151
- module.exports = { assertDestWithinConfigHome, createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan };
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
+ };
@@ -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
- for (const entries of Object.values(settings.hooks)) {
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 (CHAIN_RUNNER_COMMAND.test(trimmed) || RESOLVER_RUNNER_COMMAND.test(trimmed))
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
- function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
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
- if (!opts.portableHooks) {
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
- if (!node_fs_1.default.existsSync(configPath))
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(node_fs_1.default.readFileSync(configPath, 'utf-8'));
181
+ const parsed = JSON.parse(raw);
177
182
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
178
183
  }
179
184
  catch {