@opengsd/gsd-core 1.3.1 → 1.4.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.
Files changed (136) hide show
  1. package/.claude-plugin/plugin.json +23 -0
  2. package/GEMINI.md +53 -0
  3. package/agents/gsd-advisor-researcher.md +1 -20
  4. package/agents/gsd-ai-researcher.md +2 -21
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-code-reviewer.md +1 -1
  7. package/agents/gsd-domain-researcher.md +2 -21
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-eval-planner.md +1 -1
  10. package/agents/gsd-executor.md +1 -1
  11. package/agents/gsd-framework-selector.md +1 -1
  12. package/agents/gsd-nyquist-auditor.md +1 -1
  13. package/agents/gsd-pattern-mapper.md +1 -1
  14. package/agents/gsd-phase-researcher.md +92 -166
  15. package/agents/gsd-planner.md +9 -36
  16. package/agents/gsd-project-researcher.md +62 -141
  17. package/agents/gsd-security-auditor.md +1 -1
  18. package/agents/gsd-ui-auditor.md +1 -1
  19. package/agents/gsd-ui-checker.md +1 -1
  20. package/agents/gsd-ui-researcher.md +3 -22
  21. package/agents/gsd-user-profiler.md +1 -1
  22. package/agents/gsd-verifier.md +8 -2
  23. package/bin/install.js +1977 -339
  24. package/commands/gsd/autonomous.md +2 -0
  25. package/commands/gsd/execute-phase.md +2 -0
  26. package/commands/gsd/graphify.md +11 -6
  27. package/commands/gsd/import.md +6 -2
  28. package/commands/gsd/plan-phase.md +4 -2
  29. package/commands/gsd/progress.md +1 -0
  30. package/commands/gsd/stats.md +1 -0
  31. package/commands/gsd/update.md +3 -2
  32. package/gemini-extension.json +6 -0
  33. package/gsd-core/bin/check-latest-version.cjs +61 -6
  34. package/gsd-core/bin/gsd-tools.cjs +238 -32
  35. package/gsd-core/bin/lib/check-command-router.cjs +1 -0
  36. package/gsd-core/bin/lib/cli-exit.cjs +42 -0
  37. package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
  38. package/gsd-core/bin/lib/commands.cjs +5 -4
  39. package/gsd-core/bin/lib/config.cjs +28 -4
  40. package/gsd-core/bin/lib/core.cjs +72 -28
  41. package/gsd-core/bin/lib/graphify.cjs +2 -2
  42. package/gsd-core/bin/lib/init-command-router.cjs +2 -2
  43. package/gsd-core/bin/lib/init.cjs +19 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +58 -0
  45. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  46. package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
  47. package/gsd-core/bin/lib/intel.cjs +3 -20
  48. package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
  49. package/gsd-core/bin/lib/phase.cjs +3 -3
  50. package/gsd-core/bin/lib/research-provider.cjs +137 -0
  51. package/gsd-core/bin/lib/research-store.cjs +167 -0
  52. package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
  53. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
  54. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
  55. package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
  56. package/gsd-core/bin/lib/security.cjs +73 -0
  57. package/gsd-core/bin/lib/shell-command-projection.cjs +9 -0
  58. package/gsd-core/bin/lib/surface.cjs +54 -11
  59. package/gsd-core/bin/lib/validate.cjs +2 -2
  60. package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
  61. package/gsd-core/bin/lib/verification.cjs +193 -0
  62. package/gsd-core/bin/lib/verify.cjs +2 -2
  63. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
  64. package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
  65. package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
  66. package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
  67. package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
  68. package/gsd-core/references/planner-load-graph-context.md +36 -0
  69. package/gsd-core/references/planning-config.md +3 -1
  70. package/gsd-core/references/research-documentation-lookup.md +29 -0
  71. package/gsd-core/references/research-philosophy.md +29 -0
  72. package/gsd-core/references/research-verification-protocol.md +27 -0
  73. package/gsd-core/workflows/execute-phase.md +19 -8
  74. package/gsd-core/workflows/help/modes/full.md +4 -3
  75. package/gsd-core/workflows/ingest-docs.md +3 -2
  76. package/gsd-core/workflows/plan-phase.md +14 -10
  77. package/gsd-core/workflows/plan-review-convergence.md +3 -3
  78. package/gsd-core/workflows/review.md +24 -7
  79. package/gsd-core/workflows/ship.md +5 -8
  80. package/gsd-core/workflows/spec-phase.md +2 -1
  81. package/gsd-core/workflows/update.md +34 -6
  82. package/hooks/dist/gsd-config-reload.js +133 -0
  83. package/hooks/dist/gsd-context-monitor.js +1 -1
  84. package/hooks/dist/gsd-cursor-post-tool.js +75 -0
  85. package/hooks/dist/gsd-cursor-session-start.js +52 -0
  86. package/hooks/dist/gsd-workflow-guard.js +1 -0
  87. package/hooks/dist/gsd-worktree-path-guard.js +1 -1
  88. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  89. package/hooks/gsd-config-reload.js +133 -0
  90. package/hooks/gsd-context-monitor.js +1 -1
  91. package/hooks/gsd-cursor-post-tool.js +75 -0
  92. package/hooks/gsd-cursor-session-start.js +52 -0
  93. package/hooks/gsd-workflow-guard.js +1 -0
  94. package/hooks/gsd-worktree-path-guard.js +1 -1
  95. package/hooks/hooks.json +69 -0
  96. package/hooks/managed-hooks-registry.cjs +3 -0
  97. package/package.json +8 -1
  98. package/scripts/affected-tests-lib.cjs +3 -2
  99. package/scripts/build-hooks.js +7 -0
  100. package/scripts/changeset/cli.cjs +226 -28
  101. package/scripts/changeset/lint.cjs +5 -4
  102. package/scripts/changeset/new.cjs +4 -4
  103. package/scripts/check-alias-drift.cjs +77 -71
  104. package/scripts/check-env.cjs +185 -179
  105. package/scripts/check-npm-integrity.cjs +115 -109
  106. package/scripts/ci-guard-runner.cjs +11 -5
  107. package/scripts/ci-prepare-test-scope.cjs +27 -22
  108. package/scripts/ci-rebase-check.cjs +46 -45
  109. package/scripts/ci-test-scope.cjs +126 -22
  110. package/scripts/diff-touches-shipped-paths.cjs +52 -44
  111. package/scripts/gen-inventory-manifest.cjs +38 -32
  112. package/scripts/gen-research-agents.cjs +276 -0
  113. package/scripts/issue-dedupe.cjs +278 -0
  114. package/scripts/lib/cli-exit.cjs +56 -0
  115. package/scripts/lint-command-contract.cjs +28 -22
  116. package/scripts/lint-descriptions.cjs +32 -28
  117. package/scripts/lint-docs-required.cjs +4 -4
  118. package/scripts/lint-legacy-dir-name.cjs +56 -52
  119. package/scripts/lint-pr-check-project-dir.cjs +3 -1
  120. package/scripts/lint-shell-command-projection-drift.cjs +27 -22
  121. package/scripts/lint-skill-deps.cjs +31 -26
  122. package/scripts/lint-test-file-count.allowlist.json +2 -0
  123. package/scripts/lint-test-file-count.cjs +5 -4
  124. package/scripts/mutation-matrix.cjs +6 -3
  125. package/scripts/prompt-injection-scan.sh +1 -1
  126. package/scripts/release-notes/discord-release-summary.cjs +373 -0
  127. package/scripts/release-notes/format-github-release-notes.cjs +8 -3
  128. package/scripts/release-tarball-smoke.cjs +6 -4
  129. package/scripts/research-profiles.cjs +149 -0
  130. package/scripts/run-affected-tests.cjs +2 -1
  131. package/scripts/run-cross-platform-tests.cjs +11 -7
  132. package/scripts/run-tests.cjs +8 -7
  133. package/scripts/strip-prose-atrefs.cjs +1 -1
  134. package/scripts/sync-manifest-versions.cjs +119 -0
  135. package/scripts/sync-runtime-launcher.cjs +0 -3
  136. package/scripts/verify-npm-publish.cjs +14 -26
@@ -27,67 +27,18 @@
27
27
 
28
28
  const fs = require('fs');
29
29
  const path = require('path');
30
- const { execFileSync, spawnSync } = require('child_process');
30
+ const { spawnSync } = require('child_process');
31
+
32
+ const { ExitError, runMain } = require('./lib/cli-exit.cjs');
31
33
 
32
34
  // On Windows, npm ships as npm.cmd (a batch wrapper); spawnSync without
33
35
  // shell:true requires the exact filename including extension.
34
36
  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
35
37
 
36
- // ---------------------------------------------------------------------------
37
- // Argument parsing
38
- // ---------------------------------------------------------------------------
39
- let jsonMode = false;
40
-
41
- for (const arg of process.argv.slice(2)) {
42
- if (arg === '--json') {
43
- jsonMode = true;
44
- } else if (arg === '--help' || arg === '-h') {
45
- process.stdout.write(
46
- 'scripts/check-env.cjs — Environment parity validator for contributors (issue #117).\n' +
47
- '\n' +
48
- 'Checks that the developer\'s environment matches project requirements before\n' +
49
- 'running tests or audits. Designed to catch mismatches early rather than\n' +
50
- 'through cryptic test failures.\n' +
51
- '\n' +
52
- 'Exit codes:\n' +
53
- ' 0 All checks passed\n' +
54
- ' 1 One or more checks failed\n' +
55
- ' 2 Tool error (missing required tool, corrupt package.json, etc.)\n' +
56
- '\n' +
57
- 'Usage:\n' +
58
- ' node scripts/check-env.cjs # Human-readable report\n' +
59
- ' node scripts/check-env.cjs --json # Structured JSON report\n' +
60
- ' node scripts/check-env.cjs --help # This message\n'
61
- );
62
- process.exit(0);
63
- } else {
64
- process.stderr.write(`Unknown option: ${arg}\n`);
65
- process.exit(2);
66
- }
67
- }
68
-
69
- // ---------------------------------------------------------------------------
70
- // Locate the project root (directory containing package.json)
71
- // ---------------------------------------------------------------------------
72
- const PROJECT_ROOT = process.cwd();
73
- const PACKAGE_JSON = path.join(PROJECT_ROOT, 'package.json');
74
-
75
- if (!fs.existsSync(PACKAGE_JSON)) {
76
- process.stderr.write(`ERROR: package.json not found in ${PROJECT_ROOT}\n`);
77
- process.exit(2);
78
- }
79
-
80
38
  // ---------------------------------------------------------------------------
81
39
  // Helpers
82
40
  // ---------------------------------------------------------------------------
83
41
 
84
- /** @type {Array<{name: string, status: 'pass'|'fail'|'skip', message: string}>} */
85
- const checks = [];
86
-
87
- function addCheck(name, status, message) {
88
- checks.push({ name, status, message });
89
- }
90
-
91
42
  /**
92
43
  * Semver comparison: does `version` satisfy `constraint`?
93
44
  * Constraint forms: >=X.Y.Z, >X.Y.Z, <=X.Y.Z, <X.Y.Z, =X.Y.Z, X.Y.Z
@@ -138,7 +89,7 @@ function satisfiesConstraint(version, constraint) {
138
89
  * Returns the string value or empty string if absent.
139
90
  * Uses './package.json' so Node resolves relative to CWD on all platforms.
140
91
  */
141
- function pkgField(fieldPath) {
92
+ function pkgField(fieldPath, PROJECT_ROOT) {
142
93
  try {
143
94
  const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
144
95
  let val = pkg;
@@ -152,155 +103,210 @@ function pkgField(fieldPath) {
152
103
  }
153
104
  }
154
105
 
155
- // ---------------------------------------------------------------------------
156
- // Check 1: Node version vs engines.node
157
- // ---------------------------------------------------------------------------
158
- const enginesNode = pkgField('engines.node');
159
- let currentNode = '';
160
- try {
161
- currentNode = process.version.replace(/^v/, '');
162
- } catch { /* ignore */ }
106
+ function main() {
107
+ // ---------------------------------------------------------------------------
108
+ // Argument parsing
109
+ // ---------------------------------------------------------------------------
110
+ let jsonMode = false;
163
111
 
164
- if (!currentNode) {
165
- addCheck('node-version', 'fail', 'node binary not found on PATH');
166
- } else if (!enginesNode) {
167
- addCheck('node-version', 'fail', 'engines.node missing from package.json — add it (see D2 in docs/contributing/bootstrap.md)');
168
- } else {
169
- if (satisfiesConstraint(currentNode, enginesNode)) {
170
- addCheck('node-version', 'pass', `Node ${currentNode} satisfies ${enginesNode}`);
171
- } else {
172
- addCheck('node-version', 'fail', `Node ${currentNode} does NOT satisfy engines.node ${enginesNode}`);
112
+ for (const arg of process.argv.slice(2)) {
113
+ if (arg === '--json') {
114
+ jsonMode = true;
115
+ } else if (arg === '--help' || arg === '-h') {
116
+ process.stdout.write(
117
+ 'scripts/check-env.cjs — Environment parity validator for contributors (issue #117).\n' +
118
+ '\n' +
119
+ 'Checks that the developer\'s environment matches project requirements before\n' +
120
+ 'running tests or audits. Designed to catch mismatches early rather than\n' +
121
+ 'through cryptic test failures.\n' +
122
+ '\n' +
123
+ 'Exit codes:\n' +
124
+ ' 0 All checks passed\n' +
125
+ ' 1 One or more checks failed\n' +
126
+ ' 2 Tool error (missing required tool, corrupt package.json, etc.)\n' +
127
+ '\n' +
128
+ 'Usage:\n' +
129
+ ' node scripts/check-env.cjs # Human-readable report\n' +
130
+ ' node scripts/check-env.cjs --json # Structured JSON report\n' +
131
+ ' node scripts/check-env.cjs --help # This message\n'
132
+ );
133
+ return 0;
134
+ } else {
135
+ process.stderr.write(`Unknown option: ${arg}\n`);
136
+ throw new ExitError(2);
137
+ }
173
138
  }
174
- }
175
139
 
176
- // ---------------------------------------------------------------------------
177
- // Check 2: npm version vs engines.npm (skip if field absent)
178
- // ---------------------------------------------------------------------------
179
- const enginesNpm = pkgField('engines.npm');
180
- let currentNpm = '';
181
- try {
182
- const res = spawnSync(npmCmd, ['--version'], { encoding: 'utf8', timeout: 10_000, shell: process.platform === 'win32' });
183
- if (res.status === 0 && res.stdout) {
184
- currentNpm = res.stdout.trim();
140
+ // ---------------------------------------------------------------------------
141
+ // Locate the project root (directory containing package.json)
142
+ // ---------------------------------------------------------------------------
143
+ const PROJECT_ROOT = process.cwd();
144
+ const PACKAGE_JSON = path.join(PROJECT_ROOT, 'package.json');
145
+
146
+ if (!fs.existsSync(PACKAGE_JSON)) {
147
+ process.stderr.write(`ERROR: package.json not found in ${PROJECT_ROOT}\n`);
148
+ throw new ExitError(2);
185
149
  }
186
- } catch { /* ignore */ }
187
150
 
188
- if (!enginesNpm) {
189
- addCheck('npm-version', 'skip', 'engines.npm not set in package.json — skipping');
190
- } else if (!currentNpm) {
191
- addCheck('npm-version', 'fail', 'npm binary not found on PATH');
192
- } else {
193
- if (satisfiesConstraint(currentNpm, enginesNpm)) {
194
- addCheck('npm-version', 'pass', `npm ${currentNpm} satisfies ${enginesNpm}`);
195
- } else {
196
- addCheck('npm-version', 'fail', `npm ${currentNpm} does NOT satisfy engines.npm ${enginesNpm}`);
151
+ /** @type {Array<{name: string, status: 'pass'|'fail'|'skip', message: string}>} */
152
+ const checks = [];
153
+
154
+ function addCheck(name, status, message) {
155
+ checks.push({ name, status, message });
197
156
  }
198
- }
199
157
 
200
- // ---------------------------------------------------------------------------
201
- // Check 3: Lockfile presence
202
- // ---------------------------------------------------------------------------
203
- const LOCKFILE = path.join(PROJECT_ROOT, 'package-lock.json');
204
- if (fs.existsSync(LOCKFILE)) {
205
- addCheck('lockfile-present', 'pass', 'package-lock.json exists');
206
- } else {
207
- addCheck('lockfile-present', 'fail', "package-lock.json missing — run 'npm install' to generate it");
208
- }
158
+ // ---------------------------------------------------------------------------
159
+ // Check 1: Node version vs engines.node
160
+ // ---------------------------------------------------------------------------
161
+ const enginesNode = pkgField('engines.node', PROJECT_ROOT);
162
+ let currentNode = '';
163
+ try {
164
+ currentNode = process.version.replace(/^v/, '');
165
+ } catch { /* ignore */ }
209
166
 
210
- // ---------------------------------------------------------------------------
211
- // Check 4: Lockfile sync (npm ci --dry-run)
212
- // ---------------------------------------------------------------------------
213
- if (fs.existsSync(LOCKFILE)) {
167
+ if (!currentNode) {
168
+ addCheck('node-version', 'fail', 'node binary not found on PATH');
169
+ } else if (!enginesNode) {
170
+ addCheck('node-version', 'fail', 'engines.node missing from package.json — add it (see D2 in docs/contributing/bootstrap.md)');
171
+ } else {
172
+ if (satisfiesConstraint(currentNode, enginesNode)) {
173
+ addCheck('node-version', 'pass', `Node ${currentNode} satisfies ${enginesNode}`);
174
+ } else {
175
+ addCheck('node-version', 'fail', `Node ${currentNode} does NOT satisfy engines.node ${enginesNode}`);
176
+ }
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Check 2: npm version vs engines.npm (skip if field absent)
181
+ // ---------------------------------------------------------------------------
182
+ const enginesNpm = pkgField('engines.npm', PROJECT_ROOT);
183
+ let currentNpm = '';
214
184
  try {
215
- // --ignore-scripts: this is a lockfile-vs-package.json sync check, not a
216
- // build. Without it, npm would run the `prepare` lifecycle (build:lib via
217
- // tsc) — which fails when check:env runs before deps are installed (tsc
218
- // absent), misreporting an out-of-sync lockfile. ADR-457 build-at-publish.
219
- const res = spawnSync(npmCmd, ['ci', '--dry-run', '--ignore-scripts'], {
220
- cwd: PROJECT_ROOT,
221
- encoding: 'utf8',
222
- shell: process.platform === 'win32',
223
- });
224
- if (res.status === 0) {
225
- addCheck('lockfile-sync', 'pass', 'package-lock.json is in sync with package.json');
185
+ const res = spawnSync(npmCmd, ['--version'], { encoding: 'utf8', timeout: 10_000, shell: process.platform === 'win32' });
186
+ if (res.status === 0 && res.stdout) {
187
+ currentNpm = res.stdout.trim();
188
+ }
189
+ } catch { /* ignore */ }
190
+
191
+ if (!enginesNpm) {
192
+ addCheck('npm-version', 'skip', 'engines.npm not set in package.json — skipping');
193
+ } else if (!currentNpm) {
194
+ addCheck('npm-version', 'fail', 'npm binary not found on PATH');
195
+ } else {
196
+ if (satisfiesConstraint(currentNpm, enginesNpm)) {
197
+ addCheck('npm-version', 'pass', `npm ${currentNpm} satisfies ${enginesNpm}`);
226
198
  } else {
199
+ addCheck('npm-version', 'fail', `npm ${currentNpm} does NOT satisfy engines.npm ${enginesNpm}`);
200
+ }
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Check 3: Lockfile presence
205
+ // ---------------------------------------------------------------------------
206
+ const LOCKFILE = path.join(PROJECT_ROOT, 'package-lock.json');
207
+ if (fs.existsSync(LOCKFILE)) {
208
+ addCheck('lockfile-present', 'pass', 'package-lock.json exists');
209
+ } else {
210
+ addCheck('lockfile-present', 'fail', "package-lock.json missing — run 'npm install' to generate it");
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Check 4: Lockfile sync (npm ci --dry-run)
215
+ // ---------------------------------------------------------------------------
216
+ if (fs.existsSync(LOCKFILE)) {
217
+ try {
218
+ // --ignore-scripts: this is a lockfile-vs-package.json sync check, not a
219
+ // build. Without it, npm would run the `prepare` lifecycle (build:lib via
220
+ // tsc) — which fails when check:env runs before deps are installed (tsc
221
+ // absent), misreporting an out-of-sync lockfile. ADR-457 build-at-publish.
222
+ const res = spawnSync(npmCmd, ['ci', '--dry-run', '--ignore-scripts'], {
223
+ cwd: PROJECT_ROOT,
224
+ encoding: 'utf8',
225
+ shell: process.platform === 'win32',
226
+ });
227
+ if (res.status === 0) {
228
+ addCheck('lockfile-sync', 'pass', 'package-lock.json is in sync with package.json');
229
+ } else {
230
+ addCheck('lockfile-sync', 'fail', "package-lock.json is out of sync — run 'npm ci' to restore");
231
+ }
232
+ } catch {
227
233
  addCheck('lockfile-sync', 'fail', "package-lock.json is out of sync — run 'npm ci' to restore");
228
234
  }
229
- } catch {
230
- addCheck('lockfile-sync', 'fail', "package-lock.json is out of sync — run 'npm ci' to restore");
235
+ } else {
236
+ addCheck('lockfile-sync', 'skip', 'skipped — lockfile missing');
231
237
  }
232
- } else {
233
- addCheck('lockfile-sync', 'skip', 'skipped — lockfile missing');
234
- }
235
238
 
236
- // ---------------------------------------------------------------------------
237
- // Check 5: Version manager pin vs active Node
238
- // Looks for .nvmrc, .node-version, or .tool-versions at project root.
239
- // ---------------------------------------------------------------------------
240
- const NVMRC = path.join(PROJECT_ROOT, '.nvmrc');
241
- const NODE_VERSION_FILE = path.join(PROJECT_ROOT, '.node-version');
242
- const TOOL_VERSIONS = path.join(PROJECT_ROOT, '.tool-versions');
239
+ // ---------------------------------------------------------------------------
240
+ // Check 5: Version manager pin vs active Node
241
+ // Looks for .nvmrc, .node-version, or .tool-versions at project root.
242
+ // ---------------------------------------------------------------------------
243
+ const NVMRC = path.join(PROJECT_ROOT, '.nvmrc');
244
+ const NODE_VERSION_FILE = path.join(PROJECT_ROOT, '.node-version');
245
+ const TOOL_VERSIONS = path.join(PROJECT_ROOT, '.tool-versions');
243
246
 
244
- let pinnedMajor = '';
245
- let pinSource = '';
247
+ let pinnedMajor = '';
248
+ let pinSource = '';
246
249
 
247
- if (fs.existsSync(NVMRC)) {
248
- const content = fs.readFileSync(NVMRC, 'utf8').split('\n')[0].trim().replace(/^v/, '');
249
- pinnedMajor = content.split('.')[0];
250
- pinSource = '.nvmrc';
251
- } else if (fs.existsSync(NODE_VERSION_FILE)) {
252
- const content = fs.readFileSync(NODE_VERSION_FILE, 'utf8').split('\n')[0].trim().replace(/^v/, '');
253
- pinnedMajor = content.split('.')[0];
254
- pinSource = '.node-version';
255
- } else if (fs.existsSync(TOOL_VERSIONS)) {
256
- const lines = fs.readFileSync(TOOL_VERSIONS, 'utf8').split('\n');
257
- const nodeLine = lines.find(l => /^nodejs\s+/.test(l));
258
- if (nodeLine) {
259
- const ver = nodeLine.split(/\s+/)[1] || '';
260
- pinnedMajor = ver.replace(/^v/, '').split('.')[0];
261
- pinSource = '.tool-versions';
250
+ if (fs.existsSync(NVMRC)) {
251
+ const content = fs.readFileSync(NVMRC, 'utf8').split('\n')[0].trim().replace(/^v/, '');
252
+ pinnedMajor = content.split('.')[0];
253
+ pinSource = '.nvmrc';
254
+ } else if (fs.existsSync(NODE_VERSION_FILE)) {
255
+ const content = fs.readFileSync(NODE_VERSION_FILE, 'utf8').split('\n')[0].trim().replace(/^v/, '');
256
+ pinnedMajor = content.split('.')[0];
257
+ pinSource = '.node-version';
258
+ } else if (fs.existsSync(TOOL_VERSIONS)) {
259
+ const lines = fs.readFileSync(TOOL_VERSIONS, 'utf8').split('\n');
260
+ const nodeLine = lines.find(l => /^nodejs\s+/.test(l));
261
+ if (nodeLine) {
262
+ const ver = nodeLine.split(/\s+/)[1] || '';
263
+ pinnedMajor = ver.replace(/^v/, '').split('.')[0];
264
+ pinSource = '.tool-versions';
265
+ }
262
266
  }
263
- }
264
267
 
265
- if (!pinnedMajor) {
266
- addCheck('version-manager-pin', 'skip', 'no .nvmrc, .node-version, or .tool-versions found — skipping');
267
- } else if (process.env.CI === 'true') {
268
- addCheck('version-manager-pin', 'skip', 'CI=true — version-manager pin check skipped (matrix tests multiple Node majors)');
269
- } else {
270
- const activeMajor = process.version.replace(/^v/, '').split('.')[0];
271
- if (activeMajor === pinnedMajor) {
272
- addCheck('version-manager-pin', 'pass', `Active Node major (${activeMajor}) matches ${pinSource} pin (${pinnedMajor})`);
268
+ if (!pinnedMajor) {
269
+ addCheck('version-manager-pin', 'skip', 'no .nvmrc, .node-version, or .tool-versions found — skipping');
270
+ } else if (process.env.CI === 'true') {
271
+ addCheck('version-manager-pin', 'skip', 'CI=true — version-manager pin check skipped (matrix tests multiple Node majors)');
273
272
  } else {
274
- addCheck('version-manager-pin', 'fail', `Active Node major (${activeMajor}) does NOT match ${pinSource} pin (${pinnedMajor}) — run 'nvm use' or equivalent`);
273
+ const activeMajor = process.version.replace(/^v/, '').split('.')[0];
274
+ if (activeMajor === pinnedMajor) {
275
+ addCheck('version-manager-pin', 'pass', `Active Node major (${activeMajor}) matches ${pinSource} pin (${pinnedMajor})`);
276
+ } else {
277
+ addCheck('version-manager-pin', 'fail', `Active Node major (${activeMajor}) does NOT match ${pinSource} pin (${pinnedMajor}) — run 'nvm use' or equivalent`);
278
+ }
275
279
  }
276
- }
277
280
 
278
- // ---------------------------------------------------------------------------
279
- // Output
280
- // ---------------------------------------------------------------------------
281
- const overallPass = checks.every(c => c.status !== 'fail');
281
+ // ---------------------------------------------------------------------------
282
+ // Output
283
+ // ---------------------------------------------------------------------------
284
+ const overallPass = checks.every(c => c.status !== 'fail');
282
285
 
283
- if (jsonMode) {
284
- // Structured JSON: {pass: bool, checks: [{name, status, message}]}
285
- const out = {
286
- pass: overallPass,
287
- checks: checks.map(c => ({ name: c.name, status: c.status, message: c.message })),
288
- };
289
- process.stdout.write(JSON.stringify(out, null, 2) + '\n');
290
- } else {
291
- // Human-readable report
292
- process.stdout.write('=== Environment Check ===\n');
293
- for (const { name, status, message } of checks) {
294
- const icon = status === 'pass' ? '[PASS]' : status === 'fail' ? '[FAIL]' : '[SKIP]';
295
- const namePadded = name.padEnd(25);
296
- process.stdout.write(` ${icon} ${namePadded} ${message}\n`);
297
- }
298
- process.stdout.write('\n');
299
- if (overallPass) {
300
- process.stdout.write('Result: ALL CHECKS PASSED\n');
286
+ if (jsonMode) {
287
+ // Structured JSON: {pass: bool, checks: [{name, status, message}]}
288
+ const out = {
289
+ pass: overallPass,
290
+ checks: checks.map(c => ({ name: c.name, status: c.status, message: c.message })),
291
+ };
292
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
301
293
  } else {
302
- process.stdout.write('Result: ONE OR MORE CHECKS FAILED — see above\n');
294
+ // Human-readable report
295
+ process.stdout.write('=== Environment Check ===\n');
296
+ for (const { name, status, message } of checks) {
297
+ const icon = status === 'pass' ? '[PASS]' : status === 'fail' ? '[FAIL]' : '[SKIP]';
298
+ const namePadded = name.padEnd(25);
299
+ process.stdout.write(` ${icon} ${namePadded} ${message}\n`);
300
+ }
301
+ process.stdout.write('\n');
302
+ if (overallPass) {
303
+ process.stdout.write('Result: ALL CHECKS PASSED\n');
304
+ } else {
305
+ process.stdout.write('Result: ONE OR MORE CHECKS FAILED — see above\n');
306
+ }
303
307
  }
308
+
309
+ return overallPass ? 0 : 1;
304
310
  }
305
311
 
306
- process.exit(overallPass ? 0 : 1);
312
+ runMain(main);