@williamthorsen/release-kit 16.1.1 → 17.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.
Files changed (116) hide show
  1. package/.meta/changelog.json +79 -0
  2. package/.readyup/kits/default.js +2 -2
  3. package/.readyup/kits/npm-auto-publish.js +1 -1
  4. package/.readyup/manifest.json +204 -16
  5. package/CHANGELOG.md +28 -0
  6. package/README.md +7 -2
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertCleanWorkingTree.js +14 -7
  9. package/dist/esm/assertReleasesOnNpm.js +11 -3
  10. package/dist/esm/assertTaggedBaseline.js +2 -1
  11. package/dist/esm/assertTarballsComplete.js +16 -5
  12. package/dist/esm/bin.d.ts +2 -0
  13. package/dist/esm/bin.js +1 -0
  14. package/dist/esm/buildChangelogEntries.js +2 -2
  15. package/dist/esm/buildDependencyGraph.js +5 -4
  16. package/dist/esm/bumpVersion.js +3 -2
  17. package/dist/esm/changelogJsonUtils.js +2 -1
  18. package/dist/esm/changelogOverrides.js +3 -2
  19. package/dist/esm/cli/commandKit.d.ts +18 -0
  20. package/dist/esm/cli/commandKit.js +27 -0
  21. package/dist/esm/cli/maintenanceCommands.d.ts +6 -0
  22. package/dist/esm/cli/maintenanceCommands.js +129 -0
  23. package/dist/esm/cli/releaseCommands.d.ts +7 -0
  24. package/dist/esm/cli/releaseCommands.js +156 -0
  25. package/dist/esm/cli.js +9 -237
  26. package/dist/esm/commitCommand.d.ts +4 -1
  27. package/dist/esm/commitCommand.js +12 -10
  28. package/dist/esm/compareVersions.js +2 -1
  29. package/dist/esm/createGithubRelease.js +7 -1
  30. package/dist/esm/createGithubReleaseCommand.d.ts +10 -4
  31. package/dist/esm/createGithubReleaseCommand.js +4 -18
  32. package/dist/esm/createTags.js +6 -4
  33. package/dist/esm/deleteFileIfExists.js +2 -1
  34. package/dist/esm/deriveDirectoryConfig.js +7 -7
  35. package/dist/esm/deriveWorkspaceConfig.js +4 -4
  36. package/dist/esm/detectPackageManager.js +1 -1
  37. package/dist/esm/enterRepoRoot.js +2 -1
  38. package/dist/esm/enumerateReleaseWindows.js +2 -2
  39. package/dist/esm/findMissingManifestPaths.js +1 -1
  40. package/dist/esm/hasPrettierConfig.js +1 -1
  41. package/dist/esm/init/initCommand.js +10 -1
  42. package/dist/esm/init/parseJsonRecord.js +1 -1
  43. package/dist/esm/init/updateTemplatesCommand.js +10 -1
  44. package/dist/esm/isPublishableManifest.js +1 -1
  45. package/dist/esm/loadChangelogOverrides.js +1 -1
  46. package/dist/esm/loadConfig.js +6 -6
  47. package/dist/esm/loadValidatedConfig.d.ts +6 -1
  48. package/dist/esm/loadValidatedConfig.js +11 -3
  49. package/dist/esm/mergeConfig.js +5 -4
  50. package/dist/esm/monorepoConfigAssertions.js +6 -5
  51. package/dist/esm/npmRegistry.js +1 -1
  52. package/dist/esm/packFromCleanCheckout.js +12 -4
  53. package/dist/esm/parseChangeRecordBlock.js +1 -1
  54. package/dist/esm/planVersionBump.js +6 -5
  55. package/dist/esm/prepareArgs.d.ts +1 -49
  56. package/dist/esm/prepareArgs.js +4 -51
  57. package/dist/esm/prepareCommand.d.ts +15 -3
  58. package/dist/esm/prepareCommand.js +26 -44
  59. package/dist/esm/prepareComposite.js +2 -1
  60. package/dist/esm/prepareRunContext.js +2 -2
  61. package/dist/esm/previewTagPrefixes.js +2 -2
  62. package/dist/esm/publish.js +7 -1
  63. package/dist/esm/publishCommand.d.ts +12 -3
  64. package/dist/esm/publishCommand.js +4 -33
  65. package/dist/esm/pushCommand.d.ts +12 -4
  66. package/dist/esm/pushCommand.js +6 -27
  67. package/dist/esm/pushRelease.js +18 -6
  68. package/dist/esm/releaseFiles.js +2 -2
  69. package/dist/esm/releasePlan.js +2 -2
  70. package/dist/esm/resolveCommandTags.d.ts +4 -4
  71. package/dist/esm/resolveCommandTags.js +8 -12
  72. package/dist/esm/resolveReleaseDeclaration.js +9 -8
  73. package/dist/esm/resolveReleaseTags.js +11 -4
  74. package/dist/esm/resolveReleaseTargets.js +2 -1
  75. package/dist/esm/{resolveStylesOrExit.d.ts → resolveStyles.d.ts} +2 -2
  76. package/dist/esm/resolveStyles.js +10 -0
  77. package/dist/esm/route.d.ts +2 -0
  78. package/dist/esm/route.js +39 -0
  79. package/dist/esm/showTagPrefixesCommand.js +6 -3
  80. package/dist/esm/sync-labels/generateCommand.js +11 -7
  81. package/dist/esm/sync-labels/initCommand.js +7 -1
  82. package/dist/esm/sync-labels/presets.js +7 -8
  83. package/dist/esm/sync-labels/resolveLabels.js +4 -3
  84. package/dist/esm/sync-labels/retiredConfig.js +2 -1
  85. package/dist/esm/sync-labels/syncCommand.js +4 -3
  86. package/dist/esm/sync-labels/templates.js +2 -2
  87. package/dist/esm/tagCommand.d.ts +8 -3
  88. package/dist/esm/tagCommand.js +2 -15
  89. package/dist/esm/typeGuards.d.ts +0 -2
  90. package/dist/esm/typeGuards.js +0 -3
  91. package/dist/esm/user-error-chaining.d.ts +13 -0
  92. package/dist/esm/user-error-chaining.js +11 -0
  93. package/dist/esm/validateConfig.js +1 -1
  94. package/dist/esm/validateOverridesCommand.js +12 -5
  95. package/docs/configuration.md +1 -1
  96. package/docs/editorial-overrides.md +2 -0
  97. package/docs/labels.md +2 -0
  98. package/docs/releasing.md +1 -1
  99. package/package.json +3 -3
  100. package/dist/esm/configFlagSchema.d.ts +0 -12
  101. package/dist/esm/configFlagSchema.js +0 -3
  102. package/dist/esm/help/initHelp.d.ts +0 -4
  103. package/dist/esm/help/initHelp.js +0 -31
  104. package/dist/esm/help/overridesHelp.d.ts +0 -4
  105. package/dist/esm/help/overridesHelp.js +0 -32
  106. package/dist/esm/help/prepareHelp.d.ts +0 -7
  107. package/dist/esm/help/prepareHelp.js +0 -30
  108. package/dist/esm/help/releaseStepsHelp.d.ts +0 -12
  109. package/dist/esm/help/releaseStepsHelp.js +0 -91
  110. package/dist/esm/help/syncLabelsHelp.d.ts +0 -8
  111. package/dist/esm/help/syncLabelsHelp.js +0 -55
  112. package/dist/esm/help/usage.d.ts +0 -2
  113. package/dist/esm/help/usage.js +0 -29
  114. package/dist/esm/resolveConfigFlag.d.ts +0 -7
  115. package/dist/esm/resolveConfigFlag.js +0 -4
  116. package/dist/esm/resolveStylesOrExit.js +0 -10
@@ -1,15 +1,12 @@
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, UserError } from '@williamthorsen/nmr-core';
4
4
  import { readReleaseTags, RELEASE_SUMMARY_FILE, resolveReleaseTagsPath } from "./releaseFiles.js";
5
- const commitFlagSchema = {
6
- isDryRun: { long: '--dry-run', type: 'boolean' },
7
- };
8
- export function commitCommand(argv) {
9
- const isDryRun = parseArgsOrExit(argv, commitFlagSchema).flags.isDryRun;
5
+ import { asUserError } from "./user-error-chaining.js";
6
+ export function commitCommand({ isDryRun }) {
10
7
  const tags = readReleaseTags();
11
8
  if (tags.length === 0) {
12
- throw new Error(`Tags file at ${resolveReleaseTagsPath()} is empty. Run \`release-kit prepare\` first.`);
9
+ throw new UserError(`Tags file at ${resolveReleaseTagsPath()} is empty. Run \`release-kit prepare\` first.`);
13
10
  }
14
11
  let summary = '';
15
12
  try {
@@ -19,7 +16,7 @@ export function commitCommand(argv) {
19
16
  if (hasErrnoCode(error, 'ENOENT')) {
20
17
  }
21
18
  else {
22
- throw error;
19
+ throw asUserError(error);
23
20
  }
24
21
  }
25
22
  const title = `release: ${tags.join(' ')}`;
@@ -42,7 +39,12 @@ export function commitCommand(argv) {
42
39
  }
43
40
  return;
44
41
  }
45
- execFileSync('git', ['add', '-A'], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
46
- execFileSync('git', ['commit', '-m', message], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
42
+ try {
43
+ execFileSync('git', ['add', '-A'], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
44
+ execFileSync('git', ['commit', '-m', message], { maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
45
+ }
46
+ catch (error) {
47
+ throw asUserError(error);
48
+ }
47
49
  console.info(`Created release commit: ${title}`);
48
50
  }
@@ -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,10 +1,16 @@
1
- import { type StreamStyles } from '@williamthorsen/nmr-core';
1
+ import type { StreamStyles } from '@williamthorsen/nmr-core';
2
+ export interface CreateGithubReleaseCommandOptions {
3
+ configPath: string | undefined;
4
+ isDryRun: boolean;
5
+ /** The tag names that `--tags` selects, or `undefined` for every release tag on HEAD. */
6
+ tags: string[] | undefined;
7
+ styles: StreamStyles;
8
+ }
2
9
  /**
3
10
  * Runs the CLI `create-github-release` command: creates GitHub Releases from `changelog.json` for the tags on HEAD, or
4
11
  * a comma-separated `--tags` subset, without publishing to npm.
5
12
  *
6
13
  * A private workspace's tag is skipped with a warning and doesn't get a Release; an all-private tag set is a clean
7
- * no-op. A project tag gets a Release whatever the root `package.json#private` says. A relative `--config` resolves
8
- * against `invocationDir`.
14
+ * no-op. A project tag gets a Release whatever the root `package.json#private` says.
9
15
  */
10
- export declare function createGithubReleaseCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
16
+ export declare function createGithubReleaseCommand({ configPath, isDryRun, tags, styles, }: CreateGithubReleaseCommandOptions): Promise<void>;
@@ -1,25 +1,12 @@
1
- import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
- import { describeError } from '@williamthorsen/toolbelt.errors';
3
- import { configFlagSchema } from "./configFlagSchema.js";
4
1
  import { createGithubReleases } from "./createGithubRelease.js";
5
2
  import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
6
3
  import { formatPrivateSkip } from "./formatPrivateSkip.js";
7
4
  import { loadUsableConfig } from "./loadValidatedConfig.js";
8
- import { parseRequestedTags } from "./parseRequestedTags.js";
9
5
  import { resolveCommandTags } from "./resolveCommandTags.js";
10
- import { resolveConfigFlag } from "./resolveConfigFlag.js";
11
- const createGithubReleaseFlagSchema = {
12
- ...configFlagSchema,
13
- isDryRun: { long: '--dry-run', type: 'boolean' },
14
- tags: { long: '--tags', type: 'string' },
15
- };
16
- export async function createGithubReleaseCommand(argv, styles, invocationDir) {
17
- const parsedArgs = parseArgsOrExit(argv, createGithubReleaseFlagSchema);
18
- const configPath = resolveConfigFlag(parsedArgs.flags.config, invocationDir);
19
- const { isDryRun } = parsedArgs.flags;
6
+ import { prefixUserError } from "./user-error-chaining.js";
7
+ export async function createGithubReleaseCommand({ configPath, isDryRun, tags, styles, }) {
20
8
  const userConfig = await loadUsableConfig(styles.stderr, configPath);
21
- const requestedTags = parseRequestedTags(parsedArgs.flags.tags);
22
- const resolvedTags = resolveCommandTags(requestedTags, userConfig);
9
+ const resolvedTags = resolveCommandTags(tags, userConfig);
23
10
  const releasableTags = resolvedTags.filter(isReleasable);
24
11
  for (const resolvedTag of resolvedTags) {
25
12
  if (!isReleasable(resolvedTag)) {
@@ -35,8 +22,7 @@ export async function createGithubReleaseCommand(argv, styles, invocationDir) {
35
22
  outcome = createGithubReleases(releasableTags, changelogJsonOutputPath, isDryRun, sectionOrder);
36
23
  }
37
24
  catch (error) {
38
- reportError(`Failed to create GitHub Releases: ${describeError(error)}`);
39
- process.exit(1);
25
+ throw prefixUserError('Failed to create GitHub Releases', error);
40
26
  }
41
27
  if (outcome.skippedTags.length === 0) {
42
28
  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,13 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
4
- import { isRecord } from "./typeGuards.js";
3
+ import { isRecord, UserError } from '@williamthorsen/nmr-core';
4
+ import { chainUserError } from "./user-error-chaining.js";
5
5
  export const DIRECTORY_VERSION_FILE = 'version.json';
6
6
  const TAG_PREFIX_PATTERN = /^[\w.-]+$/;
7
7
  export function deriveDirectoryConfig(declaredDirectory, root = process.cwd()) {
8
8
  const label = `releases.${declaredDirectory.name}`;
9
9
  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`);
10
+ throw new UserError(`${label}: path '${declaredDirectory.path}' is not a directory; declare a directory that exists in the repo`);
11
11
  }
12
12
  const versionFile = resolveVersionFile(declaredDirectory.path, root);
13
13
  assertVersionField(label, versionFile, root);
@@ -28,17 +28,17 @@ export function deriveDirectoryConfig(declaredDirectory, root = process.cwd()) {
28
28
  function assertVersionField(label, versionFile, root) {
29
29
  const absolutePath = path.resolve(root, versionFile);
30
30
  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" }`);
31
+ throw new UserError(`${label}: ${versionFile} not found; create it with the directory's current version, such as { "version": "0.0.0" }`);
32
32
  }
33
33
  let parsedJson;
34
34
  try {
35
35
  parsedJson = JSON.parse(readFileSync(absolutePath, 'utf8'));
36
36
  }
37
37
  catch (error) {
38
- throw chainError(`${label}: failed to read ${versionFile}`, error);
38
+ throw chainUserError(`${label}: failed to read ${versionFile}`, error);
39
39
  }
40
40
  if (!isRecord(parsedJson) || typeof parsedJson['version'] !== 'string') {
41
- throw new Error(`${label}: ${versionFile} does not contain a string 'version' field; add one`);
41
+ throw new UserError(`${label}: ${versionFile} does not contain a string 'version' field; add one`);
42
42
  }
43
43
  }
44
44
  function deriveTagPrefix(label, directoryPath) {
@@ -47,7 +47,7 @@ function deriveTagPrefix(label, directoryPath) {
47
47
  }
48
48
  const basename = path.posix.basename(directoryPath);
49
49
  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'`);
50
+ throw new UserError(`${label}: the directory name '${basename}' cannot form a tag prefix; set tagPrefix, such as '${basename.replaceAll(/[^\w.-]+/g, '-')}-v'`);
51
51
  }
52
52
  return `${basename}-v`;
53
53
  }
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { basename } from 'node:path';
3
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
3
+ import { isRecord, UserError } from '@williamthorsen/nmr-core';
4
4
  import { isPublishableManifest } from "./isPublishableManifest.js";
5
- import { isRecord } from "./typeGuards.js";
5
+ import { chainUserError } from "./user-error-chaining.js";
6
6
  export function deriveWorkspaceConfig(workspacePath) {
7
7
  const dir = basename(workspacePath);
8
8
  const packageJsonPath = `${workspacePath}/package.json`;
@@ -11,11 +11,11 @@ export function deriveWorkspaceConfig(workspacePath) {
11
11
  parsedJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
12
12
  }
13
13
  catch (error) {
14
- throw chainError(`Failed to read ${packageJsonPath}`, error);
14
+ throw chainUserError(`Failed to read ${packageJsonPath}`, error);
15
15
  }
16
16
  const name = isRecord(parsedJson) ? parsedJson['name'] : undefined;
17
17
  if (typeof name !== 'string' || name.length === 0) {
18
- throw new Error(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
18
+ throw new UserError(`${packageJsonPath} is missing a 'name' field (required for tag derivation).`);
19
19
  }
20
20
  const unscopedName = stripNpmScope(name);
21
21
  const isPublishable = isPublishableManifest(parsedJson);
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { isRecord } from "./typeGuards.js";
3
+ import { isRecord } from '@williamthorsen/nmr-core';
4
4
  export function detectPackageManager() {
5
5
  const packageJsonPath = join(process.cwd(), 'package.json');
6
6
  try {
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { UserError } from '@williamthorsen/nmr-core';
3
4
  import { findMonorepoRoot } from '@williamthorsen/nmr-core/workspace';
4
5
  export function enterRepoRoot(startDir = process.cwd()) {
5
6
  const invocationDir = path.resolve(startDir);
6
7
  const root = findMonorepoRoot(invocationDir) ?? findSinglePackageRoot(invocationDir);
7
8
  if (root === undefined) {
8
- throw new Error(`No repo root found from ${invocationDir}: expected a pnpm-workspace.yaml in it or any parent directory, ` +
9
+ throw new UserError(`No repo root found from ${invocationDir}: expected a pnpm-workspace.yaml in it or any parent directory, ` +
9
10
  'or a package.json in it. Run release-kit from inside a monorepo or at the root of a single-package repo.');
10
11
  }
11
12
  process.chdir(root);
@@ -1,7 +1,7 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { GIT_OUTPUT_LIMIT_BYTES } from '@williamthorsen/nmr-core';
3
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
4
3
  import { matchesTagPrefix } from "./matchesTagPrefix.js";
4
+ import { chainUserError } from "./user-error-chaining.js";
5
5
  const FIELD_SEPARATOR = '\u{1F}';
6
6
  const RECORD_SEPARATOR = '\u{1E}';
7
7
  export function enumerateReleaseWindows(options) {
@@ -117,6 +117,6 @@ function runGit(args, description) {
117
117
  }).trim();
118
118
  }
119
119
  catch (error) {
120
- throw chainError(`Failed to run ${description}`, error);
120
+ throw chainUserError(`Failed to run ${description}`, error);
121
121
  }
122
122
  }
@@ -1,4 +1,4 @@
1
- import { isRecord } from "./typeGuards.js";
1
+ import { isRecord } from '@williamthorsen/nmr-core';
2
2
  const BUILT_IN_DEV_CONDITION = 'source';
3
3
  export function findMissingManifestPaths(manifest, packedFiles, devExportConditions) {
4
4
  const packedSet = new Set(packedFiles);
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { isRecord } from "./typeGuards.js";
3
+ import { isRecord } from '@williamthorsen/nmr-core';
4
4
  export function hasPrettierConfig() {
5
5
  const cwd = process.cwd();
6
6
  for (const file of PRETTIER_CONFIG_FILES) {
@@ -1,4 +1,4 @@
1
- import { printError, printSkip, printStep, printSuccess, reportWriteResult, } from '@williamthorsen/nmr-core';
1
+ import { isAnticipatedFailure, printError, printSkip, printStep, printSuccess, reportWriteResult, } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { hasPackageJson, isGitRepo, usesPnpm } from "./checks.js";
4
4
  import { detectRepoType } from "./detectRepoType.js";
@@ -19,6 +19,9 @@ export function initCommand({ isDryRun, shouldForce, styles, shouldScaffoldConfi
19
19
  eligible = checkEligibility(styles);
20
20
  }
21
21
  catch (error) {
22
+ if (!isAnticipatedFailure(error)) {
23
+ throw error;
24
+ }
22
25
  printError(`Eligibility check failed: ${describeError(error)}`, styles.stderr);
23
26
  return 1;
24
27
  }
@@ -30,6 +33,9 @@ export function initCommand({ isDryRun, shouldForce, styles, shouldScaffoldConfi
30
33
  repoType = detectRepoType();
31
34
  }
32
35
  catch (error) {
36
+ if (!isAnticipatedFailure(error)) {
37
+ throw error;
38
+ }
33
39
  printError(`Failed to detect repo type: ${describeError(error)}`, styles.stderr);
34
40
  return 1;
35
41
  }
@@ -46,6 +52,9 @@ export function initCommand({ isDryRun, shouldForce, styles, shouldScaffoldConfi
46
52
  results = scaffoldFiles({ repoType, isDryRun, shouldOverwrite: shouldForce, publishable, shouldScaffoldConfig });
47
53
  }
48
54
  catch (error) {
55
+ if (!isAnticipatedFailure(error)) {
56
+ throw error;
57
+ }
49
58
  printError(`Failed to scaffold files: ${describeError(error)}`, styles.stderr);
50
59
  return 1;
51
60
  }
@@ -1,4 +1,4 @@
1
- import { isRecord } from "../typeGuards.js";
1
+ import { isRecord } from '@williamthorsen/nmr-core';
2
2
  export function parseJsonRecord(jsonText) {
3
3
  let parsedJson;
4
4
  try {
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
- import { printError, printSkip, printStep, printSuccess, reportTemplateUpdate, updateManagedFile, } from '@williamthorsen/nmr-core';
2
+ import { isAnticipatedFailure, printError, printSkip, printStep, printSuccess, reportTemplateUpdate, updateManagedFile, } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { readSyncLabelsWorkflowTemplate } from "../sync-labels/templates.js";
5
5
  import { detectRepoType } from "./detectRepoType.js";
@@ -18,6 +18,9 @@ export function updateTemplatesCommand({ isDryRun, styles }) {
18
18
  eligible = checkEligibility(styles);
19
19
  }
20
20
  catch (error) {
21
+ if (!isAnticipatedFailure(error)) {
22
+ throw error;
23
+ }
21
24
  printError(`Eligibility check failed: ${describeError(error)}`, styles.stderr);
22
25
  return 1;
23
26
  }
@@ -29,6 +32,9 @@ export function updateTemplatesCommand({ isDryRun, styles }) {
29
32
  repoType = detectRepoType();
30
33
  }
31
34
  catch (error) {
35
+ if (!isAnticipatedFailure(error)) {
36
+ throw error;
37
+ }
32
38
  printError(`Failed to detect repo type: ${describeError(error)}`, styles.stderr);
33
39
  return 1;
34
40
  }
@@ -43,6 +49,9 @@ export function updateTemplatesCommand({ isDryRun, styles }) {
43
49
  results = files.map(([filePath, content]) => updateManagedFile(filePath, content, { isDryRun }));
44
50
  }
45
51
  catch (error) {
52
+ if (!isAnticipatedFailure(error)) {
53
+ throw error;
54
+ }
46
55
  printError(`Failed to render workflows: ${describeError(error)}`, styles.stderr);
47
56
  return 1;
48
57
  }
@@ -1,4 +1,4 @@
1
- import { isRecord } from "./typeGuards.js";
1
+ import { isRecord } from '@williamthorsen/nmr-core';
2
2
  export function isPublishableManifest(manifest) {
3
3
  const privateField = isRecord(manifest) ? manifest['private'] : undefined;
4
4
  return privateField === undefined || privateField === false;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { isRecord } from '@williamthorsen/nmr-core';
2
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
- import { isRecord } from "./typeGuards.js";
4
4
  const VALID_AUDIENCE_VALUES = new Set(['all', 'dev', 'skip']);
5
5
  const V1_SUPPORTED_AUDIENCE_VALUES = new Set(['skip']);
6
6
  const SCHEMA_KEY = '$schema';
@@ -9,8 +9,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { existsSync, readFileSync } from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { pathToFileURL } from 'node:url';
12
- import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
13
- import { isRecord } from "./typeGuards.js";
12
+ import { isRecord, UserError } from '@williamthorsen/nmr-core';
13
+ import { chainUserError } from "./user-error-chaining.js";
14
14
  export const ROOT_PACKAGE_JSON_PATH = 'package.json';
15
15
  export function readRootPackageVersion() {
16
16
  const absolutePath = path.resolve(process.cwd(), ROOT_PACKAGE_JSON_PATH);
@@ -22,14 +22,14 @@ export function readRootPackageVersion() {
22
22
  contents = readFileSync(absolutePath, 'utf8');
23
23
  }
24
24
  catch (error) {
25
- throw chainError(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}`, error);
25
+ throw chainUserError(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}`, error);
26
26
  }
27
27
  let parsedJson;
28
28
  try {
29
29
  parsedJson = JSON.parse(contents);
30
30
  }
31
31
  catch (error) {
32
- throw chainError(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}`, error);
32
+ throw chainUserError(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}`, error);
33
33
  }
34
34
  if (!isRecord(parsedJson)) {
35
35
  return { doesExist: true, version: undefined };
@@ -43,12 +43,12 @@ export async function loadConfig(configPath) {
43
43
  if (configPath === undefined) {
44
44
  return undefined;
45
45
  }
46
- throw new Error(`Config file not found: ${absoluteConfigPath}`);
46
+ throw new UserError(`Config file not found: ${absoluteConfigPath}`);
47
47
  }
48
48
  const importedModule = await import(__rewriteRelativeImportExtension(pathToFileURL(absoluteConfigPath).href));
49
49
  const resolvedConfig = isRecord(importedModule) ? (importedModule['default'] ?? importedModule['config']) : undefined;
50
50
  if (resolvedConfig === undefined) {
51
- throw new Error('Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)');
51
+ throw new UserError('Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)');
52
52
  }
53
53
  return resolvedConfig;
54
54
  }
@@ -12,6 +12,11 @@ export type ConfigProblem = {
12
12
  kind: 'validation';
13
13
  errors: string[];
14
14
  };
15
+ /** Reports a config file that a CLI command cannot use, carrying the problem for `reportConfigProblem` to render. */
16
+ export declare class ConfigProblemError extends Error {
17
+ readonly problem: ConfigProblem;
18
+ constructor(problem: ConfigProblem);
19
+ }
15
20
  /**
16
21
  * Outcome of loading and validating the consumer config file. `missing` is not an error here; the caller decides
17
22
  * whether an absent config file is one.
@@ -47,7 +52,7 @@ export declare function loadValidatedConfig(configPath?: string): Promise<LoadVa
47
52
  * Loads and validates the config for a CLI command, returning it, or `undefined` when the default config file does
48
53
  * not exist.
49
54
  *
50
- * Reports an unusable config to stderr and exits 1, whether the file failed to load or failed validation, and
55
+ * Throws a `ConfigProblemError` for an unusable config, whether the file failed to load or failed validation, and
51
56
  * whether it was named by the caller or the default one. Validation warnings print to stderr.
52
57
  */
53
58
  export declare function loadUsableConfig(stderrStyle: OutputStyle, configPath?: string): Promise<ReleaseKitConfig | undefined>;
@@ -1,7 +1,16 @@
1
1
  import { formatStatusLine, printError, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
+ import { BIN } from "./bin.js";
3
4
  import { CONFIG_FILE_PATH, loadConfig } from "./loadConfig.js";
4
5
  import { validateConfig } from "./validateConfig.js";
6
+ export class ConfigProblemError extends Error {
7
+ problem;
8
+ constructor(problem) {
9
+ super('Invalid config');
10
+ this.name = 'ConfigProblemError';
11
+ this.problem = problem;
12
+ }
13
+ }
5
14
  export async function loadValidatedConfig(configPath) {
6
15
  const configFilePath = configPath ?? CONFIG_FILE_PATH;
7
16
  let rawConfig;
@@ -23,8 +32,7 @@ export async function loadValidatedConfig(configPath) {
23
32
  export async function loadUsableConfig(stderrStyle, configPath) {
24
33
  const result = await loadValidatedConfig(configPath);
25
34
  if (result.status === 'invalid') {
26
- reportConfigProblem(result.problem, stderrStyle);
27
- process.exit(1);
35
+ throw new ConfigProblemError(result.problem);
28
36
  }
29
37
  if (result.status === 'missing') {
30
38
  return undefined;
@@ -34,7 +42,7 @@ export async function loadUsableConfig(stderrStyle, configPath) {
34
42
  }
35
43
  export function reportConfigProblem(problem, stderrStyle) {
36
44
  if (problem.kind === 'load') {
37
- reportError(`Failed to load config: ${problem.message}`);
45
+ reportError(BIN, `Failed to load config: ${problem.message}`);
38
46
  return;
39
47
  }
40
48
  process.stderr.write('Invalid config:\n');
@@ -1,3 +1,4 @@
1
+ import { UserError } from '@williamthorsen/nmr-core';
1
2
  import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_PROJECT_TAG_PREFIX, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from "./defaults.js";
2
3
  import { deriveDirectoryConfig } from "./deriveDirectoryConfig.js";
3
4
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
@@ -45,10 +46,10 @@ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
45
46
  }
46
47
  export function mergeSinglePackageConfig(userConfig) {
47
48
  if (userConfig?.project !== undefined) {
48
- throw new Error('project block is not supported in single-package mode');
49
+ throw new UserError('project block is not supported in single-package mode');
49
50
  }
50
51
  if (userConfig?.releases !== undefined) {
51
- throw new Error('releases block is not supported in single-package mode');
52
+ throw new UserError('releases block is not supported in single-package mode');
52
53
  }
53
54
  const workTypes = resolveWorkTypes(userConfig?.workTypes);
54
55
  const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
@@ -104,10 +105,10 @@ function resolveProjectConfig(declaredComposite, rootPackage, workspaces) {
104
105
  }
105
106
  const subject = declaredComposite.name === undefined ? 'project block' : `composite release '${declaredComposite.name}'`;
106
107
  if (rootPackage === undefined || !rootPackage.doesExist) {
107
- throw new Error(`${subject} requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
108
+ throw new UserError(`${subject} requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
108
109
  }
109
110
  if (rootPackage.version === undefined) {
110
- throw new Error(`${subject} requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to the root package.json`);
111
+ throw new UserError(`${subject} requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to the root package.json`);
111
112
  }
112
113
  return {
113
114
  paths: declaredComposite.paths ?? workspaces.flatMap((workspace) => workspace.paths),
@@ -1,9 +1,10 @@
1
+ import { UserError } from '@williamthorsen/nmr-core';
1
2
  import { ROOT_PACKAGE_JSON_PATH } from "./loadConfig.js";
2
3
  import { getTargetName, normalizeWorkspacePath } from "./resolveReleaseDeclaration.js";
3
4
  export function assertLegacyIdentityDoesNotMatchCurrent(dir, currentName, currentTagPrefix, legacyIdentities) {
4
5
  for (const identity of legacyIdentities) {
5
6
  if (identity.name === currentName && identity.tagPrefix === currentTagPrefix) {
6
- throw new Error(`Workspace '${dir}': legacyIdentities must not match the current identity ` +
7
+ throw new UserError(`Workspace '${dir}': legacyIdentities must not match the current identity ` +
7
8
  `(name='${currentName}', tagPrefix='${currentTagPrefix}'). ` +
8
9
  'The current identity is always searched; listing it again is a no-op.');
9
10
  }
@@ -31,7 +32,7 @@ export function assertNoSharedVersionFiles(workspaces, directories, project) {
31
32
  for (const [index, directory] of directories.entries()) {
32
33
  const peer = owners.slice(index + 1).find((owner) => owner.versionFiles.includes(directory.versionFile));
33
34
  if (peer !== undefined) {
34
- throw new Error(`directory release '${directory.name}' and ${peer.label} share the version file '${directory.versionFile}', ` +
35
+ throw new UserError(`directory release '${directory.name}' and ${peer.label} share the version file '${directory.versionFile}', ` +
35
36
  "so each release would overwrite the other's version and changelog; declare only one of them");
36
37
  }
37
38
  }
@@ -84,7 +85,7 @@ export function assertNoTagPrefixCollisions(workspaces, directories, retiredPack
84
85
  if (a === undefined || b === undefined || a.owner === b.owner)
85
86
  continue;
86
87
  if (isPrefixCollision(a.prefix, b.prefix)) {
87
- throw new Error(`Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). ` +
88
+ throw new UserError(`Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). ` +
88
89
  'One prefix is identical to or a strict prefix of the other; ' +
89
90
  'a tag under the longer prefix can then match the shorter one and be counted among its releases.');
90
91
  }
@@ -99,7 +100,7 @@ export function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredP
99
100
  for (const retiredPackage of retiredPackages) {
100
101
  const activeWorkspace = workspaceByDerivedPrefix.get(retiredPackage.tagPrefix);
101
102
  if (activeWorkspace !== undefined) {
102
- throw new Error(`retiredPackages: tagPrefix '${retiredPackage.tagPrefix}' collides with active workspace '${activeWorkspace.dir}' ` +
103
+ throw new UserError(`retiredPackages: tagPrefix '${retiredPackage.tagPrefix}' collides with active workspace '${activeWorkspace.dir}' ` +
103
104
  `(derived prefix '${activeWorkspace.tagPrefix}'). A retired package's tagPrefix cannot belong to an active workspace.`);
104
105
  }
105
106
  }
@@ -117,7 +118,7 @@ export function assertUniqueTagPrefixes(workspaces) {
117
118
  }
118
119
  for (const [prefix, paths] of pathsByPrefix) {
119
120
  if (paths.length > 1) {
120
- throw new Error(`Duplicate tag prefix '${prefix}' for workspaces: ${paths.join(', ')}`);
121
+ throw new UserError(`Duplicate tag prefix '${prefix}' for workspaces: ${paths.join(', ')}`);
121
122
  }
122
123
  }
123
124
  }
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { isRecord } from "./typeGuards.js";
2
+ import { isRecord } from '@williamthorsen/nmr-core';
3
3
  export const AUTH_ERROR_CODES = new Set(['E401', 'ENEEDAUTH']);
4
4
  export const UNREACHABLE_ERROR_CODES = new Set([
5
5
  'EAI_AGAIN',