@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
|
@@ -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,8 @@ function parseArgs(argv) {
|
|
|
40
41
|
repoSlug: defaultRepoSlug,
|
|
41
42
|
installCommand: `npx ${packageName}@latest`,
|
|
42
43
|
json: false,
|
|
44
|
+
allowEmpty: false,
|
|
45
|
+
preview: false,
|
|
43
46
|
};
|
|
44
47
|
if (argv.length === 0) return { ok: true, opts };
|
|
45
48
|
opts.cmd = argv[0];
|
|
@@ -59,6 +62,8 @@ function parseArgs(argv) {
|
|
|
59
62
|
for (let i = 1; i < argv.length; i++) {
|
|
60
63
|
const a = argv[i];
|
|
61
64
|
if (a === '--json') { opts.json = true; continue; }
|
|
65
|
+
if (a === '--allow-empty') { opts.allowEmpty = true; continue; }
|
|
66
|
+
if (a === '--preview') { opts.preview = true; continue; }
|
|
62
67
|
if (
|
|
63
68
|
a === '--repo' ||
|
|
64
69
|
a === '--version' ||
|
|
@@ -118,6 +123,31 @@ function splitChangelog(text) {
|
|
|
118
123
|
return { lead, prior };
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
// FIX 2: tiny local helper so both render paths share identical assembly logic.
|
|
127
|
+
function assembleChangelog(lead, releaseBlock) {
|
|
128
|
+
return [
|
|
129
|
+
lead || '# Changelog',
|
|
130
|
+
'',
|
|
131
|
+
'## [Unreleased]',
|
|
132
|
+
'',
|
|
133
|
+
releaseBlock.replace(/\s+$/, ''),
|
|
134
|
+
'',
|
|
135
|
+
].join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Insert a "_No notable changes._" placeholder after the dated release heading
|
|
139
|
+
// of an otherwise-empty release block. serializeChangelog with no sections
|
|
140
|
+
// yields just "## [v] - d\n"; we expand the trailing newline into a blank line
|
|
141
|
+
// + placeholder + blank line so parseChangelog still sees the dated heading
|
|
142
|
+
// first and the output is human-readable. Shared by the --allow-empty and
|
|
143
|
+
// --preview zero-fragment paths so they can never drift.
|
|
144
|
+
function injectEmptyPlaceholder(headerOnlyBlock) {
|
|
145
|
+
return headerOnlyBlock.replace(
|
|
146
|
+
/^(##\s+\[[^\]]+\][^\n]*)\n+/,
|
|
147
|
+
'$1\n\n_No notable changes._\n\n',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
function cmdRender(opts) {
|
|
122
152
|
const repo = path.resolve(opts.repo);
|
|
123
153
|
const changesetDir = path.join(repo, '.changeset');
|
|
@@ -133,14 +163,107 @@ function cmdRender(opts) {
|
|
|
133
163
|
else failures.push({ file: path.relative(repo, file), reason: r.reason, detail: r.detail || null });
|
|
134
164
|
}
|
|
135
165
|
|
|
166
|
+
// 1. parse-failure → exitCode 1 (unchanged).
|
|
136
167
|
if (failures.length > 0) {
|
|
137
168
|
return { exitCode: 1, report: { consumed: 0, failures } };
|
|
138
169
|
}
|
|
170
|
+
|
|
171
|
+
// 2. Read priorText once; reuse in all subsequent branches.
|
|
172
|
+
const priorText = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, 'utf8') : '';
|
|
173
|
+
|
|
174
|
+
// Preview mode (#759): render the dated release section WITHOUT writing
|
|
175
|
+
// CHANGELOG.md and WITHOUT consuming .changeset fragments. Used by the rc
|
|
176
|
+
// release job to surface the curated notes for the version under test while
|
|
177
|
+
// leaving the fragment set intact for the eventual finalize render.
|
|
178
|
+
if (opts.preview) {
|
|
179
|
+
// priorChangelog is intentionally null: a preview shows ONLY the new dated
|
|
180
|
+
// section for the version under test, not the full file history.
|
|
181
|
+
// serializeChangelog appends priorChangelog verbatim, so passing the prior
|
|
182
|
+
// text here would dump every past release into the rc job summary.
|
|
183
|
+
const ir = renderChangelog({
|
|
184
|
+
fragments,
|
|
185
|
+
version: opts.version,
|
|
186
|
+
date: opts.date,
|
|
187
|
+
priorChangelog: null,
|
|
188
|
+
});
|
|
189
|
+
let releaseBlock = serializeChangelog(ir);
|
|
190
|
+
if (fragments.length === 0) {
|
|
191
|
+
// Mirror --allow-empty: a no-fragment release still shows a dated heading
|
|
192
|
+
// with a placeholder rather than an empty block.
|
|
193
|
+
releaseBlock = injectEmptyPlaceholder(releaseBlock);
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
exitCode: 0,
|
|
197
|
+
report: {
|
|
198
|
+
consumed: 0,
|
|
199
|
+
failures: [],
|
|
200
|
+
preview: releaseBlock,
|
|
201
|
+
fragmentCount: fragments.length,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 3. FIX 1: idempotency guard — if the version is already promoted (a dated
|
|
207
|
+
// release heading for this version already exists in CHANGELOG), split on
|
|
208
|
+
// whether fragments are still present:
|
|
209
|
+
// • alreadyPromoted + zero fragments → legitimate CI-retry no-op (the prior
|
|
210
|
+
// render commit already deleted fragments and wrote the heading).
|
|
211
|
+
// • alreadyPromoted + fragments present → inconsistent state: the heading
|
|
212
|
+
// was written out-of-band but fragments were never consumed. Fail loudly
|
|
213
|
+
// so the operator resolves it manually rather than silently leaving stale
|
|
214
|
+
// fragments to be re-consumed in a later release.
|
|
215
|
+
const version = stripV(opts.version);
|
|
216
|
+
const { releases: existingReleases } = parseChangelog(priorText);
|
|
217
|
+
const alreadyPromoted = existingReleases.some(
|
|
218
|
+
(rel) => rel.version === version && rel.date,
|
|
219
|
+
);
|
|
220
|
+
if (alreadyPromoted) {
|
|
221
|
+
if (fragments.length === 0) {
|
|
222
|
+
return { exitCode: 0, report: { consumed: 0, failures: [], alreadyPromoted: true } };
|
|
223
|
+
}
|
|
224
|
+
const errMsg =
|
|
225
|
+
`CHANGELOG.md already has a dated heading for ${version} but ` +
|
|
226
|
+
`${fragments.length} unconsumed fragment(s) remain in .changeset/ — ` +
|
|
227
|
+
`resolve manually (the version was likely promoted out-of-band).`;
|
|
228
|
+
return {
|
|
229
|
+
exitCode: 1,
|
|
230
|
+
report: { consumed: 0, failures: [], alreadyPromoted: true, error: errMsg },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 4. Zero-fragment + !allowEmpty early-exit: write nothing.
|
|
139
235
|
if (fragments.length === 0) {
|
|
140
|
-
|
|
236
|
+
if (!opts.allowEmpty) {
|
|
237
|
+
return { exitCode: 0, report: { consumed: 0, failures: [] } };
|
|
238
|
+
}
|
|
239
|
+
// --allow-empty: emit a dated heading with a placeholder even though there
|
|
240
|
+
// are no fragments. This lets the render→verify CI chain succeed when a
|
|
241
|
+
// release contains no user-visible changes.
|
|
242
|
+
const { lead, prior } = splitChangelog(priorText);
|
|
243
|
+
// Build a header-only release block and inject the placeholder line.
|
|
244
|
+
const ir = renderChangelog({
|
|
245
|
+
fragments: [],
|
|
246
|
+
version: opts.version,
|
|
247
|
+
date: opts.date,
|
|
248
|
+
priorChangelog: prior || null,
|
|
249
|
+
});
|
|
250
|
+
const headerOnlyBlock = serializeChangelog(ir);
|
|
251
|
+
const releaseBlock = injectEmptyPlaceholder(headerOnlyBlock);
|
|
252
|
+
// FIX 2: use shared assembleChangelog helper.
|
|
253
|
+
const out = assembleChangelog(lead, releaseBlock);
|
|
254
|
+
fs.writeFileSync(changelogPath, out);
|
|
255
|
+
return {
|
|
256
|
+
exitCode: 0,
|
|
257
|
+
report: {
|
|
258
|
+
consumed: 0,
|
|
259
|
+
failures: [],
|
|
260
|
+
written: true,
|
|
261
|
+
release: { version: opts.version, date: opts.date },
|
|
262
|
+
},
|
|
263
|
+
};
|
|
141
264
|
}
|
|
142
265
|
|
|
143
|
-
|
|
266
|
+
// 5. Normal render path: fragments present — reuse priorText already read above.
|
|
144
267
|
const { lead, prior } = splitChangelog(priorText);
|
|
145
268
|
|
|
146
269
|
const ir = renderChangelog({
|
|
@@ -150,14 +273,8 @@ function cmdRender(opts) {
|
|
|
150
273
|
priorChangelog: prior || null,
|
|
151
274
|
});
|
|
152
275
|
const releaseBlock = serializeChangelog(ir);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'',
|
|
156
|
-
'## [Unreleased]',
|
|
157
|
-
'',
|
|
158
|
-
releaseBlock.replace(/\s+$/, ''),
|
|
159
|
-
'',
|
|
160
|
-
].join('\n');
|
|
276
|
+
// FIX 2: use shared assembleChangelog helper.
|
|
277
|
+
const out = assembleChangelog(lead, releaseBlock);
|
|
161
278
|
|
|
162
279
|
fs.writeFileSync(changelogPath, out);
|
|
163
280
|
|
|
@@ -188,6 +305,14 @@ function cmdRender(opts) {
|
|
|
188
305
|
};
|
|
189
306
|
}
|
|
190
307
|
|
|
308
|
+
function stripV(v) { return typeof v === 'string' ? v.replace(/^v/, '') : v; }
|
|
309
|
+
|
|
310
|
+
function resolveChangelogPath(opts) {
|
|
311
|
+
return opts.changelog
|
|
312
|
+
? path.resolve(opts.changelog)
|
|
313
|
+
: path.join(path.resolve(opts.repo), 'CHANGELOG.md');
|
|
314
|
+
}
|
|
315
|
+
|
|
191
316
|
/**
|
|
192
317
|
* extract subcommand: extracts all changelog release blocks strictly after
|
|
193
318
|
* `--from` (exclusive) up to and including `--to` (inclusive). Both
|
|
@@ -203,7 +328,6 @@ function cmdRender(opts) {
|
|
|
203
328
|
* vague/manual extraction that can silently skip intermediate versions.
|
|
204
329
|
*/
|
|
205
330
|
function cmdExtract(opts) {
|
|
206
|
-
const stripV = (v) => (typeof v === 'string' ? v.replace(/^v/, '') : v);
|
|
207
331
|
const from = stripV(opts.fromRef);
|
|
208
332
|
const to = stripV(opts.toRef);
|
|
209
333
|
|
|
@@ -225,9 +349,7 @@ function cmdExtract(opts) {
|
|
|
225
349
|
};
|
|
226
350
|
}
|
|
227
351
|
|
|
228
|
-
const changelogPath = opts
|
|
229
|
-
? path.resolve(opts.changelog)
|
|
230
|
-
: path.join(path.resolve(opts.repo), 'CHANGELOG.md');
|
|
352
|
+
const changelogPath = resolveChangelogPath(opts);
|
|
231
353
|
|
|
232
354
|
if (!fs.existsSync(changelogPath)) {
|
|
233
355
|
return {
|
|
@@ -282,6 +404,61 @@ function cmdExtract(opts) {
|
|
|
282
404
|
};
|
|
283
405
|
}
|
|
284
406
|
|
|
407
|
+
function cmdVerify(opts) {
|
|
408
|
+
const version = stripV(opts.version);
|
|
409
|
+
|
|
410
|
+
if (!isStableTripletSemver(version)) {
|
|
411
|
+
return {
|
|
412
|
+
exitCode: 1,
|
|
413
|
+
report: { error: `invalid semver for --version: "${version}" (expected N.N.N)`, ok: false },
|
|
414
|
+
textOutput: null,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const changelogPath = resolveChangelogPath(opts);
|
|
419
|
+
|
|
420
|
+
if (!fs.existsSync(changelogPath)) {
|
|
421
|
+
return {
|
|
422
|
+
exitCode: 1,
|
|
423
|
+
report: { error: `CHANGELOG not found: ${changelogPath}`, ok: false },
|
|
424
|
+
textOutput: null,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const text = fs.readFileSync(changelogPath, 'utf8');
|
|
429
|
+
const { releases } = parseChangelog(text);
|
|
430
|
+
|
|
431
|
+
const match = releases.find((r) => r.version === version);
|
|
432
|
+
|
|
433
|
+
if (!match) {
|
|
434
|
+
return {
|
|
435
|
+
exitCode: 1,
|
|
436
|
+
report: {
|
|
437
|
+
error: `CHANGELOG.md has no \`## [${version}]\` release heading — promote [Unreleased] into a dated section before releasing (see #690)`,
|
|
438
|
+
ok: false,
|
|
439
|
+
},
|
|
440
|
+
textOutput: null,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!match.date) {
|
|
445
|
+
return {
|
|
446
|
+
exitCode: 1,
|
|
447
|
+
report: {
|
|
448
|
+
error: `CHANGELOG.md heading \`## [${version}]\` has no date — expected \`## [${version}] - YYYY-MM-DD\``,
|
|
449
|
+
ok: false,
|
|
450
|
+
},
|
|
451
|
+
textOutput: null,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
exitCode: 0,
|
|
457
|
+
report: { ok: true, version, date: match.date },
|
|
458
|
+
textOutput: `CHANGELOG.md has a dated heading for ${version} (${match.date})`,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
285
462
|
function cmdGithubReleaseNotes(opts) {
|
|
286
463
|
const repo = path.resolve(opts.repo);
|
|
287
464
|
const report = renderGithubReleaseNotes({
|
|
@@ -322,12 +499,14 @@ function cmdGithubReleaseNotes(opts) {
|
|
|
322
499
|
function usage() {
|
|
323
500
|
return [
|
|
324
501
|
'usage:',
|
|
325
|
-
' changeset/cli.cjs render --repo <dir> --version V --date D [--json]',
|
|
502
|
+
' changeset/cli.cjs render --repo <dir> --version V --date D [--allow-empty] [--preview] [--json]',
|
|
503
|
+
' --preview renders the dated section to stdout without writing CHANGELOG.md or consuming fragments.',
|
|
326
504
|
' changeset/cli.cjs github-release-notes --repo <dir> --from REF --to REF [--output FILE] [--repo-slug OWNER/REPO] [--install-command CMD] [--json]',
|
|
327
505
|
' changeset/cli.cjs extract --from VERSION --to VERSION [--changelog FILE] [--repo <dir>] [--json]',
|
|
328
506
|
' Extracts changelog entries strictly after --from (exclusive) and up to',
|
|
329
507
|
' and including --to (inclusive). Accepts v-prefixed versions.',
|
|
330
508
|
' Exit 2 when no releases fall in range.',
|
|
509
|
+
' 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
510
|
'',
|
|
332
511
|
].join('\n');
|
|
333
512
|
}
|
|
@@ -337,25 +516,26 @@ function main() {
|
|
|
337
516
|
if (!parsed.ok) {
|
|
338
517
|
process.stderr.write(`${parsed.error}\n`);
|
|
339
518
|
process.stderr.write(usage());
|
|
340
|
-
|
|
519
|
+
throw new ExitError(2);
|
|
341
520
|
}
|
|
342
521
|
const { opts } = parsed;
|
|
343
|
-
if (opts.cmd !== 'render' && opts.cmd !== 'github-release-notes' && opts.cmd !== 'extract') {
|
|
522
|
+
if (opts.cmd !== 'render' && opts.cmd !== 'github-release-notes' && opts.cmd !== 'extract' && opts.cmd !== 'verify') {
|
|
344
523
|
process.stderr.write(usage());
|
|
345
|
-
|
|
524
|
+
throw new ExitError(1);
|
|
346
525
|
}
|
|
347
526
|
if (opts.cmd === 'render' && (!opts.version || !opts.date)) {
|
|
348
|
-
|
|
349
|
-
process.exit(2);
|
|
527
|
+
throw new ExitError(2, '--version and --date are required for render');
|
|
350
528
|
}
|
|
351
529
|
if (opts.cmd === 'github-release-notes' && (!opts.fromRef || !opts.toRef)) {
|
|
352
|
-
|
|
353
|
-
process.exit(2);
|
|
530
|
+
throw new ExitError(2, '--from and --to are required for github-release-notes');
|
|
354
531
|
}
|
|
355
532
|
if (opts.cmd === 'extract' && (!opts.fromRef || !opts.toRef)) {
|
|
356
533
|
process.stderr.write('--from and --to are required for extract\n');
|
|
357
534
|
process.stderr.write(usage());
|
|
358
|
-
|
|
535
|
+
throw new ExitError(1);
|
|
536
|
+
}
|
|
537
|
+
if (opts.cmd === 'verify' && !opts.version) {
|
|
538
|
+
throw new ExitError(2, '--version is required for verify');
|
|
359
539
|
}
|
|
360
540
|
|
|
361
541
|
if (opts.cmd === 'extract') {
|
|
@@ -367,15 +547,33 @@ function main() {
|
|
|
367
547
|
} else if (exitCode === 2) {
|
|
368
548
|
process.stderr.write(`no releases found in range (from=${report.from}, to=${report.to})\n`);
|
|
369
549
|
}
|
|
370
|
-
|
|
550
|
+
return exitCode;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (opts.cmd === 'verify') {
|
|
554
|
+
const { exitCode, report, textOutput } = cmdVerify(opts);
|
|
555
|
+
if (opts.json) {
|
|
556
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
557
|
+
} else if (textOutput) {
|
|
558
|
+
process.stdout.write(textOutput + '\n');
|
|
559
|
+
} else {
|
|
560
|
+
process.stderr.write(report.error + '\n');
|
|
561
|
+
}
|
|
562
|
+
return exitCode;
|
|
371
563
|
}
|
|
372
564
|
|
|
373
565
|
const { exitCode, report } = opts.cmd === 'render' ? cmdRender(opts) : cmdGithubReleaseNotes(opts);
|
|
374
566
|
if (opts.json) {
|
|
375
567
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
568
|
+
} else if (opts.cmd === 'render' && opts.preview) {
|
|
569
|
+
// render --preview: emit the rendered section verbatim (no mutation occurred).
|
|
570
|
+
process.stdout.write(report.preview);
|
|
376
571
|
} else if (opts.cmd === 'github-release-notes' && report.body) {
|
|
377
572
|
process.stdout.write(report.body);
|
|
378
573
|
} else {
|
|
574
|
+
if (report.error) {
|
|
575
|
+
process.stderr.write(`${report.error}\n`);
|
|
576
|
+
}
|
|
379
577
|
process.stdout.write(`Consumed: ${report.consumed} fragment(s)\n`);
|
|
380
578
|
if (report.failures.length > 0) {
|
|
381
579
|
process.stdout.write(`Failures: ${report.failures.length}\n`);
|
|
@@ -384,9 +582,9 @@ function main() {
|
|
|
384
582
|
}
|
|
385
583
|
}
|
|
386
584
|
}
|
|
387
|
-
|
|
585
|
+
return exitCode;
|
|
388
586
|
}
|
|
389
587
|
|
|
390
|
-
if (require.main === module) main
|
|
588
|
+
if (require.main === module) runMain(main);
|
|
391
589
|
|
|
392
|
-
module.exports = { cmdRender, cmdExtract, cmdGithubReleaseNotes, parseArgs, splitChangelog, listFragmentFiles, usage };
|
|
590
|
+
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);
|