@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.
- package/.claude-plugin/plugin.json +23 -0
- package/GEMINI.md +53 -0
- package/agents/gsd-advisor-researcher.md +1 -20
- package/agents/gsd-ai-researcher.md +2 -21
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-domain-researcher.md +2 -21
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-framework-selector.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-pattern-mapper.md +1 -1
- package/agents/gsd-phase-researcher.md +92 -166
- package/agents/gsd-planner.md +9 -36
- package/agents/gsd-project-researcher.md +62 -141
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +3 -22
- package/agents/gsd-user-profiler.md +1 -1
- package/agents/gsd-verifier.md +8 -2
- package/bin/install.js +1977 -339
- package/commands/gsd/autonomous.md +2 -0
- package/commands/gsd/execute-phase.md +2 -0
- package/commands/gsd/graphify.md +11 -6
- package/commands/gsd/import.md +6 -2
- package/commands/gsd/plan-phase.md +4 -2
- package/commands/gsd/progress.md +1 -0
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/update.md +3 -2
- package/gemini-extension.json +6 -0
- package/gsd-core/bin/check-latest-version.cjs +61 -6
- package/gsd-core/bin/gsd-tools.cjs +238 -32
- package/gsd-core/bin/lib/check-command-router.cjs +1 -0
- package/gsd-core/bin/lib/cli-exit.cjs +42 -0
- package/gsd-core/bin/lib/command-routing-hub.cjs +1 -1
- package/gsd-core/bin/lib/commands.cjs +5 -4
- package/gsd-core/bin/lib/config.cjs +28 -4
- package/gsd-core/bin/lib/core.cjs +72 -28
- package/gsd-core/bin/lib/graphify.cjs +2 -2
- package/gsd-core/bin/lib/init-command-router.cjs +2 -2
- package/gsd-core/bin/lib/init.cjs +19 -3
- package/gsd-core/bin/lib/install-profiles.cjs +58 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs +1 -1
- package/gsd-core/bin/lib/intel.cjs +3 -20
- package/gsd-core/bin/lib/package-legitimacy.cjs +368 -0
- package/gsd-core/bin/lib/phase.cjs +3 -3
- package/gsd-core/bin/lib/research-provider.cjs +137 -0
- package/gsd-core/bin/lib/research-store.cjs +167 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +4 -19
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +67 -9
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +56 -0
- package/gsd-core/bin/lib/runtime-homes.cjs +32 -11
- package/gsd-core/bin/lib/security.cjs +73 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +9 -0
- package/gsd-core/bin/lib/surface.cjs +54 -11
- package/gsd-core/bin/lib/validate.cjs +2 -2
- package/gsd-core/bin/lib/verification-command-router.cjs +31 -0
- package/gsd-core/bin/lib/verification.cjs +193 -0
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +325 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +31 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -1
- package/gsd-core/bin/verify-reapply-patches.cjs +8 -11
- package/gsd-core/references/planner-load-graph-context.md +36 -0
- package/gsd-core/references/planning-config.md +3 -1
- package/gsd-core/references/research-documentation-lookup.md +29 -0
- package/gsd-core/references/research-philosophy.md +29 -0
- package/gsd-core/references/research-verification-protocol.md +27 -0
- package/gsd-core/workflows/execute-phase.md +19 -8
- package/gsd-core/workflows/help/modes/full.md +4 -3
- package/gsd-core/workflows/ingest-docs.md +3 -2
- package/gsd-core/workflows/plan-phase.md +14 -10
- package/gsd-core/workflows/plan-review-convergence.md +3 -3
- package/gsd-core/workflows/review.md +24 -7
- package/gsd-core/workflows/ship.md +5 -8
- package/gsd-core/workflows/spec-phase.md +2 -1
- package/gsd-core/workflows/update.md +34 -6
- package/hooks/dist/gsd-config-reload.js +133 -0
- package/hooks/dist/gsd-context-monitor.js +1 -1
- package/hooks/dist/gsd-cursor-post-tool.js +75 -0
- package/hooks/dist/gsd-cursor-session-start.js +52 -0
- package/hooks/dist/gsd-workflow-guard.js +1 -0
- package/hooks/dist/gsd-worktree-path-guard.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-config-reload.js +133 -0
- package/hooks/gsd-context-monitor.js +1 -1
- package/hooks/gsd-cursor-post-tool.js +75 -0
- package/hooks/gsd-cursor-session-start.js +52 -0
- package/hooks/gsd-workflow-guard.js +1 -0
- package/hooks/gsd-worktree-path-guard.js +1 -1
- package/hooks/hooks.json +69 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +8 -1
- package/scripts/affected-tests-lib.cjs +3 -2
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/cli.cjs +226 -28
- package/scripts/changeset/lint.cjs +5 -4
- package/scripts/changeset/new.cjs +4 -4
- package/scripts/check-alias-drift.cjs +77 -71
- package/scripts/check-env.cjs +185 -179
- package/scripts/check-npm-integrity.cjs +115 -109
- package/scripts/ci-guard-runner.cjs +11 -5
- package/scripts/ci-prepare-test-scope.cjs +27 -22
- package/scripts/ci-rebase-check.cjs +46 -45
- package/scripts/ci-test-scope.cjs +126 -22
- package/scripts/diff-touches-shipped-paths.cjs +52 -44
- package/scripts/gen-inventory-manifest.cjs +38 -32
- package/scripts/gen-research-agents.cjs +276 -0
- package/scripts/issue-dedupe.cjs +278 -0
- package/scripts/lib/cli-exit.cjs +56 -0
- package/scripts/lint-command-contract.cjs +28 -22
- package/scripts/lint-descriptions.cjs +32 -28
- package/scripts/lint-docs-required.cjs +4 -4
- package/scripts/lint-legacy-dir-name.cjs +56 -52
- package/scripts/lint-pr-check-project-dir.cjs +3 -1
- package/scripts/lint-shell-command-projection-drift.cjs +27 -22
- package/scripts/lint-skill-deps.cjs +31 -26
- package/scripts/lint-test-file-count.allowlist.json +2 -0
- package/scripts/lint-test-file-count.cjs +5 -4
- package/scripts/mutation-matrix.cjs +6 -3
- package/scripts/prompt-injection-scan.sh +1 -1
- package/scripts/release-notes/discord-release-summary.cjs +373 -0
- package/scripts/release-notes/format-github-release-notes.cjs +8 -3
- package/scripts/release-tarball-smoke.cjs +6 -4
- package/scripts/research-profiles.cjs +149 -0
- package/scripts/run-affected-tests.cjs +2 -1
- package/scripts/run-cross-platform-tests.cjs +11 -7
- package/scripts/run-tests.cjs +8 -7
- package/scripts/strip-prose-atrefs.cjs +1 -1
- package/scripts/sync-manifest-versions.cjs +119 -0
- package/scripts/sync-runtime-launcher.cjs +0 -3
- package/scripts/verify-npm-publish.cjs +14 -26
|
@@ -3,18 +3,87 @@
|
|
|
3
3
|
|
|
4
4
|
const { execFileSync } = require('child_process');
|
|
5
5
|
const { existsSync, readdirSync, appendFileSync } = require('fs');
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
8
|
+
|
|
9
|
+
// Workflow files that are purely administrative / policy bots. Changes to these
|
|
10
|
+
// files do NOT require the cross-platform test matrix — only a lightweight
|
|
11
|
+
// ubuntu lane running workflow-lint tests is needed.
|
|
12
|
+
// FAIL-SAFE: any .github/workflows/*.yml NOT listed here is treated as a
|
|
13
|
+
// pipeline workflow and gets the full matrix. New workflow files default to full.
|
|
14
|
+
const INERT_WORKFLOWS = new Set([
|
|
15
|
+
'stale.yml',
|
|
16
|
+
'branch-cleanup.yml',
|
|
17
|
+
'branch-naming.yml',
|
|
18
|
+
'auto-label-issues.yml',
|
|
19
|
+
'auto-branch.yml',
|
|
20
|
+
'auto-backmerge.yml',
|
|
21
|
+
'close-draft-prs.yml',
|
|
22
|
+
'dismiss-unauthorized-pr-approvals.yml',
|
|
23
|
+
'pr-gate.yml',
|
|
24
|
+
'pr-target-validator.yml',
|
|
25
|
+
'pr-template-format.yml',
|
|
26
|
+
'require-issue-link.yml',
|
|
27
|
+
'changeset-required.yml',
|
|
28
|
+
'docs-required.yml',
|
|
29
|
+
'discord-changelog.yml',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// Workflows that gate merges, ship the product, or run security/cross-platform
|
|
33
|
+
// suites — these must ALWAYS get the full pipeline treatment and can never be
|
|
34
|
+
// added to INERT_WORKFLOWS. A module-load assertion enforces this so a mistaken
|
|
35
|
+
// or malicious addition fails CI loudly in the `changes` job on every PR.
|
|
36
|
+
const PROTECTED_WORKFLOWS = new Set([
|
|
37
|
+
'test.yml',
|
|
38
|
+
'install-smoke.yml',
|
|
39
|
+
'mutation.yml',
|
|
40
|
+
'security-scan.yml',
|
|
41
|
+
'release.yml',
|
|
42
|
+
]);
|
|
43
|
+
for (const wf of PROTECTED_WORKFLOWS) {
|
|
44
|
+
if (INERT_WORKFLOWS.has(wf)) {
|
|
45
|
+
throw new Error(`ci-test-scope: protected workflow "${wf}" must not be in INERT_WORKFLOWS (it requires the full test matrix).`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns true if the path is an inert (non-pipeline) workflow file.
|
|
51
|
+
* Only `.github/workflows/<name>` where <name> is in INERT_WORKFLOWS qualifies.
|
|
52
|
+
*/
|
|
53
|
+
function isInertCi(filePath) {
|
|
54
|
+
if (!filePath.startsWith('.github/workflows/')) return false;
|
|
55
|
+
const name = filePath.slice('.github/workflows/'.length);
|
|
56
|
+
// Must be a direct child (no further slashes) and in the allowlist.
|
|
57
|
+
return !name.includes('/') && INERT_WORKFLOWS.has(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Tests shared by both the 'workflow automation' and 'inert CI' rules.
|
|
61
|
+
const WORKFLOW_LINT_TESTS = [
|
|
62
|
+
'tests/workflow-shell-pinning.test.cjs',
|
|
63
|
+
'tests/pr-template-policy.test.cjs',
|
|
64
|
+
'tests/lint-pr-check-project-dir.test.cjs',
|
|
65
|
+
];
|
|
7
66
|
|
|
8
67
|
const RULES = [
|
|
9
68
|
{
|
|
10
69
|
name: 'workflow automation',
|
|
11
|
-
|
|
70
|
+
// Only NON-inert .github/workflows/* and all .github/rulesets/* trigger full matrix.
|
|
71
|
+
// FAIL-SAFE: any .github/workflows/*.yml not in INERT_WORKFLOWS is treated as pipeline.
|
|
72
|
+
match: filePath => (filePath.startsWith('.github/workflows/') && !isInertCi(filePath)) ||
|
|
73
|
+
filePath.startsWith('.github/rulesets/'),
|
|
12
74
|
fullMatrix: true,
|
|
13
75
|
tests: [
|
|
14
|
-
|
|
76
|
+
...WORKFLOW_LINT_TESTS,
|
|
15
77
|
'tests/release-tarball-smoke-workflow.test.cjs',
|
|
16
|
-
|
|
17
|
-
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'inert CI',
|
|
82
|
+
match: filePath => isInertCi(filePath),
|
|
83
|
+
fullMatrix: false,
|
|
84
|
+
tests: [
|
|
85
|
+
...WORKFLOW_LINT_TESTS,
|
|
86
|
+
'tests/policy-lint-shallow-checkout.test.cjs',
|
|
18
87
|
],
|
|
19
88
|
},
|
|
20
89
|
{
|
|
@@ -143,14 +212,6 @@ const RULES = [
|
|
|
143
212
|
'tests/docs-parity-live-registry.test.cjs',
|
|
144
213
|
],
|
|
145
214
|
},
|
|
146
|
-
{
|
|
147
|
-
name: 'docs content',
|
|
148
|
-
match: path => path.startsWith('docs/'),
|
|
149
|
-
fullMatrix: false,
|
|
150
|
-
tests: [
|
|
151
|
-
'tests/docs-parity-live-registry.test.cjs',
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
215
|
{
|
|
155
216
|
name: 'configuration',
|
|
156
217
|
match: path => ['config', 'configuration', 'model-catalog', 'model-profile'].some(k => path.includes(k)),
|
|
@@ -198,7 +259,7 @@ function parseArgs(argv) {
|
|
|
198
259
|
if (!out.files) throw new Error('--files requires a value');
|
|
199
260
|
} else if (arg === '--help' || arg === '-h') {
|
|
200
261
|
console.log(usage());
|
|
201
|
-
|
|
262
|
+
throw new ExitError(0);
|
|
202
263
|
} else {
|
|
203
264
|
throw new Error(`unknown argument: ${arg}`);
|
|
204
265
|
}
|
|
@@ -228,7 +289,12 @@ function changedFiles(args) {
|
|
|
228
289
|
if (!args.base || !args.head) {
|
|
229
290
|
throw new Error('--base/--head or --files is required');
|
|
230
291
|
}
|
|
231
|
-
|
|
292
|
+
// Three-dot diff (merge-base...head) matches GitHub's PR "Files changed" semantics.
|
|
293
|
+
// A two-dot `git diff base head` would surface every file `next` gained after this
|
|
294
|
+
// branch's merge-base, mis-flagging product_changed/full_matrix on docs-only PRs cut
|
|
295
|
+
// from a slightly stale base (#837). The `changes` job checks out with fetch-depth: 0,
|
|
296
|
+
// so the merge-base is always available.
|
|
297
|
+
const stdout = execFileSync('git', ['diff', '--name-only', `${args.base}...${args.head}`], {
|
|
232
298
|
encoding: 'utf8',
|
|
233
299
|
});
|
|
234
300
|
return splitFiles(stdout);
|
|
@@ -250,16 +316,30 @@ function classify(files) {
|
|
|
250
316
|
const targeted = new Set();
|
|
251
317
|
const windows = new Set();
|
|
252
318
|
const reasons = [];
|
|
253
|
-
let
|
|
319
|
+
let productOrPipelineChanged = false; // product/pipeline code (excludes docs)
|
|
320
|
+
let inertCiChanged = false; // inert workflow files
|
|
254
321
|
let fullMatrix = false;
|
|
255
322
|
|
|
256
323
|
for (const file of files) {
|
|
257
|
-
|
|
324
|
+
// Determine if this file is product/pipeline code.
|
|
325
|
+
// docs/ and root-level .md files are intentionally excluded.
|
|
326
|
+
if (
|
|
327
|
+
['bin/', 'src/', 'gsd-core/', 'agents/', 'commands/', 'hooks/', 'tests/', 'scripts/'].some(p => file.startsWith(p)) ||
|
|
258
328
|
file === 'package.json' || file === 'package-lock.json' ||
|
|
259
329
|
(file.startsWith('tsconfig') && file.endsWith('.json')) ||
|
|
260
|
-
file.startsWith('.github/
|
|
261
|
-
|
|
262
|
-
|
|
330
|
+
file.startsWith('.github/rulesets/')
|
|
331
|
+
) {
|
|
332
|
+
productOrPipelineChanged = true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Non-inert .github/workflows/* are pipeline code → full matrix.
|
|
336
|
+
if (file.startsWith('.github/workflows/') && !isInertCi(file)) {
|
|
337
|
+
productOrPipelineChanged = true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Inert workflow files set a lightweight signal.
|
|
341
|
+
if (isInertCi(file)) {
|
|
342
|
+
inertCiChanged = true;
|
|
263
343
|
}
|
|
264
344
|
|
|
265
345
|
if (file.startsWith('tests/') && file.endsWith('.test.cjs')) {
|
|
@@ -279,6 +359,10 @@ function classify(files) {
|
|
|
279
359
|
}
|
|
280
360
|
}
|
|
281
361
|
|
|
362
|
+
// code_changed: true when product/pipeline OR inert CI changed.
|
|
363
|
+
// Docs-only PRs (neither flag set) get code_changed=false → full matrix skip.
|
|
364
|
+
const codeChanged = productOrPipelineChanged || inertCiChanged;
|
|
365
|
+
|
|
282
366
|
const targetedTests = existingTests([...targeted].sort());
|
|
283
367
|
|
|
284
368
|
// When code changed but no rule matched any changed file, fall back to the
|
|
@@ -289,8 +373,25 @@ function classify(files) {
|
|
|
289
373
|
|
|
290
374
|
const windowsTests = existingTests([...new Set([...windows, ...targetedTests.filter(isWindowsHint)])].sort());
|
|
291
375
|
|
|
376
|
+
// Inert-CI-only: full_matrix must be false (override any RULES that fired).
|
|
377
|
+
if (inertCiChanged && !productOrPipelineChanged) {
|
|
378
|
+
fullMatrix = false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Normalize: when code_changed is false, the output must be self-consistent.
|
|
382
|
+
// A docs file can coincidentally match a coarse content RULE (e.g. docs/installer-migrations.md
|
|
383
|
+
// matches the installer rule via path.includes('install')), leaving full_matrix=true and
|
|
384
|
+
// non-empty targeted_tests/windows_tests. The workflow skips correctly (gated on code_changed)
|
|
385
|
+
// but the output object would be self-contradictory. Force a clean "nothing to run" result.
|
|
386
|
+
if (!codeChanged) {
|
|
387
|
+
fullMatrix = false;
|
|
388
|
+
targetedTests.length = 0;
|
|
389
|
+
windowsTests.length = 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
292
392
|
return {
|
|
293
393
|
code_changed: codeChanged,
|
|
394
|
+
product_changed: productOrPipelineChanged,
|
|
294
395
|
full_matrix: fullMatrix,
|
|
295
396
|
targeted_tests: targetedTests,
|
|
296
397
|
windows_tests: windowsTests,
|
|
@@ -302,6 +403,7 @@ function writeOutputs(result) {
|
|
|
302
403
|
if (!process.env.GITHUB_OUTPUT) return;
|
|
303
404
|
const lines = [
|
|
304
405
|
`code_changed=${result.code_changed}`,
|
|
406
|
+
`product_changed=${result.product_changed}`,
|
|
305
407
|
`full_matrix=${result.full_matrix}`,
|
|
306
408
|
`targeted_tests=${result.targeted_tests.join(' ')}`,
|
|
307
409
|
`windows_tests=${result.windows_tests.join(' ')}`,
|
|
@@ -312,16 +414,18 @@ function writeOutputs(result) {
|
|
|
312
414
|
function main() {
|
|
313
415
|
try {
|
|
314
416
|
const args = parseArgs(process.argv.slice(2));
|
|
417
|
+
|
|
315
418
|
const files = changedFiles(args);
|
|
316
419
|
const result = classify(files);
|
|
317
420
|
result.changed_files = files;
|
|
318
421
|
writeOutputs(result);
|
|
319
422
|
console.log(JSON.stringify(result, null, 2));
|
|
320
423
|
} catch (error) {
|
|
424
|
+
if (error instanceof ExitError) throw error;
|
|
321
425
|
console.error(`ci-test-scope: ${error.message}`);
|
|
322
426
|
console.error(usage());
|
|
323
|
-
|
|
427
|
+
throw new ExitError(2);
|
|
324
428
|
}
|
|
325
429
|
}
|
|
326
430
|
|
|
327
|
-
main
|
|
431
|
+
runMain(main);
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
const fs = require('node:fs');
|
|
45
45
|
const path = require('node:path');
|
|
46
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
46
47
|
|
|
47
48
|
const EXIT_SHIPPED = 0;
|
|
48
49
|
const EXIT_NOT_SHIPPED = 1;
|
|
@@ -88,60 +89,67 @@ function isPushBlocking(diffPath) {
|
|
|
88
89
|
return diffPath.replace(/\\/g, '/').startsWith('.github/workflows/');
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
function readStdin() {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
let data = '';
|
|
95
|
+
process.stdin.setEncoding('utf8');
|
|
96
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
97
|
+
process.stdin.on('end', () => resolve(data));
|
|
98
|
+
process.stdin.on('error', reject);
|
|
99
|
+
});
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
function main() {
|
|
98
|
-
// Surface ANY uncaught failure as exit 2 (classifier error) rather
|
|
99
|
-
// than letting Node's default-1 shadow the legitimate
|
|
100
|
-
// "no shipped paths" result. Bug #2983.
|
|
101
|
-
process.on('uncaughtException', (err) => fail('uncaught exception', err));
|
|
102
|
-
process.on('unhandledRejection', (err) => fail('unhandled rejection', err));
|
|
103
|
-
|
|
104
|
-
let shipPrefixes;
|
|
102
|
+
async function main() {
|
|
105
103
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
let shipPrefixes;
|
|
105
|
+
try {
|
|
106
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
107
|
+
shipPrefixes = loadShipPrefixes(pkgPath);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
process.stderr.write(`diff-touches-shipped-paths: failed to read package.json from ${process.cwd()}\n`);
|
|
110
|
+
if (err && err.stack) process.stderr.write(`${err.stack}\n`);
|
|
111
|
+
throw new ExitError(EXIT_ERROR);
|
|
112
|
+
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
process.stdin.setEncoding('utf8');
|
|
114
|
-
process.stdin.on('error', (err) => fail('stdin read error', err));
|
|
115
|
-
process.stdin.on('data', (chunk) => {
|
|
116
|
-
buf += chunk;
|
|
117
|
-
});
|
|
118
|
-
process.stdin.on('end', () => {
|
|
114
|
+
let buf;
|
|
119
115
|
try {
|
|
120
|
-
|
|
121
|
-
// #2980 still wins over #3621: any commit touching .github/workflows/*
|
|
122
|
-
// is unpickable regardless of other content because the push step
|
|
123
|
-
// fails on workflow scope rejection. Check this first.
|
|
124
|
-
if (paths.some(isPushBlocking)) {
|
|
125
|
-
process.exit(EXIT_NOT_SHIPPED);
|
|
126
|
-
}
|
|
127
|
-
if (paths.some((p) => isShipped(p, shipPrefixes))) {
|
|
128
|
-
process.exit(EXIT_SHIPPED);
|
|
129
|
-
}
|
|
130
|
-
// #3621: a commit whose only relevant paths are CI-gating tests is
|
|
131
|
-
// still pickable — it can change whether the hotfix CI passes even
|
|
132
|
-
// though it doesn't change what the npm tarball ships.
|
|
133
|
-
if (paths.some(isCiGating)) {
|
|
134
|
-
process.exit(EXIT_SHIPPED);
|
|
135
|
-
}
|
|
136
|
-
process.exit(EXIT_NOT_SHIPPED);
|
|
116
|
+
buf = await readStdin();
|
|
137
117
|
} catch (err) {
|
|
138
|
-
|
|
118
|
+
process.stderr.write(`diff-touches-shipped-paths: stdin read error\n`);
|
|
119
|
+
if (err && err.stack) process.stderr.write(`${err.stack}\n`);
|
|
120
|
+
throw new ExitError(EXIT_ERROR);
|
|
139
121
|
}
|
|
140
|
-
|
|
122
|
+
|
|
123
|
+
const paths = buf.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
124
|
+
// #2980 still wins over #3621: any commit touching .github/workflows/*
|
|
125
|
+
// is unpickable regardless of other content because the push step
|
|
126
|
+
// fails on workflow scope rejection. Check this first.
|
|
127
|
+
if (paths.some(isPushBlocking)) {
|
|
128
|
+
return EXIT_NOT_SHIPPED;
|
|
129
|
+
}
|
|
130
|
+
if (paths.some((p) => isShipped(p, shipPrefixes))) {
|
|
131
|
+
return EXIT_SHIPPED;
|
|
132
|
+
}
|
|
133
|
+
// #3621: a commit whose only relevant paths are CI-gating tests is
|
|
134
|
+
// still pickable — it can change whether the hotfix CI passes even
|
|
135
|
+
// though it doesn't change what the npm tarball ships.
|
|
136
|
+
if (paths.some(isCiGating)) {
|
|
137
|
+
return EXIT_SHIPPED;
|
|
138
|
+
}
|
|
139
|
+
return EXIT_NOT_SHIPPED;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
// Re-throw ExitError unchanged; map any unexpected error to EXIT_ERROR=2
|
|
142
|
+
// (Node's default uncaught-exception code is 1, which is indistinguishable
|
|
143
|
+
// from the legitimate EXIT_NOT_SHIPPED result — bug #2983).
|
|
144
|
+
if (e instanceof ExitError) throw e;
|
|
145
|
+
process.stderr.write(`diff-touches-shipped-paths: classification failed\n`);
|
|
146
|
+
if (e && e.stack) process.stderr.write(`${e.stack}\n`);
|
|
147
|
+
throw new ExitError(EXIT_ERROR);
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
if (require.main === module) {
|
|
144
|
-
main
|
|
152
|
+
runMain(main);
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
module.exports = { loadShipPrefixes, isShipped, isCiGating, isPushBlocking, EXIT_SHIPPED, EXIT_NOT_SHIPPED, EXIT_ERROR };
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
const fs = require('node:fs');
|
|
17
17
|
const path = require('node:path');
|
|
18
18
|
|
|
19
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
20
|
+
|
|
19
21
|
const ROOT = path.resolve(__dirname, '..');
|
|
20
22
|
const MANIFEST_PATH = path.join(ROOT, 'docs', 'INVENTORY-MANIFEST.json');
|
|
21
23
|
|
|
@@ -70,40 +72,44 @@ function buildManifest() {
|
|
|
70
72
|
return manifest;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
function main() {
|
|
76
|
+
const [, , flag] = process.argv;
|
|
74
77
|
|
|
75
|
-
if (flag === '--check') {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
78
|
+
if (flag === '--check') {
|
|
79
|
+
const committed = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
80
|
+
const live = buildManifest();
|
|
81
|
+
// Strip the generated date for comparison
|
|
82
|
+
delete committed.generated;
|
|
83
|
+
delete live.generated;
|
|
84
|
+
const committedStr = JSON.stringify(committed, null, 2);
|
|
85
|
+
const liveStr = JSON.stringify(live, null, 2);
|
|
86
|
+
if (committedStr !== liveStr) {
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
'docs/INVENTORY-MANIFEST.json is stale. Run:\n' +
|
|
89
|
+
' node scripts/gen-inventory-manifest.cjs --write\n' +
|
|
90
|
+
'then add a matching row in docs/INVENTORY.md for each new entry.\n\n',
|
|
91
|
+
);
|
|
92
|
+
// Show diff-friendly output
|
|
93
|
+
for (const family of Object.keys(live.families)) {
|
|
94
|
+
const liveSet = new Set(live.families[family]);
|
|
95
|
+
const committedSet = new Set((committed.families || {})[family] || []);
|
|
96
|
+
for (const name of liveSet) {
|
|
97
|
+
if (!committedSet.has(name)) process.stderr.write(' + ' + family + '/' + name + '\n');
|
|
98
|
+
}
|
|
99
|
+
for (const name of committedSet) {
|
|
100
|
+
if (!liveSet.has(name)) process.stderr.write(' - ' + family + '/' + name + '\n');
|
|
101
|
+
}
|
|
98
102
|
}
|
|
103
|
+
throw new ExitError(1);
|
|
99
104
|
}
|
|
100
|
-
process.
|
|
105
|
+
process.stdout.write('docs/INVENTORY-MANIFEST.json is up to date.\n');
|
|
106
|
+
} else if (flag === '--write') {
|
|
107
|
+
const manifest = buildManifest();
|
|
108
|
+
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
|
109
|
+
process.stdout.write('Wrote ' + MANIFEST_PATH + '\n');
|
|
110
|
+
} else {
|
|
111
|
+
process.stdout.write(JSON.stringify(buildManifest(), null, 2) + '\n');
|
|
101
112
|
}
|
|
102
|
-
process.stdout.write('docs/INVENTORY-MANIFEST.json is up to date.\n');
|
|
103
|
-
} else if (flag === '--write') {
|
|
104
|
-
const manifest = buildManifest();
|
|
105
|
-
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
|
106
|
-
process.stdout.write('Wrote ' + MANIFEST_PATH + '\n');
|
|
107
|
-
} else {
|
|
108
|
-
process.stdout.write(JSON.stringify(buildManifest(), null, 2) + '\n');
|
|
109
113
|
}
|
|
114
|
+
|
|
115
|
+
runMain(main);
|