@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,19 +1,18 @@
1
1
  import { reportError } from '@williamthorsen/nmr-core';
2
- import { describeError } from '@williamthorsen/toolbelt.errors';
3
2
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
4
3
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
5
4
  import { formatExcludedSkip } from "./formatExcludedSkip.js";
6
5
  import { readRootPackageVersion } from "./loadConfig.js";
7
6
  import { mergeMonorepoConfig } from "./mergeConfig.js";
8
7
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
8
+ import { prefixUserError } from "./user-error-chaining.js";
9
9
  export function resolveCommandTags(tags, userConfig) {
10
10
  let workspace;
11
11
  try {
12
12
  workspace = discoverWorkspaces();
13
13
  }
14
14
  catch (error) {
15
- reportError(`Failed to discover workspaces: ${describeError(error)}`);
16
- process.exit(1);
15
+ throw prefixUserError('Failed to discover workspaces', error);
17
16
  }
18
17
  if (workspace.kind === 'empty') {
19
18
  reportError(`No workspace package to tag. ${describeEmptyWorkspace(workspace)}`);
@@ -38,8 +37,7 @@ export function resolveCommandTags(tags, userConfig) {
38
37
  }
39
38
  }
40
39
  catch (error) {
41
- reportError(`Failed to resolve workspaces: ${describeError(error)}`);
42
- process.exit(1);
40
+ throw prefixUserError('Failed to resolve workspaces', error);
43
41
  }
44
42
  let resolvedTags;
45
43
  if (workspaces !== undefined) {
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { UserError } from '@williamthorsen/nmr-core';
2
3
  export function getTargetName(workspace) {
3
4
  return workspace.targetName ?? workspace.dir;
4
5
  }
@@ -21,13 +22,13 @@ function applyLegacyIdentities(workspace, legacyIdentities) {
21
22
  function claimPath(entryByPath, name, claimedPath, kind) {
22
23
  const claimingEntryName = entryByPath.get(claimedPath);
23
24
  if (claimingEntryName !== undefined) {
24
- throw new Error(`releases.${name}: ${kind} '${claimedPath}' is already declared by releases.${claimingEntryName}; declare each ${kind} once`);
25
+ throw new UserError(`releases.${name}: ${kind} '${claimedPath}' is already declared by releases.${claimingEntryName}; declare each ${kind} once`);
25
26
  }
26
27
  entryByPath.set(claimedPath, name);
27
28
  }
28
29
  function declareDirectory(name, entry, normalizedPath) {
29
30
  if (path.posix.isAbsolute(normalizedPath) || normalizedPath === '..' || normalizedPath.startsWith('../')) {
30
- throw new Error(`releases.${name}: path '${entry.path}' is outside the repo; declare a repo-relative directory`);
31
+ throw new UserError(`releases.${name}: path '${entry.path}' is outside the repo; declare a repo-relative directory`);
31
32
  }
32
33
  const isRoot = normalizedPath === '.' || normalizedPath === '';
33
34
  return {
@@ -52,12 +53,12 @@ function resolveCompositeMembers(composite, retainedWorkspaces, excludedWorkspac
52
53
  }
53
54
  const label = `releases.${composite.name}.includes: '${memberName}'`;
54
55
  if (memberName === composite.name) {
55
- throw new Error(`${label} is the composite itself; a composite includes package and directory targets only`);
56
+ throw new UserError(`${label} is the composite itself; a composite includes package and directory targets only`);
56
57
  }
57
58
  if (excludedWorkspaces.some((workspace) => workspace.dir === memberName)) {
58
- throw new Error(`${label} is excluded, so the composite cannot include it`);
59
+ throw new UserError(`${label} is excluded, so the composite cannot include it`);
59
60
  }
60
- throw new Error(`${label} is not a release target. Known targets: ${membersByName.keys().toArray().join(', ')}`);
61
+ throw new UserError(`${label} is not a release target. Known targets: ${membersByName.keys().toArray().join(', ')}`);
61
62
  });
62
63
  }
63
64
  function resolveLegacyBlocks(workspaces, overrides, project) {
@@ -93,7 +94,7 @@ function resolveReleasesBlock(workspaces, releases) {
93
94
  for (const [name, entry] of Object.entries(releases)) {
94
95
  if (entry !== false && 'includes' in entry) {
95
96
  if (composite !== undefined) {
96
- throw new Error(`releases declares more than one composite ('${composite.name}', '${name}'); ` +
97
+ throw new UserError(`releases declares more than one composite ('${composite.name}', '${name}'); ` +
97
98
  'a composite is versioned by the root package.json, which only one composite can own');
98
99
  }
99
100
  composite = {
@@ -106,7 +107,7 @@ function resolveReleasesBlock(workspaces, releases) {
106
107
  if (entry === false) {
107
108
  const workspace = workspaceByDir.get(name);
108
109
  if (workspace === undefined) {
109
- throw new Error(`releases.${name}: false excludes a discovered workspace by its directory name, and no workspace is named '${name}'. ` +
110
+ throw new UserError(`releases.${name}: false excludes a discovered workspace by its directory name, and no workspace is named '${name}'. ` +
110
111
  `Known workspaces: ${workspaceByDir.keys().toArray().join(', ')}`);
111
112
  }
112
113
  claimPath(entryByPath, name, workspace.workspacePath, 'workspace');
@@ -122,7 +123,7 @@ function resolveReleasesBlock(workspaces, releases) {
122
123
  continue;
123
124
  }
124
125
  if (entry.tagPrefix !== undefined) {
125
- throw new Error(`releases.${name}: tagPrefix applies only to a directory target, and '${workspace.workspacePath}' is a workspace, ` +
126
+ throw new UserError(`releases.${name}: tagPrefix applies only to a directory target, and '${workspace.workspacePath}' is a workspace, ` +
126
127
  "whose tag prefix derives from its package name; declare the workspace's earlier prefixes in legacyIdentities");
127
128
  }
128
129
  claimPath(entryByPath, name, workspace.workspacePath, 'workspace');
@@ -2,13 +2,20 @@ import { execFileSync } from 'node:child_process';
2
2
  import { GIT_OUTPUT_LIMIT_BYTES } from '@williamthorsen/nmr-core';
3
3
  import { getTargetName } from "./resolveReleaseDeclaration.js";
4
4
  import { PROJECT_TARGET_ID } from "./resolveReleaseTargets.js";
5
+ import { asUserError } from "./user-error-chaining.js";
5
6
  const VERSION_PATTERN = /^v\d+\.\d+\.\d+/;
6
7
  const SEMVER_SUFFIX_PATTERN = /^\d+\.\d+\.\d+/;
7
8
  export function resolveReleaseTags(args) {
8
- const output = execFileSync('git', ['tag', '--points-at', 'HEAD'], {
9
- encoding: 'utf8',
10
- maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
11
- });
9
+ let output;
10
+ try {
11
+ output = execFileSync('git', ['tag', '--points-at', 'HEAD'], {
12
+ encoding: 'utf8',
13
+ maxBuffer: GIT_OUTPUT_LIMIT_BYTES,
14
+ });
15
+ }
16
+ catch (error) {
17
+ throw asUserError(error);
18
+ }
12
19
  const tags = output
13
20
  .split('\n')
14
21
  .map((line) => line.trim())
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { UserError } from '@williamthorsen/nmr-core';
2
3
  import { isPublishableManifest } from "./isPublishableManifest.js";
3
4
  import { readRootPackageVersion } from "./loadConfig.js";
4
5
  import { mergeMonorepoConfig, mergeSinglePackageConfig } from "./mergeConfig.js";
@@ -35,7 +36,7 @@ function assertUniqueTargetIds(targets) {
35
36
  }
36
37
  for (const [id, paths] of pathsById) {
37
38
  if (paths.length > 1) {
38
- throw new Error(`Duplicate release target id '${id}' for workspaces: ${paths.join(', ')}`);
39
+ throw new UserError(`Duplicate release target id '${id}' for workspaces: ${paths.join(', ')}`);
39
40
  }
40
41
  }
41
42
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Dispatches the `release-kit` command named by `args[0]`. A command exits the process itself; a failure that it throws
3
+ * propagates to the caller.
4
+ */
5
+ export declare function runCli(args: string[]): Promise<void>;
@@ -0,0 +1,224 @@
1
+ import { parseArgsOrExit, reportError, resolveSelfVersion } from '@williamthorsen/nmr-core';
2
+ import { commitCommand } from "./commitCommand.js";
3
+ import { configFlagSchema } from "./configFlagSchema.js";
4
+ import { createGithubReleaseCommand } from "./createGithubReleaseCommand.js";
5
+ import { enterRepoRoot } from "./enterRepoRoot.js";
6
+ import { showInitHelp, showUpdateTemplatesHelp } from "./help/initHelp.js";
7
+ import { showOverridesHelp, showOverridesValidateHelp } from "./help/overridesHelp.js";
8
+ import { showPrepareHelp } from "./help/prepareHelp.js";
9
+ import { showCommitHelp, showCreateGithubReleaseHelp, showPublishHelp, showPushHelp, showShowTagPrefixesHelp, showTagHelp, } from "./help/releaseStepsHelp.js";
10
+ import { showSyncLabelsGenerateHelp, showSyncLabelsHelp, showSyncLabelsInitHelp, showSyncLabelsSyncHelp, } from "./help/syncLabelsHelp.js";
11
+ import { showUsage } from "./help/usage.js";
12
+ import { initCommand } from "./init/initCommand.js";
13
+ import { updateTemplatesCommand } from "./init/updateTemplatesCommand.js";
14
+ import { prepareCommand } from "./prepareCommand.js";
15
+ import { publishCommand } from "./publishCommand.js";
16
+ import { pushCommand } from "./pushCommand.js";
17
+ import { resolveConfigFlag } from "./resolveConfigFlag.js";
18
+ import { resolveStylesOrExit } from "./resolveStylesOrExit.js";
19
+ import { showTagPrefixesCommand } from "./showTagPrefixesCommand.js";
20
+ import { generateCommand } from "./sync-labels/generateCommand.js";
21
+ import { syncLabelsInitCommand } from "./sync-labels/initCommand.js";
22
+ import { syncLabelsCommand } from "./sync-labels/syncCommand.js";
23
+ import { tagCommand } from "./tagCommand.js";
24
+ import { validateOverridesCommand } from "./validateOverridesCommand.js";
25
+ const VERSION = resolveSelfVersion(import.meta.url);
26
+ export async function runCli(args) {
27
+ const command = args[0];
28
+ if (command === '--version' || command === '-V') {
29
+ console.info(VERSION);
30
+ process.exit(0);
31
+ }
32
+ if (command === undefined || ['--help', '-h'].includes(command)) {
33
+ showUsage();
34
+ process.exit(0);
35
+ }
36
+ const flags = args.slice(1);
37
+ const styles = resolveStylesOrExit({
38
+ env: process.env,
39
+ stderrIsTty: process.stderr.isTTY,
40
+ stdoutIsTty: process.stdout.isTTY,
41
+ });
42
+ if (command === 'prepare') {
43
+ if (flags.some((f) => f === '--help' || f === '-h')) {
44
+ showPrepareHelp();
45
+ process.exit(0);
46
+ }
47
+ const { invocationDir } = enterRepoRoot();
48
+ await prepareCommand(flags, styles, invocationDir);
49
+ process.exit(0);
50
+ }
51
+ if (command === 'commit') {
52
+ if (flags.some((f) => f === '--help' || f === '-h')) {
53
+ showCommitHelp();
54
+ process.exit(0);
55
+ }
56
+ enterRepoRoot();
57
+ commitCommand(flags);
58
+ process.exit(0);
59
+ }
60
+ if (command === 'tag') {
61
+ if (flags.some((f) => f === '--help' || f === '-h')) {
62
+ showTagHelp();
63
+ process.exit(0);
64
+ }
65
+ enterRepoRoot();
66
+ tagCommand(flags, styles);
67
+ process.exit(0);
68
+ }
69
+ if (command === 'push') {
70
+ if (flags.some((f) => f === '--help' || f === '-h')) {
71
+ showPushHelp();
72
+ process.exit(0);
73
+ }
74
+ const { invocationDir } = enterRepoRoot();
75
+ await pushCommand(flags, styles, invocationDir);
76
+ process.exit(0);
77
+ }
78
+ if (command === 'create-github-release') {
79
+ if (flags.some((f) => f === '--help' || f === '-h')) {
80
+ showCreateGithubReleaseHelp();
81
+ process.exit(0);
82
+ }
83
+ const { invocationDir } = enterRepoRoot();
84
+ await createGithubReleaseCommand(flags, styles, invocationDir);
85
+ process.exit(0);
86
+ }
87
+ if (command === 'publish') {
88
+ if (flags.some((f) => f === '--help' || f === '-h')) {
89
+ showPublishHelp();
90
+ process.exit(0);
91
+ }
92
+ const { invocationDir } = enterRepoRoot();
93
+ await publishCommand(flags, styles, invocationDir);
94
+ process.exit(0);
95
+ }
96
+ if (command === 'show-tag-prefixes') {
97
+ if (flags.some((f) => f === '--help' || f === '-h')) {
98
+ showShowTagPrefixesHelp();
99
+ process.exit(0);
100
+ }
101
+ const { invocationDir } = enterRepoRoot();
102
+ const { config } = parseArgsOrExit(flags, configFlagSchema).flags;
103
+ const exitCode = await showTagPrefixesCommand(styles, resolveConfigFlag(config, invocationDir));
104
+ process.exit(exitCode);
105
+ }
106
+ if (command === 'init') {
107
+ if (flags.some((f) => f === '--help' || f === '-h')) {
108
+ showInitHelp();
109
+ process.exit(0);
110
+ }
111
+ enterRepoRoot();
112
+ const initFlagSchema = {
113
+ isDryRun: { long: '--dry-run', type: 'boolean' },
114
+ shouldForce: { long: '--force', type: 'boolean' },
115
+ shouldScaffoldConfig: { long: '--with-config', type: 'boolean' },
116
+ };
117
+ const { isDryRun, shouldForce, shouldScaffoldConfig } = parseArgsOrExit(flags, initFlagSchema).flags;
118
+ const exitCode = initCommand({ isDryRun, shouldForce, styles, shouldScaffoldConfig });
119
+ process.exit(exitCode);
120
+ }
121
+ if (command === 'update-templates') {
122
+ if (flags.some((f) => f === '--help' || f === '-h')) {
123
+ showUpdateTemplatesHelp();
124
+ process.exit(0);
125
+ }
126
+ enterRepoRoot();
127
+ const updateTemplatesFlagSchema = {
128
+ isDryRun: { long: '--dry-run', type: 'boolean' },
129
+ };
130
+ const { isDryRun } = parseArgsOrExit(flags, updateTemplatesFlagSchema).flags;
131
+ const exitCode = updateTemplatesCommand({ isDryRun, styles });
132
+ process.exit(exitCode);
133
+ }
134
+ if (command === 'sync-labels') {
135
+ const subcommand = flags[0];
136
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
137
+ showSyncLabelsHelp();
138
+ process.exit(0);
139
+ }
140
+ const subflags = flags.slice(1);
141
+ if (subcommand === 'init') {
142
+ if (subflags.some((f) => f === '--help' || f === '-h')) {
143
+ showSyncLabelsInitHelp();
144
+ process.exit(0);
145
+ }
146
+ const { invocationDir } = enterRepoRoot();
147
+ const syncLabelsInitFlagSchema = {
148
+ ...configFlagSchema,
149
+ isDryRun: { long: '--dry-run', type: 'boolean' },
150
+ shouldForce: { long: '--force', type: 'boolean' },
151
+ };
152
+ const { config, isDryRun, shouldForce } = parseArgsOrExit(subflags, syncLabelsInitFlagSchema).flags;
153
+ const configPath = resolveConfigFlag(config, invocationDir);
154
+ const exitCode = await syncLabelsInitCommand({
155
+ ...(configPath !== undefined && { configPath }),
156
+ isDryRun,
157
+ shouldForce,
158
+ styles,
159
+ });
160
+ process.exit(exitCode);
161
+ }
162
+ if (subcommand === 'generate') {
163
+ if (subflags.some((f) => f === '--help' || f === '-h')) {
164
+ showSyncLabelsGenerateHelp();
165
+ process.exit(0);
166
+ }
167
+ const { invocationDir } = enterRepoRoot();
168
+ const generateFlagSchema = {
169
+ ...configFlagSchema,
170
+ shouldCheck: { long: '--check', type: 'boolean' },
171
+ };
172
+ const { shouldCheck, config } = parseArgsOrExit(subflags, generateFlagSchema).flags;
173
+ const configPath = resolveConfigFlag(config, invocationDir);
174
+ const exitCode = await generateCommand({ shouldCheck, ...(configPath !== undefined && { configPath }), styles });
175
+ process.exit(exitCode);
176
+ }
177
+ if (subcommand === 'sync') {
178
+ if (subflags.some((f) => f === '--help' || f === '-h')) {
179
+ showSyncLabelsSyncHelp();
180
+ process.exit(0);
181
+ }
182
+ if (subflags.length > 0) {
183
+ reportError(`Unknown option: ${subflags[0]}`);
184
+ process.exit(1);
185
+ }
186
+ enterRepoRoot();
187
+ const exitCode = syncLabelsCommand();
188
+ process.exit(exitCode);
189
+ }
190
+ reportError(`Unknown subcommand: ${subcommand}`);
191
+ showSyncLabelsHelp();
192
+ process.exit(1);
193
+ }
194
+ if (command === 'overrides') {
195
+ const subcommand = flags[0];
196
+ if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
197
+ showOverridesHelp();
198
+ process.exit(0);
199
+ }
200
+ const subflags = flags.slice(1);
201
+ if (subcommand === 'validate') {
202
+ if (subflags.some((f) => f === '--help' || f === '-h')) {
203
+ showOverridesValidateHelp();
204
+ process.exit(0);
205
+ }
206
+ const { invocationDir } = enterRepoRoot();
207
+ const { config } = parseArgsOrExit(subflags, configFlagSchema).flags;
208
+ const result = await validateOverridesCommand(styles, resolveConfigFlag(config, invocationDir));
209
+ if (result.exitCode === 0) {
210
+ console.info(result.message);
211
+ }
212
+ else {
213
+ process.stderr.write(`${result.message}\n`);
214
+ }
215
+ process.exit(result.exitCode);
216
+ }
217
+ reportError(`Unknown subcommand: ${subcommand}`);
218
+ showOverridesHelp();
219
+ process.exit(1);
220
+ }
221
+ reportError(`Unknown command: ${command}`);
222
+ showUsage();
223
+ process.exit(1);
224
+ }
@@ -1,5 +1,4 @@
1
- import { formatStatusLine, reportError } from '@williamthorsen/nmr-core';
2
- import { describeError } from '@williamthorsen/toolbelt.errors';
1
+ import { formatStatusLine, isAnticipatedFailure, reportError, } from '@williamthorsen/nmr-core';
3
2
  import { detectRepoType } from "./init/detectRepoType.js";
4
3
  import { loadValidatedConfig, reportConfigProblem } from "./loadValidatedConfig.js";
5
4
  import { previewTagPrefixes } from "./previewTagPrefixes.js";
@@ -18,7 +17,10 @@ export async function showTagPrefixesCommand(styles, configPath) {
18
17
  preview = previewTagPrefixes(result.status === 'ok' ? result.config : undefined);
19
18
  }
20
19
  catch (error) {
21
- reportError(describeError(error));
20
+ if (!isAnticipatedFailure(error)) {
21
+ throw error;
22
+ }
23
+ reportError(error.message);
22
24
  return 1;
23
25
  }
24
26
  process.stdout.write(renderMonorepo(preview, styles.stdout));
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { reportError } from '@williamthorsen/nmr-core';
3
+ import { isAnticipatedFailure, reportError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { stringify } from 'yaml';
6
6
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
@@ -54,6 +54,9 @@ export async function generateCommand({ shouldCheck = false, configPath, styles
54
54
  labels = resolveLabels(repoLabels);
55
55
  }
56
56
  catch (error) {
57
+ if (!isAnticipatedFailure(error)) {
58
+ throw error;
59
+ }
57
60
  const message = describeError(error);
58
61
  reportError(`Failed to resolve labels: ${message}`);
59
62
  return 1;
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { reportWriteResult, writeFileWithCheck } from '@williamthorsen/nmr-core';
2
+ import { isAnticipatedFailure, reportWriteResult, writeFileWithCheck, } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { describeEmptyWorkspace, discoverWorkspaces } from "../discoverWorkspaces.js";
5
5
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
@@ -22,6 +22,9 @@ export async function syncLabelsInitCommand({ configPath, isDryRun, shouldForce,
22
22
  workspace = discoverWorkspaces();
23
23
  }
24
24
  catch (error) {
25
+ if (!isAnticipatedFailure(error)) {
26
+ throw error;
27
+ }
25
28
  const message = describeError(error);
26
29
  process.stderr.write(` Failed to discover workspaces: ${message}\n`);
27
30
  return 1;
@@ -48,6 +51,9 @@ export async function syncLabelsInitCommand({ configPath, isDryRun, shouldForce,
48
51
  workflow = readSyncLabelsWorkflowTemplate();
49
52
  }
50
53
  catch (error) {
54
+ if (!isAnticipatedFailure(error)) {
55
+ throw error;
56
+ }
51
57
  process.stderr.write(` ${describeError(error)}\n`);
52
58
  return 1;
53
59
  }
@@ -2,10 +2,10 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { CANONICAL_TAXONOMY } from '@williamthorsen/change-grammar';
5
- import { findPackageRoot } from '@williamthorsen/nmr-core';
6
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
5
+ import { findPackageRoot, UserError } from '@williamthorsen/nmr-core';
7
6
  import { parse } from 'yaml';
8
7
  import { isRecord } from "../typeGuards.js";
8
+ import { chainUserError } from "../user-error-chaining.js";
9
9
  import { deriveTypeLabels } from "./typeLabels.js";
10
10
  const TYPE_LABELS_PRESET = 'common';
11
11
  export function hashPreset(presetName) {
@@ -16,29 +16,29 @@ export function hashPreset(presetName) {
16
16
  export function loadPreset(presetName) {
17
17
  const presetPath = resolvePresetPath(presetName);
18
18
  if (!existsSync(presetPath)) {
19
- throw new Error(`Unknown preset "${presetName}". The file ${presetPath} does not exist`);
19
+ throw new UserError(`Unknown preset "${presetName}". The file ${presetPath} does not exist`);
20
20
  }
21
21
  let content;
22
22
  try {
23
23
  content = readFileSync(presetPath, 'utf8');
24
24
  }
25
25
  catch (error) {
26
- throw chainError(`Failed to read preset "${presetName}"`, error);
26
+ throw chainUserError(`Failed to read preset "${presetName}"`, error);
27
27
  }
28
28
  const parsedYaml = parse(content);
29
29
  if (!Array.isArray(parsedYaml)) {
30
- throw new TypeError(`Preset "${presetName}" must be a YAML array of label definitions`);
30
+ throw new UserError(`Preset "${presetName}" must be a YAML array of label definitions`);
31
31
  }
32
32
  const labels = [];
33
33
  for (const entry of parsedYaml) {
34
34
  if (!isRecord(entry)) {
35
- throw new Error(`Preset "${presetName}" contains an invalid label entry: ${JSON.stringify(entry)}`);
35
+ throw new UserError(`Preset "${presetName}" contains an invalid label entry: ${JSON.stringify(entry)}`);
36
36
  }
37
37
  const { color, description, name } = entry;
38
38
  if (typeof name !== 'string' ||
39
39
  typeof color !== 'string' ||
40
40
  (description !== undefined && typeof description !== 'string')) {
41
- throw new TypeError(`Preset "${presetName}" contains a label with invalid fields: ${JSON.stringify(entry)}`);
41
+ throw new UserError(`Preset "${presetName}" contains a label with invalid fields: ${JSON.stringify(entry)}`);
42
42
  }
43
43
  labels.push({ name, color, ...(description !== undefined && { description }) });
44
44
  }
@@ -1,10 +1,11 @@
1
+ import { UserError } from '@williamthorsen/nmr-core';
1
2
  import { loadPreset } from "./presets.js";
2
3
  export function resolveLabels(config) {
3
4
  const labelEntries = Object.entries(config.labels ?? {});
4
5
  const caseVariantGroups = findCaseVariantGroups(labelEntries.map(([name]) => name));
5
6
  if (caseVariantGroups.length > 0) {
6
7
  const groupList = caseVariantGroups.map((group) => group.map((name) => `'${name}'`).join(' and ')).join('; ');
7
- throw new Error(`Keys in 'labels' differ only in case, which GitHub treats as the same label: ${groupList}. Keep one entry per label.`);
8
+ throw new UserError(`Keys in 'labels' differ only in case, which GitHub treats as the same label: ${groupList}. Keep one entry per label.`);
8
9
  }
9
10
  const resolvedLabels = new Map();
10
11
  const presetNames = config.extends ?? [];
@@ -17,7 +18,7 @@ export function resolveLabels(config) {
17
18
  const key = getLabelKey(name);
18
19
  if (spec === null) {
19
20
  if (!resolvedLabels.has(key)) {
20
- throw new Error(`Label '${name}' is set to null (remove), but the presets in 'extends' don't define it. Fix the name or delete the entry.`);
21
+ throw new UserError(`Label '${name}' is set to null (remove), but the presets in 'extends' don't define it. Fix the name or delete the entry.`);
21
22
  }
22
23
  resolvedLabels.delete(key);
23
24
  }
@@ -34,7 +35,7 @@ export function resolveLabels(config) {
34
35
  const labels = sortLabels(resolvedLabels.values().toArray());
35
36
  const aliasCollisions = findAliasCollisions(labels);
36
37
  if (aliasCollisions.length > 0) {
37
- throw new Error(`Aliases must name labels that are not declared and must not repeat, or the sync merges one label into another and deletes it: ${aliasCollisions.join('; ')}.`);
38
+ throw new UserError(`Aliases must name labels that are not declared and must not repeat, or the sync merges one label into another and deletes it: ${aliasCollisions.join('; ')}.`);
38
39
  }
39
40
  return labels;
40
41
  }
@@ -1,14 +1,14 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { basename, resolve } from 'node:path';
3
3
  import { findPackageRoot } from '@williamthorsen/nmr-core';
4
- import { describeError } from '@williamthorsen/toolbelt.errors';
4
+ import { chainUserError } from "../user-error-chaining.js";
5
5
  export function readSyncLabelsWorkflowTemplate() {
6
6
  const templatePath = resolve(findPackageRoot(import.meta.url), 'templates', 'sync-labels.yaml.template');
7
7
  try {
8
8
  return readFileSync(templatePath, 'utf8');
9
9
  }
10
10
  catch (error) {
11
- throw new Error(`Failed to read bundled template at ${templatePath}: ${describeError(error)}`, { cause: error });
11
+ throw chainUserError(`Failed to read bundled template at ${templatePath}`, error);
12
12
  }
13
13
  }
14
14
  export function buildScopeLabels(workspacePaths, retiredNames = []) {
@@ -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 { createTags } from "./createTags.js";
4
3
  const tagFlagSchema = {
5
4
  isDryRun: { long: '--dry-run', type: 'boolean' },
@@ -7,11 +6,5 @@ const tagFlagSchema = {
7
6
  };
8
7
  export function tagCommand(argv, styles) {
9
8
  const { isDryRun, shouldSkipGitChecks } = parseArgsOrExit(argv, tagFlagSchema).flags;
10
- try {
11
- createTags({ isDryRun, shouldSkipGitChecks, style: styles.stdout });
12
- }
13
- catch (error) {
14
- reportError(describeError(error));
15
- process.exit(1);
16
- }
9
+ createTags({ isDryRun, shouldSkipGitChecks, style: styles.stdout });
17
10
  }
@@ -0,0 +1,13 @@
1
+ import { UserError } from '@williamthorsen/nmr-core';
2
+ /**
3
+ * Returns `error` as a `UserError` with the same message, for a failure that the tool anticipates, such as a failed
4
+ * external command.
5
+ */
6
+ export declare function asUserError(error: unknown): UserError;
7
+ /** Wraps an anticipated failure as a `UserError` that reads `<message>: <cause's message>`. */
8
+ export declare function chainUserError(message: string, cause: unknown): UserError;
9
+ /**
10
+ * Prefixes an anticipated failure (see `isAnticipatedFailure`) with `<message>: ` as a `UserError`, and returns any
11
+ * other value unchanged, so that a crash keeps its own stack when rethrown.
12
+ */
13
+ export declare function prefixUserError(message: string, error: unknown): unknown;
@@ -0,0 +1,11 @@
1
+ import { isAnticipatedFailure, UserError } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
3
+ export function asUserError(error) {
4
+ return error instanceof UserError ? error : new UserError(describeError(error), { cause: error });
5
+ }
6
+ export function chainUserError(message, cause) {
7
+ return new UserError(`${message}: ${describeError(cause)}`, { cause });
8
+ }
9
+ export function prefixUserError(message, error) {
10
+ return isAnticipatedFailure(error) ? chainUserError(message, error) : error;
11
+ }
@@ -1,4 +1,4 @@
1
- import { formatErrorLine, formatStatusLine } from '@williamthorsen/nmr-core';
1
+ import { formatErrorLine, formatStatusLine, isAnticipatedFailure, } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
4
4
  import { resolveOverridePath } from "./changelogOverrides.js";
@@ -23,6 +23,9 @@ export async function validateOverridesCommand(styles, configPath, dependencies
23
23
  workspace = discover();
24
24
  }
25
25
  catch (error) {
26
+ if (!isAnticipatedFailure(error)) {
27
+ throw error;
28
+ }
26
29
  return { exitCode: 2, message: formatErrorLine(`Failed to discover workspaces: ${describeError(error)}`) };
27
30
  }
28
31
  if (workspace.kind === 'empty') {
@@ -36,6 +39,9 @@ export async function validateOverridesCommand(styles, configPath, dependencies
36
39
  inputs = buildValidationInputs(resolveReleaseTargets(userConfig, workspace), buildEntries);
37
40
  }
38
41
  catch (error) {
42
+ if (!isAnticipatedFailure(error)) {
43
+ throw error;
44
+ }
39
45
  return { exitCode: 2, message: formatErrorLine(`Failed to resolve overrides scope: ${describeError(error)}`) };
40
46
  }
41
47
  const result = validate(inputs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "16.1.0",
3
+ "version": "16.1.2",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -21,7 +21,7 @@
21
21
  "license": "ISC",
22
22
  "author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
23
23
  "sideEffects": [
24
- "./dist/esm/bin/*.js"
24
+ "./dist/esm/cli.js"
25
25
  ],
26
26
  "type": "module",
27
27
  "exports": {
@@ -52,10 +52,10 @@
52
52
  ],
53
53
  "dependencies": {
54
54
  "@williamthorsen/change-grammar": "0.3.0",
55
- "@williamthorsen/nmr-core": "0.14.0",
55
+ "@williamthorsen/nmr-core": "0.15.1",
56
56
  "@williamthorsen/toolbelt.errors": "0.9.0",
57
57
  "json-stringify-pretty-compact": "4.0.0",
58
- "semver": "7.8.5",
58
+ "semver": "7.8.6",
59
59
  "yaml": "2.9.1",
60
60
  "zod": "4.6.7"
61
61
  },
@@ -66,9 +66,9 @@
66
66
  "@williamthorsen/toolbelt.vitest": "0.13.1",
67
67
  "actionlint": "2.0.6",
68
68
  "esbuild": "0.28.2",
69
- "readyup": "0.44.0",
69
+ "readyup": "0.45.0",
70
70
  "smol-toml": "1.9.1",
71
- "vitest": "5.0.5"
71
+ "vitest": "5.1.0"
72
72
  },
73
73
  "engines": {
74
74
  "node": ">=24.16.0"