@williamthorsen/release-kit 5.3.2 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 6.0.0 — 2026-06-30
6
+
7
+ ### 🎉 Features
8
+
9
+ - 🚨 **Breaking:** Auto-activate integration test variant from config presence (#448)
10
+
11
+ A package can now separate its integration tests from its standalone suite simply by including a `vitest.integration.config.ts` (alongside a `vitest.standalone.config.ts`). The `--int-test` flag that previously enabled this is removed — that config-file pairing is now the only way to activate the separation. In such a package, `test` and `test:coverage` run only the standalone suite and skip integration tests, while a new `test:all` runs both suites together. The separation now holds even when tests run across every package at once, so a full-workspace `test` run still keeps integration tests out of the default suite. Packages that previously hand-copied these test scripts no longer need to.
12
+
13
+ ### ♻️ Refactoring
14
+
15
+ - Consolidate prepare parsing onto parseArgsOrExit (#441)
16
+
17
+ Makes `release-kit`'s `prepare` command report invalid arguments the same way its other commands already do. The error message and exit code a user sees for a bad argument are unchanged.
18
+
19
+ - Unify prepare help text into a testable module (#443)
20
+
21
+ Running `release-kit prepare --help` now lists the complete set of options. The `--force` flag and the restriction that `--set-version` and `--only` cannot be used when a project block is configured were previously missing from the help and are now shown there; the same `--set-version` restriction was added to the package README's `prepare` reference.
22
+
23
+ ### 📚 Documentation
24
+
25
+ - Document single-package --force rejection in release-kit prepare (#450)
26
+
27
+ The `release-kit prepare` `--force` documentation now correctly describes per-mode behavior: a bare `--force` is rejected in a single-package repo, where an explicit `--bump` level must be passed, while the "defaults to patch" shortcut applies only in monorepo and project mode.
28
+
29
+ ## 5.4.0 — 2026-06-27
30
+
31
+ ### 🎉 Features
32
+
33
+ - Centralize the per-package build as an nmr-compile bin (#419)
34
+
35
+ Introduces `nmr-compile`, a single command shipped with `@williamthorsen/nmr` that compiles each workspace package and now backs the default build. Consuming repos can delete their own per-package build script and pick up future build fixes just by upgrading nmr. Repeated builds with unchanged source now reliably skip recompiling instead of occasionally rebuilding for no reason, and import aliases now resolve correctly in symlinked checkouts.
36
+
5
37
  ## 5.3.2 — 2026-06-16
6
38
 
7
39
  ### ♻️ Refactoring
package/README.md CHANGED
@@ -4,7 +4,15 @@ Version-bumping and changelog-generation toolkit for release workflows.
4
4
 
5
5
  Provides a self-contained CLI that auto-discovers workspaces from `pnpm-workspace.yaml`, parses conventional commits, determines version bumps, updates `package.json` files, and generates changelogs from `git-cliff --context` output rendered in-process (with optional [editorial overrides](#editorial-overrides)).
6
6
 
7
- <!-- section:release-notes --><!-- /section:release-notes -->
7
+ <!-- section:release-notes -->
8
+ ## Release notes — v6.0.0 (2026-06-30)
9
+
10
+ ### 🎉 Features
11
+
12
+ - 🚨 **Breaking:** Auto-activate integration test variant from config presence (#448)
13
+
14
+ A package can now separate its integration tests from its standalone suite simply by including a `vitest.integration.config.ts` (alongside a `vitest.standalone.config.ts`). The `--int-test` flag that previously enabled this is removed — that config-file pairing is now the only way to activate the separation. In such a package, `test` and `test:coverage` run only the standalone suite and skip integration tests, while a new `test:all` runs both suites together. The separation now holds even when tests run across every package at once, so a full-workspace `test` run still keeps integration tests out of the default suite. Packages that previously hand-copied these test scripts no longer need to.
15
+ <!-- /section:release-notes -->
8
16
 
9
17
  ## Installation
10
18
 
@@ -549,15 +557,15 @@ The first release that ships under the new renderer will produce a one-time nois
549
557
 
550
558
  Run release preparation with automatic workspace discovery.
551
559
 
552
- | Flag | Description |
553
- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
554
- | `--dry-run` | Preview changes without writing files |
555
- | `--bump=major\|minor\|patch` | Override the bump type for all workspaces |
556
- | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode. |
557
- | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag (defaults to patch; combine with `--bump=X` for a different level) |
558
- | `--only=name1,name2` | Only process the named workspaces (monorepo only; rejected when a `project` block is configured) |
559
- | `--with-release-notes` | Write per-workspace release-notes previews under `{workspacePath}/docs/` |
560
- | `--help`, `-h` | Show help |
560
+ | Flag | Description |
561
+ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
562
+ | `--dry-run` | Preview changes without writing files |
563
+ | `--bump=major\|minor\|patch` | Override the bump type for all workspaces |
564
+ | `--set-version=X.Y.Z` | Set an explicit canonical semver version; bypasses commit-derived bumps. Requires `--only` in monorepo mode (rejected when a `project` block is configured). |
565
+ | `--force` | Release even when no commits or no bump-worthy commits exist since the last tag. In monorepo/project mode, defaults to patch (combine with `--bump=X` for a different level); in single-package mode, a bare `--force` is rejected — pass `--bump=major\|minor\|patch`. |
566
+ | `--only=name1,name2` | Only process the named workspaces (monorepo only; rejected when a `project` block is configured) |
567
+ | `--with-release-notes` | Write per-workspace release-notes previews under `{workspacePath}/docs/` |
568
+ | `--help`, `-h` | Show help |
561
569
 
562
570
  Workspace names for `--only` match the package directory name (e.g., `arrays`, `release-kit`).
563
571
 
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- a675735fcc8ee6abbe7201c05a9cec6517a6495d796f61322b141ed79e4e7bcf
1
+ 5ed41ffb0267b5678fc3b3273d6104a1c1c120fea20f92d48e173aee3ae9e2f5
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs, readPackageVersion, translateParseError } from "@williamthorsen/nmr-core";
2
+ import { parseArgsOrExit, readPackageVersion, reportError } from "@williamthorsen/nmr-core";
3
3
  import { checkWorkTypesDrift } from "../checkWorkTypesDrift.js";
4
4
  import { commitCommand } from "../commitCommand.js";
5
5
  import { createGithubReleaseCommand } from "../createGithubReleaseCommand.js";
6
+ import { showPrepareHelp } from "../help/prepareHelp.js";
6
7
  import { initCommand } from "../init/initCommand.js";
7
8
  import { prepareCommand } from "../prepareCommand.js";
8
9
  import { publishCommand } from "../publishCommand.js";
@@ -99,24 +100,6 @@ Options:
99
100
  --help, -h Show this help message
100
101
  `);
101
102
  }
102
- function showPrepareHelp() {
103
- console.info(`
104
- Usage: release-kit prepare [options]
105
-
106
- Run release preparation with automatic workspace discovery.
107
-
108
- Options:
109
- --dry-run Run without modifying any files
110
- --bump=major|minor|patch Override the bump type for all workspaces
111
- --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps. Requires --only in monorepo mode.
112
- --no-git-checks, -n Skip the clean-working-tree check
113
- --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only)
114
- --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
115
- (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
116
- Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
117
- --help, -h Show this help message
118
- `);
119
- }
120
103
  function showCommitHelp() {
121
104
  console.info(`
122
105
  Usage: release-kit commit [options]
@@ -295,7 +278,7 @@ if (command === "commit") {
295
278
  try {
296
279
  commitCommand(flags);
297
280
  } catch (error) {
298
- console.error(error instanceof Error ? error.message : String(error));
281
+ reportError(error instanceof Error ? error.message : String(error));
299
282
  process.exit(1);
300
283
  }
301
284
  process.exit(0);
@@ -338,7 +321,7 @@ if (command === "show-tag-prefixes") {
338
321
  process.exit(0);
339
322
  }
340
323
  if (flags.length > 0) {
341
- console.error(`Error: Unknown option: ${flags[0]}`);
324
+ reportError(`Unknown option: ${flags[0]}`);
342
325
  process.exit(1);
343
326
  }
344
327
  const exitCode = await showTagPrefixesCommand();
@@ -354,14 +337,7 @@ if (command === "init") {
354
337
  force: { long: "--force", type: "boolean" },
355
338
  withConfig: { long: "--with-config", type: "boolean" }
356
339
  };
357
- let parsed;
358
- try {
359
- parsed = parseArgs(flags, initFlagSchema);
360
- } catch (error) {
361
- console.error(`Error: ${translateParseError(error)}`);
362
- process.exit(1);
363
- }
364
- const { dryRun, force, withConfig } = parsed.flags;
340
+ const { dryRun, force, withConfig } = parseArgsOrExit(flags, initFlagSchema).flags;
365
341
  const exitCode = initCommand({ dryRun, force, withConfig });
366
342
  process.exit(exitCode);
367
343
  }
@@ -381,14 +357,7 @@ if (command === "sync-labels") {
381
357
  dryRun: { long: "--dry-run", type: "boolean" },
382
358
  force: { long: "--force", type: "boolean" }
383
359
  };
384
- let syncParsed;
385
- try {
386
- syncParsed = parseArgs(subflags, syncLabelsInitFlagSchema);
387
- } catch (error) {
388
- console.error(`Error: ${translateParseError(error)}`);
389
- process.exit(1);
390
- }
391
- const { dryRun, force } = syncParsed.flags;
360
+ const { dryRun, force } = parseArgsOrExit(subflags, syncLabelsInitFlagSchema).flags;
392
361
  const exitCode = await syncLabelsInitCommand({ dryRun, force });
393
362
  process.exit(exitCode);
394
363
  }
@@ -398,7 +367,7 @@ if (command === "sync-labels") {
398
367
  process.exit(0);
399
368
  }
400
369
  if (subflags.length > 0) {
401
- console.error(`Error: Unknown option: ${subflags[0]}`);
370
+ reportError(`Unknown option: ${subflags[0]}`);
402
371
  process.exit(1);
403
372
  }
404
373
  const exitCode = await generateCommand();
@@ -410,13 +379,13 @@ if (command === "sync-labels") {
410
379
  process.exit(0);
411
380
  }
412
381
  if (subflags.length > 0) {
413
- console.error(`Error: Unknown option: ${subflags[0]}`);
382
+ reportError(`Unknown option: ${subflags[0]}`);
414
383
  process.exit(1);
415
384
  }
416
385
  const exitCode = syncLabelsCommand();
417
386
  process.exit(exitCode);
418
387
  }
419
- console.error(`Error: Unknown subcommand: ${subcommand}`);
388
+ reportError(`Unknown subcommand: ${subcommand}`);
420
389
  showSyncLabelsHelp();
421
390
  process.exit(1);
422
391
  }
@@ -433,18 +402,19 @@ if (command === "overrides") {
433
402
  process.exit(0);
434
403
  }
435
404
  if (subflags.length > 0) {
436
- console.error(`Error: Unknown option: ${subflags[0]}`);
405
+ reportError(`Unknown option: ${subflags[0]}`);
437
406
  process.exit(1);
438
407
  }
439
408
  const result = await validateOverridesCommand();
440
409
  if (result.exitCode === 0) {
441
410
  console.info(result.message);
442
411
  } else {
443
- console.error(result.message);
412
+ process.stderr.write(`${result.message}
413
+ `);
444
414
  }
445
415
  process.exit(result.exitCode);
446
416
  }
447
- console.error(`Error: Unknown subcommand: ${subcommand}`);
417
+ reportError(`Unknown subcommand: ${subcommand}`);
448
418
  showOverridesHelp();
449
419
  process.exit(1);
450
420
  }
@@ -461,14 +431,15 @@ if (command === "work-types") {
461
431
  process.exit(0);
462
432
  }
463
433
  if (subflags.length > 0) {
464
- console.error(`Error: Unknown option: ${subflags[0]}`);
434
+ reportError(`Unknown option: ${subflags[0]}`);
465
435
  process.exit(1);
466
436
  }
467
437
  const result = await checkWorkTypesDrift();
468
438
  if (result.exitCode === 0) {
469
439
  console.info(result.message);
470
440
  } else {
471
- console.error(result.message);
441
+ process.stderr.write(`${result.message}
442
+ `);
472
443
  }
473
444
  process.exit(result.exitCode);
474
445
  }
@@ -478,21 +449,22 @@ if (command === "work-types") {
478
449
  process.exit(0);
479
450
  }
480
451
  if (subflags.length > 0) {
481
- console.error(`Error: Unknown option: ${subflags[0]}`);
452
+ reportError(`Unknown option: ${subflags[0]}`);
482
453
  process.exit(1);
483
454
  }
484
455
  const result = await syncWorkTypes();
485
456
  if (result.exitCode === 0) {
486
457
  console.info(result.message);
487
458
  } else {
488
- console.error(result.message);
459
+ process.stderr.write(`${result.message}
460
+ `);
489
461
  }
490
462
  process.exit(result.exitCode);
491
463
  }
492
- console.error(`Error: Unknown subcommand: ${subcommand}`);
464
+ reportError(`Unknown subcommand: ${subcommand}`);
493
465
  showWorkTypesHelp();
494
466
  process.exit(1);
495
467
  }
496
- console.error(`Error: Unknown command: ${command}`);
468
+ reportError(`Unknown command: ${command}`);
497
469
  showUsage();
498
470
  process.exit(1);
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { formatErrorLine } from "@williamthorsen/nmr-core";
4
5
  import { buildFetchInit, errorMessage, hasExpectedTopLevelShape } from "./workTypesUtils.js";
5
6
  const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json";
6
7
  function resolveDefaultLocalPath() {
@@ -28,7 +29,7 @@ async function checkWorkTypesDrift(dependencies = {}) {
28
29
  } catch (error) {
29
30
  return {
30
31
  exitCode: 2,
31
- message: `Network error fetching upstream work-types.json: ${errorMessage(error)}`
32
+ message: formatErrorLine(`Failed to fetch upstream work-types.json: ${errorMessage(error)}`)
32
33
  };
33
34
  }
34
35
  if (response.status === 404) {
@@ -40,7 +41,9 @@ async function checkWorkTypesDrift(dependencies = {}) {
40
41
  if (!response.ok) {
41
42
  return {
42
43
  exitCode: 2,
43
- message: `Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`
44
+ message: formatErrorLine(
45
+ `Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`
46
+ )
44
47
  };
45
48
  }
46
49
  const upstreamText = await response.text();
@@ -1,19 +1,12 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
- import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
3
+ import { parseArgsOrExit } from "@williamthorsen/nmr-core";
4
4
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./prepareCommand.js";
5
5
  const commitFlagSchema = {
6
6
  dryRun: { long: "--dry-run", type: "boolean" }
7
7
  };
8
8
  function commitCommand(argv) {
9
- let parsed;
10
- try {
11
- parsed = parseArgs(argv, commitFlagSchema);
12
- } catch (error) {
13
- console.error(`Error: ${translateParseError(error)}`);
14
- process.exit(1);
15
- }
16
- const dryRun = parsed.flags.dryRun;
9
+ const dryRun = parseArgsOrExit(argv, commitFlagSchema).flags.dryRun;
17
10
  let tagsContent;
18
11
  try {
19
12
  tagsContent = readFileSync(RELEASE_TAGS_FILE, "utf8");
@@ -1,4 +1,4 @@
1
- import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
1
+ import { parseArgsOrExit, reportError } from "@williamthorsen/nmr-core";
2
2
  import { createGithubReleases } from "./createGithubRelease.js";
3
3
  import { parseRequestedTags } from "./parseRequestedTags.js";
4
4
  import { resolveCommandTags } from "./resolveCommandTags.js";
@@ -8,13 +8,7 @@ const createGithubReleaseFlagSchema = {
8
8
  tags: { long: "--tags", type: "string" }
9
9
  };
10
10
  async function createGithubReleaseCommand(argv) {
11
- let parsed;
12
- try {
13
- parsed = parseArgs(argv, createGithubReleaseFlagSchema);
14
- } catch (error) {
15
- console.error(`Error: ${translateParseError(error)}`);
16
- process.exit(1);
17
- }
11
+ const parsed = parseArgsOrExit(argv, createGithubReleaseFlagSchema);
18
12
  const { dryRun } = parsed.flags;
19
13
  const requestedTags = parseRequestedTags(parsed.flags.tags);
20
14
  const resolvedTags = await resolveCommandTags(requestedTags);
@@ -23,7 +17,7 @@ async function createGithubReleaseCommand(argv) {
23
17
  try {
24
18
  outcome = createGithubReleases(resolvedTags, changelogJsonOutputPath, dryRun, sectionOrder);
25
19
  } catch (error) {
26
- console.error(`Error creating GitHub Releases: ${error instanceof Error ? error.message : String(error)}`);
20
+ reportError(`Failed to create GitHub Releases: ${error instanceof Error ? error.message : String(error)}`);
27
21
  process.exit(1);
28
22
  }
29
23
  if (outcome.skipped.length > 0) {
@@ -0,0 +1,2 @@
1
+ export declare const prepareHelpText = "\nUsage: release-kit prepare [options]\n\nRun release preparation with automatic workspace discovery.\n\nOptions:\n --dry-run Run without modifying any files\n --bump=major|minor|patch Override the bump type for all workspaces\n --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.\n Requires --only in monorepo mode (rejected when a 'project' block is configured).\n --force Release even when no commits or no bump-worthy commits exist\n since the last tag. In monorepo and project mode, defaults to\n patch when --bump is not given; use --bump=X for a different\n level. In single-package mode, a bare --force is rejected \u2014\n pass --bump=major|minor|patch.\n --no-git-checks, -n Skip the clean-working-tree check\n --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;\n rejected when a 'project' block is configured)\n --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/\n (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).\n Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).\n --help, -h Show this help message\n";
2
+ export declare function showPrepareHelp(): void;
@@ -0,0 +1,30 @@
1
+ const prepareHelpText = `
2
+ Usage: release-kit prepare [options]
3
+
4
+ Run release preparation with automatic workspace discovery.
5
+
6
+ Options:
7
+ --dry-run Run without modifying any files
8
+ --bump=major|minor|patch Override the bump type for all workspaces
9
+ --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.
10
+ Requires --only in monorepo mode (rejected when a 'project' block is configured).
11
+ --force Release even when no commits or no bump-worthy commits exist
12
+ since the last tag. In monorepo and project mode, defaults to
13
+ patch when --bump is not given; use --bump=X for a different
14
+ level. In single-package mode, a bare --force is rejected \u2014
15
+ pass --bump=major|minor|patch.
16
+ --no-git-checks, -n Skip the clean-working-tree check
17
+ --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;
18
+ rejected when a 'project' block is configured)
19
+ --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
20
+ (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
21
+ Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
22
+ --help, -h Show this help message
23
+ `;
24
+ function showPrepareHelp() {
25
+ console.info(prepareHelpText);
26
+ }
27
+ export {
28
+ prepareHelpText,
29
+ showPrepareHelp
30
+ };
@@ -2,6 +2,37 @@ import type { WriteResult } from '@williamthorsen/nmr-core';
2
2
  import type { ReleaseType } from './types.ts';
3
3
  export declare const RELEASE_TAGS_FILE = "tmp/.release-tags";
4
4
  export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
5
+ export declare const prepareFlagSchema: {
6
+ dryRun: {
7
+ long: string;
8
+ type: "boolean";
9
+ };
10
+ force: {
11
+ long: string;
12
+ type: "boolean";
13
+ };
14
+ noGitChecks: {
15
+ long: string;
16
+ type: "boolean";
17
+ short: string;
18
+ };
19
+ bump: {
20
+ long: string;
21
+ type: "string";
22
+ };
23
+ setVersion: {
24
+ long: string;
25
+ type: "string";
26
+ };
27
+ only: {
28
+ long: string;
29
+ type: "string";
30
+ };
31
+ withReleaseNotes: {
32
+ long: string;
33
+ type: "boolean";
34
+ };
35
+ };
5
36
  export declare function parseArgs(argv: string[]): {
6
37
  dryRun: boolean;
7
38
  force: boolean;
@@ -1,4 +1,4 @@
1
- import { parseArgs as coreParseArgs, translateParseError, writeFileWithCheck } from "@williamthorsen/nmr-core";
1
+ import { parseArgsOrExit, reportError, writeFileWithCheck } from "@williamthorsen/nmr-core";
2
2
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
3
3
  import { buildDependencyGraph } from "./buildDependencyGraph.js";
4
4
  import { buildReleaseSummary } from "./buildReleaseSummary.js";
@@ -18,27 +18,6 @@ const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
18
18
  function isReleaseType(value) {
19
19
  return VALID_BUMP_TYPES.includes(value);
20
20
  }
21
- function showHelp() {
22
- console.info(`
23
- Usage: npx @williamthorsen/release-kit prepare [options]
24
-
25
- Options:
26
- --dry-run Run without modifying any files
27
- --bump=major|minor|patch Override the bump type for all workspaces
28
- --set-version=X.Y.Z Set an explicit version; bypasses commit-derived bumps.
29
- Requires --only in monorepo mode (rejected when a 'project' block is configured).
30
- --force Release even when no commits or no bump-worthy commits exist
31
- since the last tag. Defaults to patch when --bump is not given;
32
- use --bump=X to release at a different level.
33
- --no-git-checks, -n Skip the clean-working-tree check
34
- --only=name1,name2 Only process the named workspaces (comma-separated, monorepo only;
35
- rejected when a 'project' block is configured)
36
- --with-release-notes Also write per-workspace release-notes previews under {workspacePath}/docs/
37
- (docs/README.v{version}.md and docs/RELEASE_NOTES.v{version}.md).
38
- Recommended .gitignore entry: packages/*/docs/*.v*.md (or docs/*.v*.md).
39
- --help Show this help message
40
- `);
41
- }
42
21
  const prepareFlagSchema = {
43
22
  dryRun: { long: "--dry-run", type: "boolean" },
44
23
  force: { long: "--force", type: "boolean" },
@@ -50,34 +29,25 @@ const prepareFlagSchema = {
50
29
  bump: { long: "--bump", type: "string" },
51
30
  setVersion: { long: "--set-version", type: "string" },
52
31
  only: { long: "--only", type: "string" },
53
- withReleaseNotes: { long: "--with-release-notes", type: "boolean" },
54
- help: { long: "--help", type: "boolean", short: "-h" }
32
+ withReleaseNotes: { long: "--with-release-notes", type: "boolean" }
55
33
  };
56
34
  function parseArgs(argv) {
57
- let parsed;
58
- try {
59
- parsed = coreParseArgs(argv, prepareFlagSchema);
60
- } catch (error) {
61
- throw new Error(translateParseError(error));
62
- }
63
- const { flags } = parsed;
64
- if (flags.help) {
65
- showHelp();
66
- process.exit(0);
67
- }
35
+ const { flags } = parseArgsOrExit(argv, prepareFlagSchema);
68
36
  let bumpOverride;
69
37
  if (flags.bump !== void 0) {
70
38
  if (!isReleaseType(flags.bump)) {
71
- throw new Error(`Invalid bump type "${flags.bump}". Must be one of: ${VALID_BUMP_TYPES.join(", ")}`);
39
+ reportError(`Invalid bump type "${flags.bump}". Must be one of: ${VALID_BUMP_TYPES.join(", ")}`);
40
+ process.exit(1);
72
41
  }
73
42
  bumpOverride = flags.bump;
74
43
  }
75
44
  let setVersion;
76
45
  if (flags.setVersion !== void 0) {
77
46
  if (!CANONICAL_SEMVER_PATTERN.test(flags.setVersion)) {
78
- throw new Error(
47
+ reportError(
79
48
  `Invalid --set-version value "${flags.setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`
80
49
  );
50
+ process.exit(1);
81
51
  }
82
52
  setVersion = flags.setVersion;
83
53
  }
@@ -86,10 +56,12 @@ function parseArgs(argv) {
86
56
  only = flags.only.split(",");
87
57
  }
88
58
  if (setVersion !== void 0 && bumpOverride !== void 0) {
89
- throw new Error("--set-version cannot be combined with --bump");
59
+ reportError("--set-version cannot be combined with --bump");
60
+ process.exit(1);
90
61
  }
91
62
  if (setVersion !== void 0 && flags.force) {
92
- throw new Error("--set-version cannot be combined with --force");
63
+ reportError("--set-version cannot be combined with --force");
64
+ process.exit(1);
93
65
  }
94
66
  return {
95
67
  dryRun: flags.dryRun,
@@ -111,19 +83,7 @@ function writeReleaseTags(tags, dryRun) {
111
83
  });
112
84
  }
113
85
  async function prepareCommand(argv) {
114
- let dryRun;
115
- let force;
116
- let noGitChecks;
117
- let bumpOverride;
118
- let only;
119
- let setVersion;
120
- let withReleaseNotes;
121
- try {
122
- ({ dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv));
123
- } catch (error) {
124
- console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
125
- process.exit(1);
126
- }
86
+ const { dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv);
127
87
  const options = {
128
88
  dryRun,
129
89
  force,
@@ -138,7 +98,7 @@ async function prepareCommand(argv) {
138
98
  try {
139
99
  assertCleanWorkingTree();
140
100
  } catch (error) {
141
- console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
101
+ reportError(error instanceof Error ? error.message : String(error));
142
102
  process.exit(1);
143
103
  }
144
104
  }
@@ -147,7 +107,7 @@ async function prepareCommand(argv) {
147
107
  try {
148
108
  discoveredPaths = await discoverWorkspaces();
149
109
  } catch (error) {
150
- console.error(`Error discovering workspaces: ${error instanceof Error ? error.message : String(error)}`);
110
+ reportError(`Failed to discover workspaces: ${error instanceof Error ? error.message : String(error)}`);
151
111
  process.exit(1);
152
112
  }
153
113
  if (discoveredPaths === void 0) {
@@ -158,12 +118,12 @@ async function prepareCommand(argv) {
158
118
  }
159
119
  function runSinglePackageMode(userConfig, options, only, dryRun) {
160
120
  if (only !== void 0) {
161
- console.error("Error: --only is only supported for monorepo configurations");
121
+ reportError("--only is only supported for monorepo configurations");
162
122
  process.exit(1);
163
123
  }
164
124
  if (options.force && options.bumpOverride === void 0) {
165
- console.error(
166
- "Error: --force without --bump is only supported for monorepo configurations. Use --bump=major|minor|patch to set the level for a single-package release."
125
+ reportError(
126
+ "--force without --bump is only supported for monorepo configurations. Use --bump=major|minor|patch to set the level for a single-package release."
167
127
  );
168
128
  process.exit(1);
169
129
  }
@@ -176,18 +136,18 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
176
136
  const rootPackage = readRootPackageVersion();
177
137
  config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
178
138
  } catch (error) {
179
- console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
139
+ reportError(`Failed to resolve workspaces: ${error instanceof Error ? error.message : String(error)}`);
180
140
  process.exit(1);
181
141
  }
182
142
  if (setVersion !== void 0 && config.project !== void 0) {
183
- console.error(
184
- "Error: --set-version cannot be combined with a project release. --set-version operates on a single workspace; a project release rolls up every contributing workspace. To use --set-version, run on a config without a `project` block."
143
+ reportError(
144
+ "--set-version cannot be combined with a project release. --set-version operates on a single workspace; a project release rolls up every contributing workspace. To use --set-version, run on a config without a `project` block."
185
145
  );
186
146
  process.exit(1);
187
147
  }
188
148
  if (only !== void 0 && config.project !== void 0) {
189
- console.error(
190
- "Error: --only cannot be combined with a project release. To release a single workspace, use a config without a `project` block, or run a full `prepare` (no --only) to include the project release."
149
+ reportError(
150
+ "--only cannot be combined with a project release. To release a single workspace, use a config without a `project` block, or run a full `prepare` (no --only) to include the project release."
191
151
  );
192
152
  process.exit(1);
193
153
  }
@@ -195,7 +155,7 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
195
155
  const knownNames = config.workspaces.map((w) => w.dir);
196
156
  for (const name of only) {
197
157
  if (!knownNames.includes(name)) {
198
- console.error(`Error: Unknown workspace "${name}". Known workspaces: ${knownNames.join(", ")}`);
158
+ reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(", ")}`);
199
159
  process.exit(1);
200
160
  }
201
161
  }
@@ -209,26 +169,27 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
209
169
  return { has: result.commits.length > 0, tag: result.tag };
210
170
  });
211
171
  if (violations !== void 0) {
212
- console.error("Error: --only excludes packages with changes that would be stranded by the release.");
213
- console.error("The following packages must be added to --only or have their dependencies removed:");
172
+ reportError("--only excludes packages with changes that would be stranded by the release.");
173
+ process.stderr.write("The following packages must be added to --only or have their dependencies removed:\n");
214
174
  for (const violation of violations) {
215
175
  const since = violation.tag ?? "the beginning";
216
- console.error(` - ${violation.dir} (downstream of ${violation.downstreamOf}; has commits since ${since})`);
176
+ process.stderr.write(
177
+ ` - ${violation.dir} (downstream of ${violation.downstreamOf}; has commits since ${since})
178
+ `
179
+ );
217
180
  }
218
- console.error("Alternatively, run `release-kit prepare` without --only to release everything.");
181
+ process.stderr.write("Alternatively, run `release-kit prepare` without --only to release everything.\n");
219
182
  process.exit(1);
220
183
  }
221
184
  config.workspaces = config.workspaces.filter((w) => only.includes(w.dir));
222
185
  }
223
186
  if (setVersion !== void 0) {
224
187
  if (only === void 0) {
225
- console.error("Error: --set-version requires --only in monorepo mode");
188
+ reportError("--set-version requires --only in monorepo mode");
226
189
  process.exit(1);
227
190
  }
228
191
  if (config.workspaces.length !== 1) {
229
- console.error(
230
- `Error: --set-version requires --only to match exactly one workspace; matched ${config.workspaces.length}`
231
- );
192
+ reportError(`--set-version requires --only to match exactly one workspace; matched ${config.workspaces.length}`);
232
193
  process.exit(1);
233
194
  }
234
195
  }
@@ -239,7 +200,7 @@ async function loadAndValidateConfig() {
239
200
  try {
240
201
  rawConfig = await loadConfig();
241
202
  } catch (error) {
242
- console.error(`Error loading config: ${error instanceof Error ? error.message : String(error)}`);
203
+ reportError(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
243
204
  process.exit(1);
244
205
  }
245
206
  if (rawConfig === void 0) {
@@ -247,9 +208,10 @@ async function loadAndValidateConfig() {
247
208
  }
248
209
  const { config, errors, warnings } = validateConfig(rawConfig);
249
210
  if (errors.length > 0) {
250
- console.error("Invalid config:");
211
+ process.stderr.write("Invalid config:\n");
251
212
  for (const err of errors) {
252
- console.error(` \u274C ${err}`);
213
+ process.stderr.write(` \u274C ${err}
214
+ `);
253
215
  }
254
216
  process.exit(1);
255
217
  }
@@ -263,13 +225,13 @@ function runAndReport(execute, dryRun) {
263
225
  try {
264
226
  result = execute();
265
227
  } catch (error) {
266
- console.error(error instanceof Error ? error.message : String(error));
228
+ reportError(error instanceof Error ? error.message : String(error));
267
229
  process.exit(1);
268
230
  }
269
231
  process.stdout.write(reportPrepare(result) + "\n");
270
232
  const writeResult = writeReleaseTags(result.tags, dryRun);
271
233
  if (writeResult?.outcome === "failed") {
272
- console.error(`Error writing release tags: ${writeResult.error ?? "unknown error"}`);
234
+ reportError(`Failed to write release tags: ${writeResult.error ?? "unknown error"}`);
273
235
  process.exit(1);
274
236
  }
275
237
  if (writeResult) {
@@ -288,7 +250,7 @@ function runAndReport(execute, dryRun) {
288
250
  overwrite: true
289
251
  });
290
252
  if (summaryResult.outcome === "failed") {
291
- console.error(`Error writing release summary: ${summaryResult.error ?? "unknown error"}`);
253
+ reportError(`Failed to write release summary: ${summaryResult.error ?? "unknown error"}`);
292
254
  process.exit(1);
293
255
  }
294
256
  if (dryRun) {
@@ -298,8 +260,9 @@ function runAndReport(execute, dryRun) {
298
260
  }
299
261
  }
300
262
  if (writeResult && !dryRun) {
301
- console.error(`
302
- Run 'release-kit commit' to create the release commit.`);
263
+ process.stderr.write(`
264
+ Run 'release-kit commit' to create the release commit.
265
+ `);
303
266
  }
304
267
  }
305
268
  export {
@@ -307,5 +270,6 @@ export {
307
270
  RELEASE_TAGS_FILE,
308
271
  parseArgs,
309
272
  prepareCommand,
273
+ prepareFlagSchema,
310
274
  writeReleaseTags
311
275
  };
@@ -1,6 +1,6 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
3
+ import { parseArgsOrExit, reportError } from "@williamthorsen/nmr-core";
4
4
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
5
5
  import { detectPackageManager } from "./detectPackageManager.js";
6
6
  import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
@@ -15,19 +15,13 @@ const publishFlagSchema = {
15
15
  tags: { long: "--tags", type: "string" }
16
16
  };
17
17
  async function publishCommand(argv) {
18
- let parsed;
19
- try {
20
- parsed = parseArgs(argv, publishFlagSchema);
21
- } catch (error) {
22
- console.error(`Error: ${translateParseError(error)}`);
23
- process.exit(1);
24
- }
18
+ const parsed = parseArgsOrExit(argv, publishFlagSchema);
25
19
  const { dryRun, noGitChecks, provenance } = parsed.flags;
26
20
  if (!dryRun && !noGitChecks) {
27
21
  try {
28
22
  assertCleanWorkingTree();
29
23
  } catch (error) {
30
- console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
24
+ reportError(error instanceof Error ? error.message : String(error));
31
25
  process.exit(1);
32
26
  }
33
27
  }
@@ -80,7 +74,7 @@ async function publishCommand(argv) {
80
74
  console.warn(` ${t}`);
81
75
  }
82
76
  }
83
- console.error(error instanceof Error ? error.message : String(error));
77
+ reportError(error instanceof Error ? error.message : String(error));
84
78
  process.exit(1);
85
79
  }
86
80
  }
@@ -92,7 +86,7 @@ function filterPublishableTags(resolvedTags, isExplicit) {
92
86
  }
93
87
  if (isExplicit && unpublishable.length > 0) {
94
88
  for (const { tag, workspacePath } of unpublishable) {
95
- console.error(`Error: ${tag} (${workspacePath}) cannot be published: package.json#private is true.`);
89
+ reportError(`${tag} (${workspacePath}) cannot be published: package.json#private is true.`);
96
90
  }
97
91
  process.exit(1);
98
92
  }
@@ -1,4 +1,4 @@
1
- import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
1
+ import { parseArgsOrExit, reportError } from "@williamthorsen/nmr-core";
2
2
  import { parseRequestedTags } from "./parseRequestedTags.js";
3
3
  import { pushRelease } from "./pushRelease.js";
4
4
  import { resolveCommandTags } from "./resolveCommandTags.js";
@@ -8,13 +8,7 @@ const pushFlagSchema = {
8
8
  tagsOnly: { long: "--tags-only", type: "boolean" }
9
9
  };
10
10
  async function pushCommand(argv) {
11
- let parsed;
12
- try {
13
- parsed = parseArgs(argv, pushFlagSchema);
14
- } catch (error) {
15
- console.error(`Error: ${translateParseError(error)}`);
16
- process.exit(1);
17
- }
11
+ const parsed = parseArgsOrExit(argv, pushFlagSchema);
18
12
  const { dryRun, tagsOnly } = parsed.flags;
19
13
  const requestedTags = parseRequestedTags(parsed.flags.tags);
20
14
  const resolvedTags = await resolveCommandTags(requestedTags);
@@ -38,7 +32,7 @@ async function pushCommand(argv) {
38
32
  }
39
33
  }
40
34
  } catch (error) {
41
- console.error(error instanceof Error ? error.message : String(error));
35
+ reportError(error instanceof Error ? error.message : String(error));
42
36
  process.exit(1);
43
37
  }
44
38
  }
@@ -487,8 +487,9 @@ function maybeEmitBaselineHint(workspace, knownPrefixes, state) {
487
487
  if (candidates.length === 0) return;
488
488
  const totalTags = candidates.reduce((sum, candidate) => sum + candidate.tagCount, 0);
489
489
  const example = candidates[0]?.exampleTags[0] ?? `${candidates[0]?.prefix ?? ""}?`;
490
- console.error(
491
- `Hint: no baseline tag found for ${workspace.dir} under '${workspace.tagPrefix}', but ${totalTags} candidate-shaped tags exist (e.g., ${example}). Run 'release-kit show-tag-prefixes' to check for undeclared legacy prefixes.`
490
+ process.stderr.write(
491
+ `Hint: no baseline tag found for ${workspace.dir} under '${workspace.tagPrefix}', but ${totalTags} candidate-shaped tags exist (e.g., ${example}). Run 'release-kit show-tag-prefixes' to check for undeclared legacy prefixes.
492
+ `
492
493
  );
493
494
  state.emitted = true;
494
495
  }
@@ -1,3 +1,4 @@
1
+ import { reportError } from "@williamthorsen/nmr-core";
1
2
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
2
3
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
3
4
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
@@ -6,7 +7,7 @@ async function resolveCommandTags(tags) {
6
7
  try {
7
8
  discoveredPaths = await discoverWorkspaces();
8
9
  } catch (error) {
9
- console.error(`Error discovering workspaces: ${error instanceof Error ? error.message : String(error)}`);
10
+ reportError(`Failed to discover workspaces: ${error instanceof Error ? error.message : String(error)}`);
10
11
  process.exit(1);
11
12
  }
12
13
  let workspaces;
@@ -18,7 +19,7 @@ async function resolveCommandTags(tags) {
18
19
  workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
19
20
  }
20
21
  } catch (error) {
21
- console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
22
+ reportError(`Failed to resolve workspaces: ${error instanceof Error ? error.message : String(error)}`);
22
23
  process.exit(1);
23
24
  }
24
25
  let resolvedTags;
@@ -30,14 +31,14 @@ async function resolveCommandTags(tags) {
30
31
  throw new Error("resolveCommandTags: invariant violated \u2014 neither workspaces nor singleWorkspace was derived");
31
32
  }
32
33
  if (resolvedTags.length === 0) {
33
- console.error("Error: No release tags found on HEAD. Create tags with `release-kit tag` first.");
34
+ reportError("No release tags found on HEAD. Create tags with `release-kit tag` first.");
34
35
  process.exit(1);
35
36
  }
36
37
  if (tags !== void 0) {
37
38
  const availableTagNames = resolvedTags.map((t) => t.tag);
38
39
  for (const name of tags) {
39
40
  if (!availableTagNames.includes(name)) {
40
- console.error(`Error: Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(", ")}`);
41
+ reportError(`Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(", ")}`);
41
42
  process.exit(1);
42
43
  }
43
44
  }
@@ -1,3 +1,4 @@
1
+ import { reportError } from "@williamthorsen/nmr-core";
1
2
  import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
2
3
  import { loadConfig, resolveWorkTypes } from "./loadConfig.js";
3
4
  import { validateConfig } from "./validateConfig.js";
@@ -9,7 +10,7 @@ async function resolveReleaseNotesConfig(options = {}) {
9
10
  } catch (error) {
10
11
  const message = error instanceof Error ? error.message : String(error);
11
12
  if (strictLoad) {
12
- console.error(`Error: failed to load config: ${message}`);
13
+ reportError(`Failed to load config: ${message}`);
13
14
  process.exit(1);
14
15
  }
15
16
  console.warn(`Warning: failed to load config; using defaults: ${message}`);
@@ -23,9 +24,10 @@ async function resolveReleaseNotesConfig(options = {}) {
23
24
  }
24
25
  const { config, errors, warnings } = validateConfig(rawConfig);
25
26
  if (errors.length > 0) {
26
- console.error("Invalid config:");
27
+ process.stderr.write("Invalid config:\n");
27
28
  for (const err of errors) {
28
- console.error(` \u274C ${err}`);
29
+ process.stderr.write(` \u274C ${err}
30
+ `);
29
31
  }
30
32
  process.exit(1);
31
33
  }
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+ import { reportError } from "@williamthorsen/nmr-core";
3
4
  import { stringify } from "yaml";
4
5
  import { loadSyncLabelsConfig, SYNC_LABELS_CONFIG_PATH } from "./loadSyncLabelsConfig.js";
5
6
  import { hashPresetFile } from "./presets.js";
@@ -19,11 +20,11 @@ async function generateCommand() {
19
20
  config = await loadSyncLabelsConfig();
20
21
  } catch (error) {
21
22
  const message = error instanceof Error ? error.message : String(error);
22
- console.error(`Error loading config: ${message}`);
23
+ reportError(`Failed to load config: ${message}`);
23
24
  return 1;
24
25
  }
25
26
  if (config === void 0) {
26
- console.error(`No config file found at ${SYNC_LABELS_CONFIG_PATH}. Run \`release-kit sync-labels init\` first.`);
27
+ reportError(`No config file found at ${SYNC_LABELS_CONFIG_PATH}. Run \`release-kit sync-labels init\` first.`);
27
28
  return 1;
28
29
  }
29
30
  let labels;
@@ -31,7 +32,7 @@ async function generateCommand() {
31
32
  labels = resolveLabels(config);
32
33
  } catch (error) {
33
34
  const message = error instanceof Error ? error.message : String(error);
34
- console.error(`Error resolving labels: ${message}`);
35
+ reportError(`Failed to resolve labels: ${message}`);
35
36
  return 1;
36
37
  }
37
38
  const presetHashes = /* @__PURE__ */ new Map();
@@ -44,7 +45,7 @@ async function generateCommand() {
44
45
  writeFileSync(LABELS_OUTPUT_PATH, content, "utf8");
45
46
  } catch (error) {
46
47
  const message = error instanceof Error ? error.message : String(error);
47
- console.error(`Error writing ${LABELS_OUTPUT_PATH}: ${message}`);
48
+ reportError(`Failed to write ${LABELS_OUTPUT_PATH}: ${message}`);
48
49
  return 1;
49
50
  }
50
51
  console.info(`Generated ${LABELS_OUTPUT_PATH} with ${String(labels.length)} labels.`);
@@ -14,7 +14,8 @@ async function syncLabelsInitCommand({ dryRun, force }) {
14
14
  workspacePaths = await discoverWorkspaces();
15
15
  } catch (error) {
16
16
  const message = error instanceof Error ? error.message : String(error);
17
- console.error(` Failed to discover workspaces: ${message}`);
17
+ process.stderr.write(` Failed to discover workspaces: ${message}
18
+ `);
18
19
  return 1;
19
20
  }
20
21
  const scopeLabels = workspacePaths === void 0 ? [] : buildScopeLabels(workspacePaths);
@@ -32,7 +33,7 @@ async function syncLabelsInitCommand({ dryRun, force }) {
32
33
  reportWriteResult(workflowResult, dryRun);
33
34
  reportWriteResult(configResult, dryRun);
34
35
  if (workflowResult.outcome === "failed" || configResult.outcome === "failed") {
35
- console.error("Failed to scaffold one or more files.");
36
+ process.stderr.write("Failed to scaffold one or more files.\n");
36
37
  return 1;
37
38
  }
38
39
  if (dryRun) {
@@ -1,5 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ import { reportError } from "@williamthorsen/nmr-core";
3
4
  const WORKFLOW_FILE = ".github/workflows/sync-labels.yaml";
4
5
  function checkGhAvailable() {
5
6
  try {
@@ -11,18 +12,18 @@ function checkGhAvailable() {
11
12
  }
12
13
  function syncLabelsCommand() {
13
14
  if (!checkGhAvailable()) {
14
- console.error("Error: The `gh` CLI is not installed or not in PATH. Install it from https://cli.github.com/");
15
+ reportError("The `gh` CLI is not installed or not in PATH. Install it from https://cli.github.com/");
15
16
  return 1;
16
17
  }
17
18
  if (!existsSync(WORKFLOW_FILE)) {
18
- console.error(`Error: ${WORKFLOW_FILE} not found. Run \`release-kit sync-labels init\` first.`);
19
+ reportError(`${WORKFLOW_FILE} not found. Run \`release-kit sync-labels init\` first.`);
19
20
  return 1;
20
21
  }
21
22
  try {
22
23
  execSync("gh workflow run sync-labels.yaml", { stdio: "inherit" });
23
24
  } catch (error) {
24
25
  const message = error instanceof Error ? error.message : String(error);
25
- console.error(`Error triggering workflow: ${message}`);
26
+ reportError(`Failed to trigger workflow: ${message}`);
26
27
  return 1;
27
28
  }
28
29
  console.info("Workflow triggered successfully. View runs at: gh run list --workflow=sync-labels.yaml");
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { formatErrorLine } from "@williamthorsen/nmr-core";
4
5
  import { UPSTREAM_WORK_TYPES_URL } from "./checkWorkTypesDrift.js";
5
6
  import { isRecord } from "./typeGuards.js";
6
7
  import { buildFetchInit, errorMessage, hasExpectedTopLevelShape } from "./workTypesUtils.js";
@@ -32,13 +33,15 @@ async function syncWorkTypes(dependencies = {}) {
32
33
  } catch (error) {
33
34
  return {
34
35
  exitCode: 2,
35
- message: `Network error fetching upstream work-types.json: ${errorMessage(error)}`
36
+ message: formatErrorLine(`Failed to fetch upstream work-types.json: ${errorMessage(error)}`)
36
37
  };
37
38
  }
38
39
  if (!response.ok) {
39
40
  return {
40
41
  exitCode: 2,
41
- message: `Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`
42
+ message: formatErrorLine(
43
+ `Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`
44
+ )
42
45
  };
43
46
  }
44
47
  const upstreamText = await response.text();
@@ -78,7 +81,7 @@ async function syncWorkTypes(dependencies = {}) {
78
81
  } catch (error) {
79
82
  return {
80
83
  exitCode: 2,
81
- message: `Failed to write ${localPath}: ${errorMessage(error)}`
84
+ message: formatErrorLine(`Failed to write ${localPath}: ${errorMessage(error)}`)
82
85
  };
83
86
  }
84
87
  return {
@@ -1,22 +1,15 @@
1
- import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
1
+ import { parseArgsOrExit, reportError } from "@williamthorsen/nmr-core";
2
2
  import { createTags } from "./createTags.js";
3
3
  const tagFlagSchema = {
4
4
  dryRun: { long: "--dry-run", type: "boolean" },
5
5
  noGitChecks: { long: "--no-git-checks", type: "boolean" }
6
6
  };
7
7
  function tagCommand(argv) {
8
- let parsed;
9
- try {
10
- parsed = parseArgs(argv, tagFlagSchema);
11
- } catch (error) {
12
- console.error(`Error: ${translateParseError(error)}`);
13
- process.exit(1);
14
- }
15
- const { dryRun, noGitChecks } = parsed.flags;
8
+ const { dryRun, noGitChecks } = parseArgsOrExit(argv, tagFlagSchema).flags;
16
9
  try {
17
10
  createTags({ dryRun, noGitChecks });
18
11
  } catch (error) {
19
- console.error(error instanceof Error ? error.message : String(error));
12
+ reportError(error instanceof Error ? error.message : String(error));
20
13
  process.exit(1);
21
14
  }
22
15
  }
@@ -1,3 +1,4 @@
1
+ import { formatErrorLine } from "@williamthorsen/nmr-core";
1
2
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
2
3
  import {
3
4
  resolveOverridePath,
@@ -13,9 +14,15 @@ async function validateOverridesCommand(dependencies = {}) {
13
14
  const load = dependencies.loadConfig ?? loadConfig;
14
15
  const buildEntries = dependencies.buildEntries ?? defaultBuildEntries;
15
16
  const validate = dependencies.validate ?? validateAllChangelogOverrides;
17
+ let rawConfig;
18
+ try {
19
+ rawConfig = await load();
20
+ } catch (error) {
21
+ return { exitCode: 2, message: formatErrorLine(`Failed to load config: ${errorMessage(error)}`) };
22
+ }
16
23
  let userConfig;
17
24
  try {
18
- userConfig = await loadAndValidateConfig(load);
25
+ userConfig = validateLoadedConfig(rawConfig);
19
26
  } catch (error) {
20
27
  return { exitCode: 2, message: errorMessage(error) };
21
28
  }
@@ -23,13 +30,13 @@ async function validateOverridesCommand(dependencies = {}) {
23
30
  try {
24
31
  discoveredPaths = await discover();
25
32
  } catch (error) {
26
- return { exitCode: 2, message: `Error discovering workspaces: ${errorMessage(error)}` };
33
+ return { exitCode: 2, message: formatErrorLine(`Failed to discover workspaces: ${errorMessage(error)}`) };
27
34
  }
28
35
  let inputs;
29
36
  try {
30
37
  inputs = discoveredPaths === void 0 ? buildSinglePackageInputs(userConfig, buildEntries) : buildMonorepoInputs(discoveredPaths, userConfig, buildEntries);
31
38
  } catch (error) {
32
- return { exitCode: 2, message: `Error resolving overrides scope: ${errorMessage(error)}` };
39
+ return { exitCode: 2, message: formatErrorLine(`Failed to resolve overrides scope: ${errorMessage(error)}`) };
33
40
  }
34
41
  const result = validate(inputs);
35
42
  return formatValidateOverridesResult(result);
@@ -85,13 +92,7 @@ function flattenEntriesToHashes(entries) {
85
92
  }
86
93
  return hashes;
87
94
  }
88
- async function loadAndValidateConfig(load) {
89
- let rawConfig;
90
- try {
91
- rawConfig = await load();
92
- } catch (error) {
93
- throw new Error(`Error loading config: ${errorMessage(error)}`);
94
- }
95
+ function validateLoadedConfig(rawConfig) {
95
96
  if (rawConfig === void 0) {
96
97
  return void 0;
97
98
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { writeFileWithCheck } from "@williamthorsen/nmr-core";
3
+ import { reportError, writeFileWithCheck } from "@williamthorsen/nmr-core";
4
4
  import { extractVersion } from "./changelogJsonUtils.js";
5
5
  import { dim } from "./format.js";
6
6
  import { renderInjectedReadme } from "./injectReleaseNotesIntoReadme.js";
@@ -51,7 +51,7 @@ function writePreviewFile(filePath, content, dryRun) {
51
51
  }
52
52
  const result = writeFileWithCheck(filePath, content, { dryRun: false, overwrite: true });
53
53
  if (result.outcome === "failed") {
54
- console.error(`Error writing ${filePath}: ${result.error ?? "unknown error"}`);
54
+ reportError(`Failed to write ${filePath}: ${result.error ?? "unknown error"}`);
55
55
  return { filePath, outcome: "failed", ...result.error === void 0 ? {} : { error: result.error } };
56
56
  }
57
57
  if (result.outcome === "created" || result.outcome === "overwritten") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "5.3.2",
3
+ "version": "6.0.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/release-kit#readme",
@@ -35,14 +35,14 @@
35
35
  "glob": "13.0.6",
36
36
  "jiti": "2.7.0",
37
37
  "json-stringify-pretty-compact": "4.0.0",
38
- "semver": "7.8.4",
38
+ "semver": "7.8.5",
39
39
  "yaml": "2.9.0",
40
40
  "zod": "4.4.3",
41
- "@williamthorsen/nmr-core": "0.3.2"
41
+ "@williamthorsen/nmr-core": "0.5.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/semver": "7.7.1",
45
- "smol-toml": "1.6.1"
45
+ "smol-toml": "1.7.0"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=18.17.0"
@@ -53,10 +53,6 @@
53
53
  },
54
54
  "scripts": {
55
55
  "build:post": "node scripts/copy-work-types.js",
56
- "test": "pnpm exec vitest --config=vitest.standalone.config.ts",
57
- "test:coverage": "pnpm exec vitest --config=vitest.standalone.config.ts --coverage",
58
- "test:integration": "pnpm exec vitest --config=vitest.integration.config.ts",
59
- "test:watch": "pnpm exec vitest --config=vitest.standalone.config.ts --watch",
60
56
  "work-types:check": "pnpm exec release-kit work-types check",
61
57
  "work-types:sync": "pnpm exec release-kit work-types sync"
62
58
  }