@opengsd/gsd-core 1.3.1 → 1.4.0-rc.1
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/agents/gsd-advisor-researcher.md +1 -20
- package/agents/gsd-ai-researcher.md +1 -20
- package/agents/gsd-domain-researcher.md +1 -20
- package/agents/gsd-executor.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-ui-researcher.md +2 -21
- package/agents/gsd-verifier.md +8 -2
- package/bin/install.js +85 -4
- package/commands/gsd/graphify.md +11 -6
- package/commands/gsd/import.md +6 -2
- package/commands/gsd/plan-phase.md +2 -2
- package/gsd-core/bin/check-latest-version.cjs +3 -2
- 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/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/security.cjs +73 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +3 -0
- 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 +2 -2
- 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 +22 -5
- package/gsd-core/workflows/ship.md +5 -8
- package/gsd-core/workflows/spec-phase.md +2 -1
- package/gsd-core/workflows/update.md +2 -1
- package/hooks/dist/gsd-context-monitor.js +1 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -0
- package/hooks/dist/gsd-worktree-path-guard.js +1 -1
- package/hooks/gsd-context-monitor.js +1 -1
- package/hooks/gsd-workflow-guard.js +1 -0
- package/hooks/gsd-worktree-path-guard.js +1 -1
- package/package.json +4 -1
- package/scripts/affected-tests-lib.cjs +3 -2
- package/scripts/changeset/cli.cjs +183 -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 +6 -4
- 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/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 +1 -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/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-runtime-launcher.cjs +0 -3
- package/scripts/verify-npm-publish.cjs +14 -26
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
const fs = require('node:fs');
|
|
18
18
|
const path = require('node:path');
|
|
19
19
|
|
|
20
|
-
const {
|
|
20
|
+
const { ExitError, runMain } = require('../lib/cli-exit.cjs');
|
|
21
|
+
const { parseFragment } = require('./parse.cjs');
|
|
21
22
|
const { renderChangelog } = require('./render.cjs');
|
|
22
23
|
const { serializeChangelog, parseChangelog } = require('./serialize.cjs');
|
|
23
24
|
const { renderGithubReleaseNotes } = require('./github-release-notes.cjs');
|
|
@@ -40,6 +41,7 @@ function parseArgs(argv) {
|
|
|
40
41
|
repoSlug: defaultRepoSlug,
|
|
41
42
|
installCommand: `npx ${packageName}@latest`,
|
|
42
43
|
json: false,
|
|
44
|
+
allowEmpty: false,
|
|
43
45
|
};
|
|
44
46
|
if (argv.length === 0) return { ok: true, opts };
|
|
45
47
|
opts.cmd = argv[0];
|
|
@@ -59,6 +61,7 @@ function parseArgs(argv) {
|
|
|
59
61
|
for (let i = 1; i < argv.length; i++) {
|
|
60
62
|
const a = argv[i];
|
|
61
63
|
if (a === '--json') { opts.json = true; continue; }
|
|
64
|
+
if (a === '--allow-empty') { opts.allowEmpty = true; continue; }
|
|
62
65
|
if (
|
|
63
66
|
a === '--repo' ||
|
|
64
67
|
a === '--version' ||
|
|
@@ -118,6 +121,18 @@ function splitChangelog(text) {
|
|
|
118
121
|
return { lead, prior };
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
// FIX 2: tiny local helper so both render paths share identical assembly logic.
|
|
125
|
+
function assembleChangelog(lead, releaseBlock) {
|
|
126
|
+
return [
|
|
127
|
+
lead || '# Changelog',
|
|
128
|
+
'',
|
|
129
|
+
'## [Unreleased]',
|
|
130
|
+
'',
|
|
131
|
+
releaseBlock.replace(/\s+$/, ''),
|
|
132
|
+
'',
|
|
133
|
+
].join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
function cmdRender(opts) {
|
|
122
137
|
const repo = path.resolve(opts.repo);
|
|
123
138
|
const changesetDir = path.join(repo, '.changeset');
|
|
@@ -133,14 +148,83 @@ function cmdRender(opts) {
|
|
|
133
148
|
else failures.push({ file: path.relative(repo, file), reason: r.reason, detail: r.detail || null });
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
// 1. parse-failure → exitCode 1 (unchanged).
|
|
136
152
|
if (failures.length > 0) {
|
|
137
153
|
return { exitCode: 1, report: { consumed: 0, failures } };
|
|
138
154
|
}
|
|
155
|
+
|
|
156
|
+
// 2. Read priorText once; reuse in all subsequent branches.
|
|
157
|
+
const priorText = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, 'utf8') : '';
|
|
158
|
+
|
|
159
|
+
// 3. FIX 1: idempotency guard — if the version is already promoted (a dated
|
|
160
|
+
// release heading for this version already exists in CHANGELOG), split on
|
|
161
|
+
// whether fragments are still present:
|
|
162
|
+
// • alreadyPromoted + zero fragments → legitimate CI-retry no-op (the prior
|
|
163
|
+
// render commit already deleted fragments and wrote the heading).
|
|
164
|
+
// • alreadyPromoted + fragments present → inconsistent state: the heading
|
|
165
|
+
// was written out-of-band but fragments were never consumed. Fail loudly
|
|
166
|
+
// so the operator resolves it manually rather than silently leaving stale
|
|
167
|
+
// fragments to be re-consumed in a later release.
|
|
168
|
+
const version = stripV(opts.version);
|
|
169
|
+
const { releases: existingReleases } = parseChangelog(priorText);
|
|
170
|
+
const alreadyPromoted = existingReleases.some(
|
|
171
|
+
(rel) => rel.version === version && rel.date,
|
|
172
|
+
);
|
|
173
|
+
if (alreadyPromoted) {
|
|
174
|
+
if (fragments.length === 0) {
|
|
175
|
+
return { exitCode: 0, report: { consumed: 0, failures: [], alreadyPromoted: true } };
|
|
176
|
+
}
|
|
177
|
+
const errMsg =
|
|
178
|
+
`CHANGELOG.md already has a dated heading for ${version} but ` +
|
|
179
|
+
`${fragments.length} unconsumed fragment(s) remain in .changeset/ — ` +
|
|
180
|
+
`resolve manually (the version was likely promoted out-of-band).`;
|
|
181
|
+
return {
|
|
182
|
+
exitCode: 1,
|
|
183
|
+
report: { consumed: 0, failures: [], alreadyPromoted: true, error: errMsg },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 4. Zero-fragment + !allowEmpty early-exit: write nothing.
|
|
139
188
|
if (fragments.length === 0) {
|
|
140
|
-
|
|
189
|
+
if (!opts.allowEmpty) {
|
|
190
|
+
return { exitCode: 0, report: { consumed: 0, failures: [] } };
|
|
191
|
+
}
|
|
192
|
+
// --allow-empty: emit a dated heading with a placeholder even though there
|
|
193
|
+
// are no fragments. This lets the render→verify CI chain succeed when a
|
|
194
|
+
// release contains no user-visible changes.
|
|
195
|
+
const { lead, prior } = splitChangelog(priorText);
|
|
196
|
+
// Build a header-only release block and inject the placeholder line.
|
|
197
|
+
const ir = renderChangelog({
|
|
198
|
+
fragments: [],
|
|
199
|
+
version: opts.version,
|
|
200
|
+
date: opts.date,
|
|
201
|
+
priorChangelog: prior || null,
|
|
202
|
+
});
|
|
203
|
+
const headerOnlyBlock = serializeChangelog(ir);
|
|
204
|
+
// Insert placeholder after the release header line.
|
|
205
|
+
// serializeChangelog with no sections yields just "## [v] - d\n" (single
|
|
206
|
+
// trailing newline, no blank line). We replace that trailing newline with
|
|
207
|
+
// a blank line + placeholder + blank line so parseChangelog still sees the
|
|
208
|
+
// dated heading first and the file is human-readable.
|
|
209
|
+
const releaseBlock = headerOnlyBlock.replace(
|
|
210
|
+
/^(##\s+\[[^\]]+\][^\n]*)\n+/,
|
|
211
|
+
'$1\n\n_No notable changes._\n\n',
|
|
212
|
+
);
|
|
213
|
+
// FIX 2: use shared assembleChangelog helper.
|
|
214
|
+
const out = assembleChangelog(lead, releaseBlock);
|
|
215
|
+
fs.writeFileSync(changelogPath, out);
|
|
216
|
+
return {
|
|
217
|
+
exitCode: 0,
|
|
218
|
+
report: {
|
|
219
|
+
consumed: 0,
|
|
220
|
+
failures: [],
|
|
221
|
+
written: true,
|
|
222
|
+
release: { version: opts.version, date: opts.date },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
141
225
|
}
|
|
142
226
|
|
|
143
|
-
|
|
227
|
+
// 5. Normal render path: fragments present — reuse priorText already read above.
|
|
144
228
|
const { lead, prior } = splitChangelog(priorText);
|
|
145
229
|
|
|
146
230
|
const ir = renderChangelog({
|
|
@@ -150,14 +234,8 @@ function cmdRender(opts) {
|
|
|
150
234
|
priorChangelog: prior || null,
|
|
151
235
|
});
|
|
152
236
|
const releaseBlock = serializeChangelog(ir);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'',
|
|
156
|
-
'## [Unreleased]',
|
|
157
|
-
'',
|
|
158
|
-
releaseBlock.replace(/\s+$/, ''),
|
|
159
|
-
'',
|
|
160
|
-
].join('\n');
|
|
237
|
+
// FIX 2: use shared assembleChangelog helper.
|
|
238
|
+
const out = assembleChangelog(lead, releaseBlock);
|
|
161
239
|
|
|
162
240
|
fs.writeFileSync(changelogPath, out);
|
|
163
241
|
|
|
@@ -188,6 +266,14 @@ function cmdRender(opts) {
|
|
|
188
266
|
};
|
|
189
267
|
}
|
|
190
268
|
|
|
269
|
+
function stripV(v) { return typeof v === 'string' ? v.replace(/^v/, '') : v; }
|
|
270
|
+
|
|
271
|
+
function resolveChangelogPath(opts) {
|
|
272
|
+
return opts.changelog
|
|
273
|
+
? path.resolve(opts.changelog)
|
|
274
|
+
: path.join(path.resolve(opts.repo), 'CHANGELOG.md');
|
|
275
|
+
}
|
|
276
|
+
|
|
191
277
|
/**
|
|
192
278
|
* extract subcommand: extracts all changelog release blocks strictly after
|
|
193
279
|
* `--from` (exclusive) up to and including `--to` (inclusive). Both
|
|
@@ -203,7 +289,6 @@ function cmdRender(opts) {
|
|
|
203
289
|
* vague/manual extraction that can silently skip intermediate versions.
|
|
204
290
|
*/
|
|
205
291
|
function cmdExtract(opts) {
|
|
206
|
-
const stripV = (v) => (typeof v === 'string' ? v.replace(/^v/, '') : v);
|
|
207
292
|
const from = stripV(opts.fromRef);
|
|
208
293
|
const to = stripV(opts.toRef);
|
|
209
294
|
|
|
@@ -225,9 +310,7 @@ function cmdExtract(opts) {
|
|
|
225
310
|
};
|
|
226
311
|
}
|
|
227
312
|
|
|
228
|
-
const changelogPath = opts
|
|
229
|
-
? path.resolve(opts.changelog)
|
|
230
|
-
: path.join(path.resolve(opts.repo), 'CHANGELOG.md');
|
|
313
|
+
const changelogPath = resolveChangelogPath(opts);
|
|
231
314
|
|
|
232
315
|
if (!fs.existsSync(changelogPath)) {
|
|
233
316
|
return {
|
|
@@ -282,6 +365,61 @@ function cmdExtract(opts) {
|
|
|
282
365
|
};
|
|
283
366
|
}
|
|
284
367
|
|
|
368
|
+
function cmdVerify(opts) {
|
|
369
|
+
const version = stripV(opts.version);
|
|
370
|
+
|
|
371
|
+
if (!isStableTripletSemver(version)) {
|
|
372
|
+
return {
|
|
373
|
+
exitCode: 1,
|
|
374
|
+
report: { error: `invalid semver for --version: "${version}" (expected N.N.N)`, ok: false },
|
|
375
|
+
textOutput: null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const changelogPath = resolveChangelogPath(opts);
|
|
380
|
+
|
|
381
|
+
if (!fs.existsSync(changelogPath)) {
|
|
382
|
+
return {
|
|
383
|
+
exitCode: 1,
|
|
384
|
+
report: { error: `CHANGELOG not found: ${changelogPath}`, ok: false },
|
|
385
|
+
textOutput: null,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const text = fs.readFileSync(changelogPath, 'utf8');
|
|
390
|
+
const { releases } = parseChangelog(text);
|
|
391
|
+
|
|
392
|
+
const match = releases.find((r) => r.version === version);
|
|
393
|
+
|
|
394
|
+
if (!match) {
|
|
395
|
+
return {
|
|
396
|
+
exitCode: 1,
|
|
397
|
+
report: {
|
|
398
|
+
error: `CHANGELOG.md has no \`## [${version}]\` release heading — promote [Unreleased] into a dated section before releasing (see #690)`,
|
|
399
|
+
ok: false,
|
|
400
|
+
},
|
|
401
|
+
textOutput: null,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!match.date) {
|
|
406
|
+
return {
|
|
407
|
+
exitCode: 1,
|
|
408
|
+
report: {
|
|
409
|
+
error: `CHANGELOG.md heading \`## [${version}]\` has no date — expected \`## [${version}] - YYYY-MM-DD\``,
|
|
410
|
+
ok: false,
|
|
411
|
+
},
|
|
412
|
+
textOutput: null,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
exitCode: 0,
|
|
418
|
+
report: { ok: true, version, date: match.date },
|
|
419
|
+
textOutput: `CHANGELOG.md has a dated heading for ${version} (${match.date})`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
285
423
|
function cmdGithubReleaseNotes(opts) {
|
|
286
424
|
const repo = path.resolve(opts.repo);
|
|
287
425
|
const report = renderGithubReleaseNotes({
|
|
@@ -322,12 +460,13 @@ function cmdGithubReleaseNotes(opts) {
|
|
|
322
460
|
function usage() {
|
|
323
461
|
return [
|
|
324
462
|
'usage:',
|
|
325
|
-
' changeset/cli.cjs render --repo <dir> --version V --date D [--json]',
|
|
463
|
+
' changeset/cli.cjs render --repo <dir> --version V --date D [--allow-empty] [--json]',
|
|
326
464
|
' changeset/cli.cjs github-release-notes --repo <dir> --from REF --to REF [--output FILE] [--repo-slug OWNER/REPO] [--install-command CMD] [--json]',
|
|
327
465
|
' changeset/cli.cjs extract --from VERSION --to VERSION [--changelog FILE] [--repo <dir>] [--json]',
|
|
328
466
|
' Extracts changelog entries strictly after --from (exclusive) and up to',
|
|
329
467
|
' and including --to (inclusive). Accepts v-prefixed versions.',
|
|
330
468
|
' Exit 2 when no releases fall in range.',
|
|
469
|
+
' changeset/cli.cjs verify --version <X.Y.Z> [--changelog <path>] Exit non-zero if CHANGELOG.md has no dated `## [X.Y.Z]` heading (release gate, #690)',
|
|
331
470
|
'',
|
|
332
471
|
].join('\n');
|
|
333
472
|
}
|
|
@@ -337,25 +476,26 @@ function main() {
|
|
|
337
476
|
if (!parsed.ok) {
|
|
338
477
|
process.stderr.write(`${parsed.error}\n`);
|
|
339
478
|
process.stderr.write(usage());
|
|
340
|
-
|
|
479
|
+
throw new ExitError(2);
|
|
341
480
|
}
|
|
342
481
|
const { opts } = parsed;
|
|
343
|
-
if (opts.cmd !== 'render' && opts.cmd !== 'github-release-notes' && opts.cmd !== 'extract') {
|
|
482
|
+
if (opts.cmd !== 'render' && opts.cmd !== 'github-release-notes' && opts.cmd !== 'extract' && opts.cmd !== 'verify') {
|
|
344
483
|
process.stderr.write(usage());
|
|
345
|
-
|
|
484
|
+
throw new ExitError(1);
|
|
346
485
|
}
|
|
347
486
|
if (opts.cmd === 'render' && (!opts.version || !opts.date)) {
|
|
348
|
-
|
|
349
|
-
process.exit(2);
|
|
487
|
+
throw new ExitError(2, '--version and --date are required for render');
|
|
350
488
|
}
|
|
351
489
|
if (opts.cmd === 'github-release-notes' && (!opts.fromRef || !opts.toRef)) {
|
|
352
|
-
|
|
353
|
-
process.exit(2);
|
|
490
|
+
throw new ExitError(2, '--from and --to are required for github-release-notes');
|
|
354
491
|
}
|
|
355
492
|
if (opts.cmd === 'extract' && (!opts.fromRef || !opts.toRef)) {
|
|
356
493
|
process.stderr.write('--from and --to are required for extract\n');
|
|
357
494
|
process.stderr.write(usage());
|
|
358
|
-
|
|
495
|
+
throw new ExitError(1);
|
|
496
|
+
}
|
|
497
|
+
if (opts.cmd === 'verify' && !opts.version) {
|
|
498
|
+
throw new ExitError(2, '--version is required for verify');
|
|
359
499
|
}
|
|
360
500
|
|
|
361
501
|
if (opts.cmd === 'extract') {
|
|
@@ -367,7 +507,19 @@ function main() {
|
|
|
367
507
|
} else if (exitCode === 2) {
|
|
368
508
|
process.stderr.write(`no releases found in range (from=${report.from}, to=${report.to})\n`);
|
|
369
509
|
}
|
|
370
|
-
|
|
510
|
+
return exitCode;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (opts.cmd === 'verify') {
|
|
514
|
+
const { exitCode, report, textOutput } = cmdVerify(opts);
|
|
515
|
+
if (opts.json) {
|
|
516
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
517
|
+
} else if (textOutput) {
|
|
518
|
+
process.stdout.write(textOutput + '\n');
|
|
519
|
+
} else {
|
|
520
|
+
process.stderr.write(report.error + '\n');
|
|
521
|
+
}
|
|
522
|
+
return exitCode;
|
|
371
523
|
}
|
|
372
524
|
|
|
373
525
|
const { exitCode, report } = opts.cmd === 'render' ? cmdRender(opts) : cmdGithubReleaseNotes(opts);
|
|
@@ -376,6 +528,9 @@ function main() {
|
|
|
376
528
|
} else if (opts.cmd === 'github-release-notes' && report.body) {
|
|
377
529
|
process.stdout.write(report.body);
|
|
378
530
|
} else {
|
|
531
|
+
if (report.error) {
|
|
532
|
+
process.stderr.write(`${report.error}\n`);
|
|
533
|
+
}
|
|
379
534
|
process.stdout.write(`Consumed: ${report.consumed} fragment(s)\n`);
|
|
380
535
|
if (report.failures.length > 0) {
|
|
381
536
|
process.stdout.write(`Failures: ${report.failures.length}\n`);
|
|
@@ -384,9 +539,9 @@ function main() {
|
|
|
384
539
|
}
|
|
385
540
|
}
|
|
386
541
|
}
|
|
387
|
-
|
|
542
|
+
return exitCode;
|
|
388
543
|
}
|
|
389
544
|
|
|
390
|
-
if (require.main === module) main
|
|
545
|
+
if (require.main === module) runMain(main);
|
|
391
546
|
|
|
392
|
-
module.exports = { cmdRender, cmdExtract, cmdGithubReleaseNotes, parseArgs, splitChangelog, listFragmentFiles, usage };
|
|
547
|
+
module.exports = { cmdRender, cmdExtract, cmdVerify, cmdGithubReleaseNotes, parseArgs, splitChangelog, assembleChangelog, listFragmentFiles, usage };
|
|
@@ -60,6 +60,8 @@ function evaluateLint({ changedFiles, labels }) {
|
|
|
60
60
|
return { ok: false, reason: LINT_REASON.FAIL_MISSING_FRAGMENT };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const { ExitError, runMain } = require('../lib/cli-exit.cjs');
|
|
64
|
+
|
|
63
65
|
function main() {
|
|
64
66
|
const fs = require('node:fs');
|
|
65
67
|
const cp = require('node:child_process');
|
|
@@ -87,8 +89,7 @@ function main() {
|
|
|
87
89
|
);
|
|
88
90
|
changedFiles = out.split('\n').filter(Boolean);
|
|
89
91
|
} catch (e) {
|
|
90
|
-
|
|
91
|
-
process.exit(2);
|
|
92
|
+
throw new ExitError(2, `could not compute diff: ${e.message}`);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
const verdict = evaluateLint({ changedFiles, labels });
|
|
@@ -102,9 +103,9 @@ function main() {
|
|
|
102
103
|
process.stderr.write(`Run \`npm run changeset\` to create one, or add the \`${OPT_OUT_LABEL}\` label\n`);
|
|
103
104
|
process.stderr.write(`if this PR genuinely has no user-facing impact (test refactor, CI tweak, etc.).\n`);
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
+
return verdict.ok ? 0 : 1;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
if (require.main === module) main
|
|
109
|
+
if (require.main === module) runMain(main);
|
|
109
110
|
|
|
110
111
|
module.exports = { evaluateLint, LINT_REASON, OPT_OUT_LABEL, isUserFacing, isFragment };
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('node:fs');
|
|
15
15
|
const path = require('node:path');
|
|
16
|
+
const { ExitError, runMain } = require('../lib/cli-exit.cjs');
|
|
16
17
|
|
|
17
18
|
// Small word lists — keep the function simple and dependency-free.
|
|
18
19
|
// Together this gives ~40 * 40 * 40 = 64,000 distinct names. The lint
|
|
@@ -121,17 +122,16 @@ function main() {
|
|
|
121
122
|
if (!parsed.ok) {
|
|
122
123
|
process.stderr.write(`${parsed.error}\n`);
|
|
123
124
|
process.stderr.write('usage: changeset/new.cjs --type <Fixed|Added|...> --pr NNNN --body "..."\n');
|
|
124
|
-
|
|
125
|
+
throw new ExitError(2);
|
|
125
126
|
}
|
|
126
127
|
const { opts } = parsed;
|
|
127
128
|
if (!opts.type || !opts.pr || !opts.body) {
|
|
128
|
-
|
|
129
|
-
process.exit(2);
|
|
129
|
+
throw new ExitError(2, 'usage: changeset/new.cjs --type <Fixed|Added|...> --pr NNNN --body "..."');
|
|
130
130
|
}
|
|
131
131
|
const file = scaffoldFragment(opts);
|
|
132
132
|
process.stdout.write(`${path.relative(process.cwd(), file)}\n`);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
if (require.main === module) main
|
|
135
|
+
if (require.main === module) runMain(main);
|
|
136
136
|
|
|
137
137
|
module.exports = { generateFragmentName, scaffoldFragment, parseArgs, ALLOWED_TYPES };
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
7
|
+
|
|
6
8
|
const ROOT = path.resolve(__dirname, '..');
|
|
7
9
|
const aliasesPath = path.join(ROOT, 'gsd-core', 'bin', 'lib', 'command-aliases.cjs');
|
|
8
10
|
|
|
9
11
|
function fail(message) {
|
|
10
12
|
process.stderr.write(`${message}\n`);
|
|
11
|
-
|
|
13
|
+
throw new ExitError(1);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
function ensureArray(value, name) {
|
|
@@ -27,82 +29,86 @@ function assertNoDuplicates(values, label) {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const aliases = require(aliasesPath);
|
|
35
|
-
|
|
36
|
-
const families = [
|
|
37
|
-
{
|
|
38
|
-
commandAliases: 'STATE_COMMAND_ALIASES',
|
|
39
|
-
subcommands: 'STATE_SUBCOMMANDS',
|
|
40
|
-
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'state-command-router.cjs'),
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
commandAliases: 'VERIFY_COMMAND_ALIASES',
|
|
44
|
-
subcommands: 'VERIFY_SUBCOMMANDS',
|
|
45
|
-
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'verify-command-router.cjs'),
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
commandAliases: 'INIT_COMMAND_ALIASES',
|
|
49
|
-
subcommands: 'INIT_SUBCOMMANDS',
|
|
50
|
-
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'init-command-router.cjs'),
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
commandAliases: 'PHASE_COMMAND_ALIASES',
|
|
54
|
-
subcommands: 'PHASE_SUBCOMMANDS',
|
|
55
|
-
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'phase-command-router.cjs'),
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
commandAliases: 'PHASES_COMMAND_ALIASES',
|
|
59
|
-
subcommands: 'PHASES_SUBCOMMANDS',
|
|
60
|
-
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'phases-command-router.cjs'),
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
commandAliases: 'VALIDATE_COMMAND_ALIASES',
|
|
64
|
-
subcommands: 'VALIDATE_SUBCOMMANDS',
|
|
65
|
-
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'validate-command-router.cjs'),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
commandAliases: 'ROADMAP_COMMAND_ALIASES',
|
|
69
|
-
subcommands: 'ROADMAP_SUBCOMMANDS',
|
|
70
|
-
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'roadmap-command-router.cjs'),
|
|
71
|
-
},
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
for (const family of families) {
|
|
75
|
-
const commandAliases = aliases[family.commandAliases];
|
|
76
|
-
const subcommands = aliases[family.subcommands];
|
|
77
|
-
|
|
78
|
-
ensureArray(commandAliases, family.commandAliases);
|
|
79
|
-
ensureArray(subcommands, family.subcommands);
|
|
80
|
-
|
|
81
|
-
const derivedSubcommands = commandAliases.map((entry) => entry && entry.subcommand);
|
|
82
|
-
assertNoDuplicates(derivedSubcommands, `${family.commandAliases}.subcommand`);
|
|
83
|
-
|
|
84
|
-
if (derivedSubcommands.length !== subcommands.length) {
|
|
85
|
-
fail(
|
|
86
|
-
`check:alias-drift: ${family.subcommands} length ${subcommands.length} does not match ` +
|
|
87
|
-
`${family.commandAliases} length ${derivedSubcommands.length}`,
|
|
88
|
-
);
|
|
32
|
+
function main() {
|
|
33
|
+
if (!fs.existsSync(aliasesPath)) {
|
|
34
|
+
fail(`check:alias-drift: missing ${path.relative(ROOT, aliasesPath)}`);
|
|
89
35
|
}
|
|
90
36
|
|
|
91
|
-
|
|
92
|
-
|
|
37
|
+
const aliases = require(aliasesPath);
|
|
38
|
+
|
|
39
|
+
const families = [
|
|
40
|
+
{
|
|
41
|
+
commandAliases: 'STATE_COMMAND_ALIASES',
|
|
42
|
+
subcommands: 'STATE_SUBCOMMANDS',
|
|
43
|
+
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'state-command-router.cjs'),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
commandAliases: 'VERIFY_COMMAND_ALIASES',
|
|
47
|
+
subcommands: 'VERIFY_SUBCOMMANDS',
|
|
48
|
+
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'verify-command-router.cjs'),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
commandAliases: 'INIT_COMMAND_ALIASES',
|
|
52
|
+
subcommands: 'INIT_SUBCOMMANDS',
|
|
53
|
+
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'init-command-router.cjs'),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
commandAliases: 'PHASE_COMMAND_ALIASES',
|
|
57
|
+
subcommands: 'PHASE_SUBCOMMANDS',
|
|
58
|
+
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'phase-command-router.cjs'),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
commandAliases: 'PHASES_COMMAND_ALIASES',
|
|
62
|
+
subcommands: 'PHASES_SUBCOMMANDS',
|
|
63
|
+
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'phases-command-router.cjs'),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
commandAliases: 'VALIDATE_COMMAND_ALIASES',
|
|
67
|
+
subcommands: 'VALIDATE_SUBCOMMANDS',
|
|
68
|
+
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'validate-command-router.cjs'),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
commandAliases: 'ROADMAP_COMMAND_ALIASES',
|
|
72
|
+
subcommands: 'ROADMAP_SUBCOMMANDS',
|
|
73
|
+
routerPath: path.join(ROOT, 'gsd-core', 'bin', 'lib', 'roadmap-command-router.cjs'),
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const family of families) {
|
|
78
|
+
const commandAliases = aliases[family.commandAliases];
|
|
79
|
+
const subcommands = aliases[family.subcommands];
|
|
80
|
+
|
|
81
|
+
ensureArray(commandAliases, family.commandAliases);
|
|
82
|
+
ensureArray(subcommands, family.subcommands);
|
|
83
|
+
|
|
84
|
+
const derivedSubcommands = commandAliases.map((entry) => entry && entry.subcommand);
|
|
85
|
+
assertNoDuplicates(derivedSubcommands, `${family.commandAliases}.subcommand`);
|
|
86
|
+
|
|
87
|
+
if (derivedSubcommands.length !== subcommands.length) {
|
|
93
88
|
fail(
|
|
94
|
-
`check:alias-drift: ${family.subcommands}
|
|
95
|
-
|
|
89
|
+
`check:alias-drift: ${family.subcommands} length ${subcommands.length} does not match ` +
|
|
90
|
+
`${family.commandAliases} length ${derivedSubcommands.length}`,
|
|
96
91
|
);
|
|
97
92
|
}
|
|
98
|
-
}
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
for (let i = 0; i < derivedSubcommands.length; i++) {
|
|
95
|
+
if (derivedSubcommands[i] !== subcommands[i]) {
|
|
96
|
+
fail(
|
|
97
|
+
`check:alias-drift: ${family.subcommands}[${i}] = "${subcommands[i]}" ` +
|
|
98
|
+
`does not match ${family.commandAliases}[${i}].subcommand = "${derivedSubcommands[i]}"`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const routerSource = fs.readFileSync(family.routerPath, 'utf8');
|
|
104
|
+
if (!routerSource.includes(family.subcommands)) {
|
|
105
|
+
fail(
|
|
106
|
+
`check:alias-drift: ${path.relative(ROOT, family.routerPath)} does not reference ${family.subcommands}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
105
109
|
}
|
|
110
|
+
|
|
111
|
+
process.stdout.write('check:alias-drift ok\n');
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
|
|
114
|
+
runMain(main);
|