@williamthorsen/release-kit 16.1.0 → 16.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/.meta/changelog.json +45 -0
  2. package/.readyup/kits/default.js +2 -2
  3. package/.readyup/kits/npm-auto-publish.js +1 -1
  4. package/.readyup/manifest.json +6 -6
  5. package/CHANGELOG.md +16 -0
  6. package/README.md +3 -5
  7. package/bin/release-kit.js +7 -3
  8. package/dist/esm/assertCleanWorkingTree.js +14 -7
  9. package/dist/esm/assertReleasesOnNpm.js +11 -2
  10. package/dist/esm/assertTaggedBaseline.js +2 -1
  11. package/dist/esm/assertTarballsComplete.js +15 -3
  12. package/dist/esm/buildChangelogEntries.js +2 -2
  13. package/dist/esm/buildDependencyGraph.js +5 -4
  14. package/dist/esm/bumpVersion.js +3 -2
  15. package/dist/esm/changelogOverrides.js +3 -2
  16. package/dist/esm/cli.js +11 -0
  17. package/dist/esm/commitCommand.js +11 -5
  18. package/dist/esm/compareVersions.js +2 -1
  19. package/dist/esm/createGithubRelease.js +7 -1
  20. package/dist/esm/createGithubReleaseCommand.js +3 -4
  21. package/dist/esm/createTags.js +6 -4
  22. package/dist/esm/deleteFileIfExists.js +2 -1
  23. package/dist/esm/deriveDirectoryConfig.js +7 -6
  24. package/dist/esm/deriveWorkspaceConfig.js +4 -3
  25. package/dist/esm/enterRepoRoot.js +2 -1
  26. package/dist/esm/enumerateReleaseWindows.js +2 -2
  27. package/dist/esm/init/initCommand.js +10 -1
  28. package/dist/esm/init/updateTemplatesCommand.js +10 -1
  29. package/dist/esm/loadConfig.js +6 -5
  30. package/dist/esm/mergeConfig.js +5 -4
  31. package/dist/esm/monorepoConfigAssertions.js +6 -5
  32. package/dist/esm/packFromCleanCheckout.js +12 -3
  33. package/dist/esm/planVersionBump.js +6 -5
  34. package/dist/esm/prepareCommand.js +12 -22
  35. package/dist/esm/prepareComposite.js +2 -1
  36. package/dist/esm/prepareRunContext.js +2 -2
  37. package/dist/esm/previewTagPrefixes.js +2 -2
  38. package/dist/esm/publish.js +7 -1
  39. package/dist/esm/publishCommand.js +4 -18
  40. package/dist/esm/pushCommand.js +5 -12
  41. package/dist/esm/pushRelease.js +18 -6
  42. package/dist/esm/releaseFiles.js +2 -2
  43. package/dist/esm/releasePlan.js +2 -2
  44. package/dist/esm/resolveCommandTags.d.ts +4 -4
  45. package/dist/esm/resolveCommandTags.js +3 -5
  46. package/dist/esm/resolveReleaseDeclaration.js +9 -8
  47. package/dist/esm/resolveReleaseTags.js +11 -4
  48. package/dist/esm/resolveReleaseTargets.js +2 -1
  49. package/dist/esm/runCli.d.ts +5 -0
  50. package/dist/esm/runCli.js +224 -0
  51. package/dist/esm/showTagPrefixesCommand.js +5 -3
  52. package/dist/esm/sync-labels/generateCommand.js +4 -1
  53. package/dist/esm/sync-labels/initCommand.js +7 -1
  54. package/dist/esm/sync-labels/presets.js +7 -7
  55. package/dist/esm/sync-labels/resolveLabels.js +4 -3
  56. package/dist/esm/sync-labels/templates.js +2 -2
  57. package/dist/esm/tagCommand.js +2 -9
  58. package/dist/esm/user-error-chaining.d.ts +13 -0
  59. package/dist/esm/user-error-chaining.js +11 -0
  60. package/dist/esm/validateOverridesCommand.js +7 -1
  61. package/package.json +6 -6
  62. package/dist/esm/bin/release-kit.js +0 -240
  63. /package/dist/esm/{bin/release-kit.d.ts → cli.d.ts} +0 -0
@@ -1,4 +1,49 @@
1
1
  [
2
+ {
3
+ "version": "16.1.2",
4
+ "date": "2026-11-06",
5
+ "sections": [
6
+ {
7
+ "title": "🐛 Bug fixes",
8
+ "audience": "all",
9
+ "items": [
10
+ {
11
+ "description": "Fixes the issue that `release-kit` printed a bug in its own code as an ordinary `Error: <message>`, which it now prints as `release-kit: internal error: <message>` followed by the stack, while a failure in the user's input, config, or environment, including a failed `git`, `gh`, or `npm` call, keeps the `Error: <message>` form. (#1158)",
12
+ "hash": "3a62cc768b23f37156c846f32fd4acaa70f7f931",
13
+ "entry": 1
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ },
19
+ {
20
+ "version": "16.1.1",
21
+ "date": "2026-11-05",
22
+ "sections": [
23
+ {
24
+ "title": "🐛 Bug fixes",
25
+ "audience": "all",
26
+ "items": [
27
+ {
28
+ "description": "Fixes the issue that the bin scripts of `nmr`, `release-kit`, and `v11y-check` reported every error other than missing build output as `<bin>: failed to load: <message>` without a stack, which hid where the fault occurred. (#1157)",
29
+ "hash": "ef1ed5f50bf0b01f19b6d6fe82a40322a8286002",
30
+ "entry": 2
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "title": "♻️ Refactoring",
36
+ "audience": "dev",
37
+ "items": [
38
+ {
39
+ "description": "Moves the CLI entries of `release-kit` and `v11y-check` from `src/bin/` to `src/cli.ts`, matching the layout of `nmr`, and renames the command module previously at `v11y-check`'s `src/cli.ts` to `src/commands.ts`. (#1154)",
40
+ "hash": "8ec4abd71b8952d9bee798031bd336c386fa7cf1",
41
+ "entry": 1
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ },
2
47
  {
3
48
  "version": "16.1.0",
4
49
  "date": "2026-11-03",
@@ -1,6 +1,6 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.44.0";
3
+ export const __readyupVersion = "0.45.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/default.ts
@@ -234,7 +234,7 @@ var CREATE_GITHUB_RELEASE_WORKFLOW_HASH_SINGLE = "d6de961de913c67b08dfa7e07f7349
234
234
 
235
235
  // .readyup/kits/default.ts
236
236
  function getMinVersion() {
237
- const picked = { "version": "16.1.0" };
237
+ const picked = { "version": "16.1.2" };
238
238
  if (typeof picked["version"] !== "string") {
239
239
  throw new TypeError("release-kit/package.json: 'version' must be a string");
240
240
  }
@@ -1,6 +1,6 @@
1
1
  /** @noformat -- @generated. Do not edit. Compiled by rdy. */
2
2
  /* eslint-disable */
3
- export const __readyupVersion = "0.44.0";
3
+ export const __readyupVersion = "0.45.0";
4
4
 
5
5
 
6
6
  // .readyup/kits/npm-auto-publish.ts
@@ -7,7 +7,7 @@
7
7
  "checklist": "release-kit",
8
8
  "checks": [
9
9
  "@williamthorsen/release-kit in devDependencies",
10
- "@williamthorsen/release-kit >= 16.1.0",
10
+ "@williamthorsen/release-kit >= 16.1.2",
11
11
  "release.yaml workflow exists",
12
12
  "release.yaml matches template",
13
13
  "release.yaml does not reference deprecated tag ref",
@@ -63,7 +63,7 @@
63
63
  "path": "lib/workflow-hashes.ts"
64
64
  },
65
65
  {
66
- "hash": "b44f7148",
66
+ "hash": "ff1fde3d",
67
67
  "kind": "inline",
68
68
  "path": "../package.json",
69
69
  "paths": [
@@ -98,10 +98,10 @@
98
98
  ],
99
99
  "name": "default",
100
100
  "path": "kits/default.js",
101
- "readyupVersion": "0.44.0",
101
+ "readyupVersion": "0.45.0",
102
102
  "source": "kits/default.ts",
103
103
  "sourceHash": "56566d0b",
104
- "targetHash": "5f810bb7"
104
+ "targetHash": "6a43de10"
105
105
  },
106
106
  {
107
107
  "checklistChecks": [
@@ -157,10 +157,10 @@
157
157
  ],
158
158
  "name": "npm-auto-publish",
159
159
  "path": "kits/npm-auto-publish.js",
160
- "readyupVersion": "0.44.0",
160
+ "readyupVersion": "0.45.0",
161
161
  "source": "kits/npm-auto-publish.ts",
162
162
  "sourceHash": "682fdd4a",
163
- "targetHash": "84f31b6d"
163
+ "targetHash": "52d15648"
164
164
  }
165
165
  ]
166
166
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 16.1.2 — 2026-11-06
6
+
7
+ ### 🐛 Bug fixes
8
+
9
+ - Fixes the issue that `release-kit` printed a bug in its own code as an ordinary `Error: <message>`, which it now prints as `release-kit: internal error: <message>` followed by the stack, while a failure in the user's input, config, or environment, including a failed `git`, `gh`, or `npm` call, keeps the `Error: <message>` form. (#1158)
10
+
11
+ ## 16.1.1 — 2026-11-05
12
+
13
+ ### 🐛 Bug fixes
14
+
15
+ - Fixes the issue that the bin scripts of `nmr`, `release-kit`, and `v11y-check` reported every error other than missing build output as `<bin>: failed to load: <message>` without a stack, which hid where the fault occurred. (#1157)
16
+
17
+ ### ♻️ Refactoring
18
+
19
+ - Moves the CLI entries of `release-kit` and `v11y-check` from `src/bin/` to `src/cli.ts`, matching the layout of `nmr`, and renames the command module previously at `v11y-check`'s `src/cli.ts` to `src/commands.ts`. (#1154)
20
+
5
21
  ## 16.1.0 — 2026-11-03
6
22
 
7
23
  ### 🎉 Features
package/README.md CHANGED
@@ -7,13 +7,11 @@ Version-bumping and changelog-generation toolkit for release workflows.
7
7
  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 the same commit history that decides each bump (with optional [editorial overrides](docs/editorial-overrides.md)).
8
8
 
9
9
  <!-- section:release-notes -->
10
- ## Release notes — v16.1.0 (2026-11-03)
10
+ ## Release notes — v16.1.2 (2026-11-06)
11
11
 
12
- ### 🎉 Features
12
+ ### 🐛 Bug fixes
13
13
 
14
- - Makes `prepare` and `publish` refuse a release, dry runs included, when a tarball packed from a clean checkout of HEAD lacks a file named by the package's `main`, `types`, `bin`, or `exports`, which keeps a package with missing build output from reaching the registry. (#1147)
15
- - Adds the `devExportConditions` config key, which lists the export conditions that resolve only inside the repo and whose targets the tarball check does not require, in addition to `source`. (#1147)
16
- - Adds a `content` label to the `common` label preset, for consumer-facing material such as copy, images, or displayed data rather than functionality. (#1150)
14
+ - Fixes the issue that `release-kit` printed a bug in its own code as an ordinary `Error: <message>`, which it now prints as `release-kit: internal error: <message>` followed by the stack, while a failure in the user's input, config, or environment, including a failed `git`, `gh`, or `npm` call, keeps the `Error: <message>` form. (#1158)
17
15
  <!-- /section:release-notes -->
18
16
 
19
17
  ## Installation
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  try {
3
- await import('../dist/esm/bin/release-kit.js');
3
+ await import('../dist/esm/cli.js');
4
4
  } catch (error) {
5
- if (error.code === 'ERR_MODULE_NOT_FOUND') {
5
+ if (error?.code === 'ERR_MODULE_NOT_FOUND') {
6
6
  process.stderr.write('release-kit: build output not found; run `nmr build` first\n');
7
7
  } else {
8
- process.stderr.write(`release-kit: failed to load: ${error.message}\n`);
8
+ const message = typeof error?.message === 'string' && error.message !== '' ? error.message : String(error);
9
+ const lines = typeof error?.stack === 'string' ? error.stack.split('\n') : [];
10
+ const firstFrame = lines.findIndex((line) => /^\s+at /.test(line));
11
+ const frames = firstFrame === -1 ? '' : `${lines.slice(firstFrame).join('\n')}\n`;
12
+ process.stderr.write(`release-kit: internal error: ${message}\n${frames}`);
9
13
  }
10
14
  process.exit(1);
11
15
  }
@@ -1,12 +1,19 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { GIT_OUTPUT_LIMIT_BYTES } from '@williamthorsen/nmr-core';
2
+ import { GIT_OUTPUT_LIMIT_BYTES, UserError } from '@williamthorsen/nmr-core';
3
+ import { asUserError } from "./user-error-chaining.js";
3
4
  export function assertCleanWorkingTree() {
4
- const status = execFileSync('git', ['status', '--porcelain'], {
5
- encoding: 'utf8',
6
- maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
7
- stdio: ['pipe', 'pipe', 'pipe'],
8
- }).trim();
5
+ let status;
6
+ try {
7
+ status = execFileSync('git', ['status', '--porcelain'], {
8
+ encoding: 'utf8',
9
+ maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
10
+ stdio: ['pipe', 'pipe', 'pipe'],
11
+ }).trim();
12
+ }
13
+ catch (error) {
14
+ throw asUserError(error);
15
+ }
9
16
  if (status.length > 0) {
10
- throw new Error('Working tree has uncommitted changes. Commit or stash them, or use --no-git-checks to bypass this check.');
17
+ throw new UserError('Working tree has uncommitted changes. Commit or stash them, or use --no-git-checks to bypass this check.');
11
18
  }
12
19
  }
@@ -1,8 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { UserError } from '@williamthorsen/nmr-core';
3
4
  import { isPublishableManifest } from "./isPublishableManifest.js";
4
5
  import { lookUpNpmPackage } from "./npmRegistry.js";
5
6
  import { isRecord } from "./typeGuards.js";
7
+ import { chainUserError } from "./user-error-chaining.js";
6
8
  export function assertReleasesOnNpm(workspacePaths) {
7
9
  const unpublishedCandidates = [];
8
10
  const unverifiableCandidates = [];
@@ -22,10 +24,17 @@ export function assertReleasesOnNpm(workspacePaths) {
22
24
  if (unpublishedCandidates.length === 0 && unverifiableCandidates.length === 0) {
23
25
  return;
24
26
  }
25
- throw new Error(describeRefusal(unpublishedCandidates, unverifiableCandidates));
27
+ throw new UserError(describeRefusal(unpublishedCandidates, unverifiableCandidates));
26
28
  }
27
29
  function buildNpmCandidate(workspacePath) {
28
- const manifest = JSON.parse(readFileSync(join(workspacePath, 'package.json'), 'utf8'));
30
+ const manifestPath = join(workspacePath, 'package.json');
31
+ let manifest;
32
+ try {
33
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
34
+ }
35
+ catch (error) {
36
+ throw chainUserError(`Failed to parse ${manifestPath}`, error);
37
+ }
29
38
  if (!isRecord(manifest) || !isPublishableManifest(manifest) || typeof manifest['name'] !== 'string') {
30
39
  return undefined;
31
40
  }
@@ -1,4 +1,5 @@
1
1
  import { join } from 'node:path';
2
+ import { UserError } from '@williamthorsen/nmr-core';
2
3
  import { resolveChangelogJsonPath } from "./changelogJsonFile.js";
3
4
  import { readChangelogEntries } from "./changelogJsonUtils.js";
4
5
  import { readCurrentVersion } from "./planVersionBump.js";
@@ -8,7 +9,7 @@ export function assertTaggedBaseline(findings) {
8
9
  if (untaggedFindings.length === 0)
9
10
  return;
10
11
  const lines = untaggedFindings.map(({ label, version, tag }) => ` - ${label}: ${version} (create tag ${tag})`);
11
- throw new Error([
12
+ throw new UserError([
12
13
  'The current version is recorded in the changelog, but its tag is not the previous tag reachable from HEAD:',
13
14
  ...lines,
14
15
  'Tag the commit that released each version, then run prepare again.',
@@ -1,11 +1,13 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { isAnticipatedFailure, UserError } from '@williamthorsen/nmr-core';
3
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
5
  import { detectPackageManager } from "./detectPackageManager.js";
5
6
  import { findMissingManifestPaths } from "./findMissingManifestPaths.js";
6
7
  import { isPublishableManifest } from "./isPublishableManifest.js";
7
8
  import { packFromCleanCheckout } from "./packFromCleanCheckout.js";
8
9
  import { isRecord } from "./typeGuards.js";
10
+ import { chainUserError } from "./user-error-chaining.js";
9
11
  export function assertTarballsComplete(workspacePaths, devExportConditions, pack = packFromCleanCheckout) {
10
12
  const candidates = workspacePaths
11
13
  .map((workspacePath) => ({ workspacePath, manifest: readManifest(workspacePath) }))
@@ -19,7 +21,10 @@ export function assertTarballsComplete(workspacePaths, devExportConditions, pack
19
21
  filesByWorkspace = pack(candidates.map(({ workspacePath }) => workspacePath), detectPackageManager());
20
22
  }
21
23
  catch (error) {
22
- throw new Error(`Cannot release: The packages could not be packed from a clean checkout of HEAD.\n${describeError(error)}`, { cause: error });
24
+ if (!isAnticipatedFailure(error)) {
25
+ throw error;
26
+ }
27
+ throw new UserError(`Cannot release: The packages could not be packed from a clean checkout of HEAD.\n${describeError(error)}`, { cause: error });
23
28
  }
24
29
  const incompletePackages = candidates
25
30
  .map(({ workspacePath, manifest }) => ({
@@ -31,7 +36,7 @@ export function assertTarballsComplete(workspacePaths, devExportConditions, pack
31
36
  if (incompletePackages.length === 0) {
32
37
  return;
33
38
  }
34
- throw new Error(describeRefusal(incompletePackages));
39
+ throw new UserError(describeRefusal(incompletePackages));
35
40
  }
36
41
  function describeRefusal(incompletePackages) {
37
42
  const sections = ['Cannot release: The tarball of every package must contain each file that its manifest names.'];
@@ -47,6 +52,13 @@ function describeRefusal(incompletePackages) {
47
52
  return sections.join('\n\n');
48
53
  }
49
54
  function readManifest(workspacePath) {
50
- const manifest = JSON.parse(readFileSync(join(workspacePath, 'package.json'), 'utf8'));
55
+ const manifestPath = join(workspacePath, 'package.json');
56
+ let manifest;
57
+ try {
58
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
59
+ }
60
+ catch (error) {
61
+ throw chainUserError(`Failed to parse ${manifestPath}`, error);
62
+ }
51
63
  return isRecord(manifest) ? manifest : {};
52
64
  }
@@ -1,4 +1,3 @@
1
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
2
1
  import { buildEntryItem, buildTitleItem, hasRoutedEntry, } from "./buildChangelogItem.js";
3
2
  import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
4
3
  import { extractVersion } from "./changelogJsonUtils.js";
@@ -8,6 +7,7 @@ import { determineBumpType } from "./determineBumpType.js";
8
7
  import { enumerateReleaseWindows } from "./enumerateReleaseWindows.js";
9
8
  import { parseChangeRecordBlock } from "./parseChangeRecordBlock.js";
10
9
  import { stripEmojiPrefix } from "./stripEmojiPrefix.js";
10
+ import { prefixUserError } from "./user-error-chaining.js";
11
11
  const UNRELEASED_TAG = 'unreleased';
12
12
  const CANONICAL_SECTION_ORDER = new Map(Object.values(DEFAULT_WORK_TYPES).map((config, index) => [stripGroupDecorations(config.header), index]));
13
13
  export function buildChangelogEntries(config, tag, options) {
@@ -49,7 +49,7 @@ export function readReleaseHistory(config, options) {
49
49
  return transformReleases(windows, context);
50
50
  }
51
51
  catch (error) {
52
- throw chainError(`Failed to read the release history for ${options.tagPrefixes.join(', ')}`, error);
52
+ throw prefixUserError(`Failed to read the release history for ${options.tagPrefixes.join(', ')}`, error);
53
53
  }
54
54
  }
55
55
  export function stripGroupDecorations(group) {
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
2
+ import { UserError } from '@williamthorsen/nmr-core';
3
+ import { chainUserError } from "./user-error-chaining.js";
3
4
  function isPackageJsonSubset(value) {
4
5
  return typeof value === 'object' && value !== null;
5
6
  }
@@ -56,17 +57,17 @@ function readPackageJsonSubset(filePath) {
56
57
  content = readFileSync(filePath, 'utf8');
57
58
  }
58
59
  catch (error) {
59
- throw chainError(`Failed to read ${filePath}`, error);
60
+ throw chainUserError(`Failed to read ${filePath}`, error);
60
61
  }
61
62
  let parsedJson;
62
63
  try {
63
64
  parsedJson = JSON.parse(content);
64
65
  }
65
66
  catch (error) {
66
- throw chainError(`Failed to parse JSON in ${filePath}`, error);
67
+ throw chainUserError(`Failed to parse JSON in ${filePath}`, error);
67
68
  }
68
69
  if (!isPackageJsonSubset(parsedJson)) {
69
- throw new Error(`Invalid package.json at ${filePath}`);
70
+ throw new UserError(`Invalid package.json at ${filePath}`);
70
71
  }
71
72
  return parsedJson;
72
73
  }
@@ -1,13 +1,14 @@
1
+ import { UserError } from '@williamthorsen/nmr-core';
1
2
  export function bumpVersion(version, releaseType) {
2
3
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
3
4
  if (!match) {
4
- throw new Error(`Invalid semver version: '${version}'`);
5
+ throw new UserError(`Invalid semver version: '${version}'`);
5
6
  }
6
7
  const major = match[1];
7
8
  const minor = match[2];
8
9
  const patch = match[3];
9
10
  if (major === undefined || minor === undefined || patch === undefined) {
10
- throw new Error(`Invalid semver version: '${version}'`);
11
+ throw new UserError(`Invalid semver version: '${version}'`);
11
12
  }
12
13
  const majorNumber = Number.parseInt(major, 10);
13
14
  const minorNumber = Number.parseInt(minor, 10);
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { UserError } from '@williamthorsen/nmr-core';
2
3
  import { applyChangelogOverrides, formatStaleOverrideKeyWarning } from "./applyChangelogOverrides.js";
3
4
  import { loadChangelogOverrides } from "./loadChangelogOverrides.js";
4
5
  const OVERRIDES_FILENAME = '.meta/changelog-overrides.json';
@@ -8,7 +9,7 @@ export function applyWorkspaceOverrides(newEntries, workspacePath, overrideConte
8
9
  const composedOverrides = composeOverrides(projectOverrides, workspaceOverrides);
9
10
  const appliedResult = applyChangelogOverrides(newEntries, composedOverrides);
10
11
  if (appliedResult.errors.length > 0) {
11
- throw new Error(`Changelog override application failed:\n - ${appliedResult.errors.join('\n - ')}`);
12
+ throw new UserError(`Changelog override application failed:\n - ${appliedResult.errors.join('\n - ')}`);
12
13
  }
13
14
  overrideWarnings.push(...appliedResult.warnings);
14
15
  const matchedSet = new Set(appliedResult.matchedKeys);
@@ -38,7 +39,7 @@ export function composeOverrides(rootEntries, workspaceEntries) {
38
39
  export function createOverrideContext(workspacePaths) {
39
40
  const result = loadOverridesForScopes({ projectPath: '.', workspaces: [...workspacePaths] });
40
41
  if (result.errors.length > 0) {
41
- throw new Error(`Failed to load changelog overrides:\n - ${result.errors.join('\n - ')}`);
42
+ throw new UserError(`Failed to load changelog overrides:\n - ${result.errors.join('\n - ')}`);
42
43
  }
43
44
  return {
44
45
  projectOverrides: result.projectOverrides,
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process';
3
+ import { reportFailure } from '@williamthorsen/nmr-core';
4
+ import { runCli } from "./runCli.js";
5
+ try {
6
+ await runCli(process.argv.slice(2));
7
+ }
8
+ catch (error) {
9
+ reportFailure('release-kit', error);
10
+ process.exitCode = 1;
11
+ }
@@ -1,7 +1,8 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { readFileSync } from 'node:fs';
3
- import { GIT_OUTPUT_LIMIT_BYTES, hasErrnoCode, parseArgsOrExit } from '@williamthorsen/nmr-core';
3
+ import { GIT_OUTPUT_LIMIT_BYTES, hasErrnoCode, parseArgsOrExit, UserError } from '@williamthorsen/nmr-core';
4
4
  import { readReleaseTags, RELEASE_SUMMARY_FILE, resolveReleaseTagsPath } from "./releaseFiles.js";
5
+ import { asUserError } from "./user-error-chaining.js";
5
6
  const commitFlagSchema = {
6
7
  isDryRun: { long: '--dry-run', type: 'boolean' },
7
8
  };
@@ -9,7 +10,7 @@ export function commitCommand(argv) {
9
10
  const isDryRun = parseArgsOrExit(argv, commitFlagSchema).flags.isDryRun;
10
11
  const tags = readReleaseTags();
11
12
  if (tags.length === 0) {
12
- throw new Error(`Tags file at ${resolveReleaseTagsPath()} is empty. Run \`release-kit prepare\` first.`);
13
+ throw new UserError(`Tags file at ${resolveReleaseTagsPath()} is empty. Run \`release-kit prepare\` first.`);
13
14
  }
14
15
  let summary = '';
15
16
  try {
@@ -19,7 +20,7 @@ export function commitCommand(argv) {
19
20
  if (hasErrnoCode(error, 'ENOENT')) {
20
21
  }
21
22
  else {
22
- throw error;
23
+ throw asUserError(error);
23
24
  }
24
25
  }
25
26
  const title = `release: ${tags.join(' ')}`;
@@ -42,7 +43,12 @@ export function commitCommand(argv) {
42
43
  }
43
44
  return;
44
45
  }
45
- execFileSync('git', ['add', '-A'], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
46
- execFileSync('git', ['commit', '-m', message], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
46
+ try {
47
+ execFileSync('git', ['add', '-A'], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
48
+ execFileSync('git', ['commit', '-m', message], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
49
+ }
50
+ catch (error) {
51
+ throw asUserError(error);
52
+ }
47
53
  console.info(`Created release commit: ${title}`);
48
54
  }
@@ -1,9 +1,10 @@
1
+ import { UserError } from '@williamthorsen/nmr-core';
1
2
  import semver from 'semver';
2
3
  const CANONICAL_SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
3
4
  function parseCanonicalSemver(version) {
4
5
  const match = version.match(CANONICAL_SEMVER_PATTERN);
5
6
  if (!match) {
6
- throw new Error(`Invalid semver version: '${version}'`);
7
+ throw new UserError(`Invalid semver version: '${version}'`);
7
8
  }
8
9
  const [, major = '', minor = '', patch = ''] = match;
9
10
  return {
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { extractVersion, readChangelogEntries } from "./changelogJsonUtils.js";
5
5
  import { matchesAudience, renderReleaseNotesSingle } from "./renderReleaseNotes.js";
6
+ import { asUserError } from "./user-error-chaining.js";
6
7
  export function createGithubRelease(options) {
7
8
  const { tag, changelogJsonPath, isDryRun, sectionOrder } = options;
8
9
  if (!existsSync(changelogJsonPath)) {
@@ -36,7 +37,12 @@ export function createGithubRelease(options) {
36
37
  console.info(`[dry-run] Would run: gh ${args.join(' ')}`);
37
38
  return { status: 'created' };
38
39
  }
39
- execFileSync('gh', args, { stdio: 'inherit' });
40
+ try {
41
+ execFileSync('gh', args, { stdio: 'inherit' });
42
+ }
43
+ catch (error) {
44
+ throw asUserError(error);
45
+ }
40
46
  return { status: 'created' };
41
47
  }
42
48
  export function createGithubReleases(tags, changelogJsonOutputPath, isDryRun, sectionOrder) {
@@ -1,5 +1,4 @@
1
- import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
- import { describeError } from '@williamthorsen/toolbelt.errors';
1
+ import { parseArgsOrExit } from '@williamthorsen/nmr-core';
3
2
  import { configFlagSchema } from "./configFlagSchema.js";
4
3
  import { createGithubReleases } from "./createGithubRelease.js";
5
4
  import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
@@ -8,6 +7,7 @@ import { loadUsableConfig } from "./loadValidatedConfig.js";
8
7
  import { parseRequestedTags } from "./parseRequestedTags.js";
9
8
  import { resolveCommandTags } from "./resolveCommandTags.js";
10
9
  import { resolveConfigFlag } from "./resolveConfigFlag.js";
10
+ import { prefixUserError } from "./user-error-chaining.js";
11
11
  const createGithubReleaseFlagSchema = {
12
12
  ...configFlagSchema,
13
13
  isDryRun: { long: '--dry-run', type: 'boolean' },
@@ -35,8 +35,7 @@ export async function createGithubReleaseCommand(argv, styles, invocationDir) {
35
35
  outcome = createGithubReleases(releasableTags, changelogJsonOutputPath, isDryRun, sectionOrder);
36
36
  }
37
37
  catch (error) {
38
- reportError(`Failed to create GitHub Releases: ${describeError(error)}`);
39
- process.exit(1);
38
+ throw prefixUserError('Failed to create GitHub Releases', error);
40
39
  }
41
40
  if (outcome.skippedTags.length === 0) {
42
41
  return;
@@ -1,8 +1,9 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { formatGlyphLine, GIT_OUTPUT_LIMIT_BYTES, hasErrnoCode } from '@williamthorsen/nmr-core';
2
+ import { formatGlyphLine, GIT_OUTPUT_LIMIT_BYTES, hasErrnoCode, UserError, } from '@williamthorsen/nmr-core';
3
3
  import { deleteFileIfExists } from "./deleteFileIfExists.js";
4
4
  import { RELEASE_GLYPHS } from "./glyphs.js";
5
5
  import { readReleaseTags, RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
6
+ import { asUserError } from "./user-error-chaining.js";
6
7
  export function createTags(options) {
7
8
  const { isDryRun, shouldSkipGitChecks, style } = options;
8
9
  const tags = readReleaseTags();
@@ -26,13 +27,14 @@ export function createTags(options) {
26
27
  createdTags.push(tag);
27
28
  }
28
29
  catch (error) {
30
+ const failure = asUserError(error);
29
31
  if (createdTags.length > 0) {
30
32
  console.warn('Tags created before failure:');
31
33
  for (const tag of createdTags) {
32
34
  console.warn(` ${tag}`);
33
35
  }
34
36
  }
35
- throw error;
37
+ throw failure;
36
38
  }
37
39
  }
38
40
  console.info('Created tags:');
@@ -50,8 +52,8 @@ function assertCleanWorkingTree() {
50
52
  }
51
53
  catch (error) {
52
54
  if (hasErrnoCode(error, 'ENOENT')) {
53
- throw error;
55
+ throw asUserError(error);
54
56
  }
55
- throw new Error('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.', { cause: error });
57
+ throw new UserError('Working tree is dirty. Commit or stash changes before tagging, or use `--no-git-checks` to skip this check.', { cause: error });
56
58
  }
57
59
  }
@@ -1,5 +1,6 @@
1
1
  import { unlinkSync } from 'node:fs';
2
2
  import { hasErrnoCode } from '@williamthorsen/nmr-core';
3
+ import { asUserError } from "./user-error-chaining.js";
3
4
  export function deleteFileIfExists(filePath) {
4
5
  try {
5
6
  unlinkSync(filePath);
@@ -8,6 +9,6 @@ export function deleteFileIfExists(filePath) {
8
9
  if (hasErrnoCode(error, 'ENOENT')) {
9
10
  return;
10
11
  }
11
- throw error;
12
+ throw asUserError(error);
12
13
  }
13
14
  }
@@ -1,13 +1,14 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
3
+ import { UserError } from '@williamthorsen/nmr-core';
4
4
  import { isRecord } from "./typeGuards.js";
5
+ import { chainUserError } from "./user-error-chaining.js";
5
6
  export const DIRECTORY_VERSION_FILE = 'version.json';
6
7
  const TAG_PREFIX_PATTERN = /^[\w.-]+$/;
7
8
  export function deriveDirectoryConfig(declaredDirectory, root = process.cwd()) {
8
9
  const label = `releases.${declaredDirectory.name}`;
9
10
  if (statSync(path.resolve(root, declaredDirectory.path), { throwIfNoEntry: false })?.isDirectory() !== true) {
10
- throw new Error(`${label}: path '${declaredDirectory.path}' is not a directory; declare a directory that exists in the repo`);
11
+ throw new UserError(`${label}: path '${declaredDirectory.path}' is not a directory; declare a directory that exists in the repo`);
11
12
  }
12
13
  const versionFile = resolveVersionFile(declaredDirectory.path, root);
13
14
  assertVersionField(label, versionFile, root);
@@ -28,17 +29,17 @@ export function deriveDirectoryConfig(declaredDirectory, root = process.cwd()) {
28
29
  function assertVersionField(label, versionFile, root) {
29
30
  const absolutePath = path.resolve(root, versionFile);
30
31
  if (!existsSync(absolutePath)) {
31
- throw new Error(`${label}: ${versionFile} not found; create it with the directory's current version, such as { "version": "0.0.0" }`);
32
+ throw new UserError(`${label}: ${versionFile} not found; create it with the directory's current version, such as { "version": "0.0.0" }`);
32
33
  }
33
34
  let parsedJson;
34
35
  try {
35
36
  parsedJson = JSON.parse(readFileSync(absolutePath, 'utf8'));
36
37
  }
37
38
  catch (error) {
38
- throw chainError(`${label}: failed to read ${versionFile}`, error);
39
+ throw chainUserError(`${label}: failed to read ${versionFile}`, error);
39
40
  }
40
41
  if (!isRecord(parsedJson) || typeof parsedJson['version'] !== 'string') {
41
- throw new Error(`${label}: ${versionFile} does not contain a string 'version' field; add one`);
42
+ throw new UserError(`${label}: ${versionFile} does not contain a string 'version' field; add one`);
42
43
  }
43
44
  }
44
45
  function deriveTagPrefix(label, directoryPath) {
@@ -47,7 +48,7 @@ function deriveTagPrefix(label, directoryPath) {
47
48
  }
48
49
  const basename = path.posix.basename(directoryPath);
49
50
  if (!TAG_PREFIX_PATTERN.test(basename)) {
50
- throw new Error(`${label}: the directory name '${basename}' cannot form a tag prefix; set tagPrefix, such as '${basename.replaceAll(/[^\w.-]+/g, '-')}-v'`);
51
+ throw new UserError(`${label}: the directory name '${basename}' cannot form a tag prefix; set tagPrefix, such as '${basename.replaceAll(/[^\w.-]+/g, '-')}-v'`);
51
52
  }
52
53
  return `${basename}-v`;
53
54
  }
@@ -1,8 +1,9 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { basename } from 'node:path';
3
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
3
+ import { UserError } from '@williamthorsen/nmr-core';
4
4
  import { isPublishableManifest } from "./isPublishableManifest.js";
5
5
  import { isRecord } from "./typeGuards.js";
6
+ import { chainUserError } from "./user-error-chaining.js";
6
7
  export function deriveWorkspaceConfig(workspacePath) {
7
8
  const dir = basename(workspacePath);
8
9
  const packageJsonPath = `${workspacePath}/package.json`;
@@ -11,11 +12,11 @@ export function deriveWorkspaceConfig(workspacePath) {
11
12
  parsedJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
12
13
  }
13
14
  catch (error) {
14
- throw chainError(`Failed to read ${packageJsonPath}`, error);
15
+ throw chainUserError(`Failed to read ${packageJsonPath}`, error);
15
16
  }
16
17
  const name = isRecord(parsedJson) ? parsedJson['name'] : undefined;
17
18
  if (typeof name !== 'string' || name.length === 0) {
18
- throw new Error(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
19
+ throw new UserError(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
19
20
  }
20
21
  const unscopedName = stripNpmScope(name);
21
22
  const isPublishable = isPublishableManifest(parsedJson);