@williamthorsen/release-kit 10.3.1 → 10.4.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 (46) hide show
  1. package/.readyup/kits/default.js +2 -2
  2. package/.readyup/kits/npm-auto-publish.js +1 -1
  3. package/.readyup/manifest.json +43 -4
  4. package/CHANGELOG.md +50 -0
  5. package/README.md +36 -9
  6. package/dist/esm/bin/release-kit.js +6 -5
  7. package/dist/esm/buildChangelogEntries.js +2 -1
  8. package/dist/esm/buildDependencyGraph.js +3 -6
  9. package/dist/esm/changelogJsonFile.js +2 -1
  10. package/dist/esm/changelogOverrides.js +3 -2
  11. package/dist/esm/checkWorkTypesDrift.js +5 -4
  12. package/dist/esm/commitCommand.js +2 -2
  13. package/dist/esm/createGithubReleaseCommand.js +2 -1
  14. package/dist/esm/createTags.js +2 -2
  15. package/dist/esm/deleteFileIfExists.js +2 -1
  16. package/dist/esm/deriveWorkspaceConfig.js +2 -2
  17. package/dist/esm/discoverWorkspaces.js +2 -1
  18. package/dist/esm/getCommitsSinceTarget.js +3 -5
  19. package/dist/esm/init/initCommand.js +4 -3
  20. package/dist/esm/init/scaffold.js +2 -1
  21. package/dist/esm/loadConfig.js +9 -5
  22. package/dist/esm/loadValidatedConfig.js +2 -1
  23. package/dist/esm/planReleaseNotesPreviews.js +2 -1
  24. package/dist/esm/planVersionBump.js +3 -6
  25. package/dist/esm/prepareCommand.js +8 -7
  26. package/dist/esm/previewTagPrefixes.js +2 -1
  27. package/dist/esm/publishCommand.js +3 -2
  28. package/dist/esm/pushCommand.js +2 -1
  29. package/dist/esm/readCurrentVersion.js +2 -1
  30. package/dist/esm/releaseFiles.js +4 -8
  31. package/dist/esm/releasePrepareMono.js +2 -5
  32. package/dist/esm/releasePrepareProject.js +1 -1
  33. package/dist/esm/resolveCommandTags.js +3 -2
  34. package/dist/esm/resolveReleaseNotesConfig.js +2 -1
  35. package/dist/esm/sync-labels/generateCommand.js +3 -2
  36. package/dist/esm/sync-labels/initCommand.js +2 -1
  37. package/dist/esm/sync-labels/presets.js +2 -2
  38. package/dist/esm/sync-labels/syncCommand.js +2 -1
  39. package/dist/esm/syncWorkTypes.js +5 -4
  40. package/dist/esm/tagCommand.js +2 -1
  41. package/dist/esm/types.d.ts +3 -0
  42. package/dist/esm/types.js +1 -0
  43. package/dist/esm/validateOverridesCommand.js +6 -9
  44. package/dist/esm/workTypesUtils.d.ts +0 -1
  45. package/dist/esm/workTypesUtils.js +0 -3
  46. package/package.json +16 -4
@@ -9,6 +9,7 @@ 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';
12
13
  import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_PROJECT_TAG_PREFIX, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from "./defaults.js";
13
14
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
14
15
  import { isRecord } from "./typeGuards.js";
@@ -23,14 +24,14 @@ export function readRootPackageVersion() {
23
24
  contents = readFileSync(absolutePath, 'utf8');
24
25
  }
25
26
  catch (error) {
26
- throw new Error(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
27
+ throw chainError(`Failed to read root ${ROOT_PACKAGE_JSON_PATH}`, error);
27
28
  }
28
29
  let parsed;
29
30
  try {
30
31
  parsed = JSON.parse(contents);
31
32
  }
32
33
  catch (error) {
33
- throw new Error(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
34
+ throw chainError(`Failed to parse root ${ROOT_PACKAGE_JSON_PATH}`, error);
34
35
  }
35
36
  if (!isRecord(parsed)) {
36
37
  return { exists: true, version: undefined };
@@ -72,7 +73,7 @@ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
72
73
  if (userConfig?.retiredPackages !== undefined) {
73
74
  assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
74
75
  }
75
- const project = resolveProjectConfig(userConfig?.project, rootPackage);
76
+ const project = resolveProjectConfig(userConfig?.project, rootPackage, workspaces);
76
77
  const workTypes = resolveWorkTypes(userConfig?.workTypes);
77
78
  const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
78
79
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
@@ -160,7 +161,7 @@ function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages
160
161
  }
161
162
  }
162
163
  }
163
- function resolveProjectConfig(userProject, rootPackage) {
164
+ function resolveProjectConfig(userProject, rootPackage, workspaces) {
164
165
  if (userProject === undefined) {
165
166
  return undefined;
166
167
  }
@@ -170,7 +171,10 @@ function resolveProjectConfig(userProject, rootPackage) {
170
171
  if (rootPackage.version === undefined) {
171
172
  throw new Error(`project block requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to your root package.json`);
172
173
  }
173
- return { tagPrefix: userProject.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX };
174
+ return {
175
+ paths: userProject.paths ?? workspaces.flatMap((workspace) => workspace.paths),
176
+ tagPrefix: userProject.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX,
177
+ };
174
178
  }
175
179
  function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
176
180
  const sources = [];
@@ -1,4 +1,5 @@
1
1
  import { reportError } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
3
  import { loadConfig } from "./loadConfig.js";
3
4
  import { validateConfig } from "./validateConfig.js";
4
5
  export async function loadValidatedConfig() {
@@ -7,7 +8,7 @@ export async function loadValidatedConfig() {
7
8
  raw = await loadConfig();
8
9
  }
9
10
  catch (error) {
10
- const message = error instanceof Error ? error.message : String(error);
11
+ const message = describeError(error);
11
12
  reportError(`Failed to load config: ${message}`);
12
13
  return { status: 'invalid' };
13
14
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { describeError } from '@williamthorsen/toolbelt.errors';
3
4
  import { extractVersion } from "./changelogJsonUtils.js";
4
5
  import { renderInjectedReadmeFromEntries } from "./injectReleaseNotesIntoReadme.js";
5
6
  export function planReleaseNotesPreviews(options) {
@@ -36,7 +37,7 @@ function readWorkspaceReadme(readmePath, warnings) {
36
37
  return { content: readFileSync(readmePath, 'utf8'), exists: true, unreadable: false };
37
38
  }
38
39
  catch (error) {
39
- const message = error instanceof Error ? error.message : String(error);
40
+ const message = describeError(error);
40
41
  warnings.push(`failed to read ${readmePath}: ${message}; skipping injected-README preview`);
41
42
  return { content: '', exists: false, unreadable: true };
42
43
  }
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
2
3
  import { bumpVersion } from "./bumpVersion.js";
3
4
  function isPackageJson(value) {
4
5
  return typeof value === 'object' && value !== null && 'version' in value && typeof value.version === 'string';
@@ -34,18 +35,14 @@ function readPackageJson(filePath) {
34
35
  content = readFileSync(filePath, 'utf8');
35
36
  }
36
37
  catch (error) {
37
- throw new Error(`Failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
38
- cause: error,
39
- });
38
+ throw chainError(`Failed to read ${filePath}`, error);
40
39
  }
41
40
  let parsed;
42
41
  try {
43
42
  parsed = JSON.parse(content);
44
43
  }
45
44
  catch (error) {
46
- throw new Error(`Failed to parse JSON in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, {
47
- cause: error,
48
- });
45
+ throw chainError(`Failed to parse JSON in ${filePath}`, error);
49
46
  }
50
47
  if (!isPackageJson(parsed)) {
51
48
  throw new Error(`No valid 'version' field found in ${filePath}`);
@@ -1,5 +1,6 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
3
+ import { describeError } from '@williamthorsen/toolbelt.errors';
3
4
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
4
5
  import { buildDependencyGraph } from "./buildDependencyGraph.js";
5
6
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
@@ -87,7 +88,7 @@ export async function prepareCommand(argv) {
87
88
  assertCleanWorkingTree();
88
89
  }
89
90
  catch (error) {
90
- reportError(error instanceof Error ? error.message : String(error));
91
+ reportError(describeError(error));
91
92
  process.exit(1);
92
93
  }
93
94
  }
@@ -97,7 +98,7 @@ export async function prepareCommand(argv) {
97
98
  discoveredPaths = await discoverWorkspaces();
98
99
  }
99
100
  catch (error) {
100
- reportError(`Failed to discover workspaces: ${error instanceof Error ? error.message : String(error)}`);
101
+ reportError(`Failed to discover workspaces: ${describeError(error)}`);
101
102
  process.exit(1);
102
103
  }
103
104
  if (discoveredPaths === undefined) {
@@ -127,7 +128,7 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
127
128
  config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
128
129
  }
129
130
  catch (error) {
130
- reportError(`Failed to resolve workspaces: ${error instanceof Error ? error.message : String(error)}`);
131
+ reportError(`Failed to resolve workspaces: ${describeError(error)}`);
131
132
  process.exit(1);
132
133
  }
133
134
  if (setVersion !== undefined && config.project !== undefined) {
@@ -190,7 +191,7 @@ async function loadAndValidateConfig() {
190
191
  rawConfig = await loadConfig();
191
192
  }
192
193
  catch (error) {
193
- reportError(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`);
194
+ reportError(`Failed to load config: ${describeError(error)}`);
194
195
  process.exit(1);
195
196
  }
196
197
  if (rawConfig === undefined) {
@@ -215,7 +216,7 @@ function runAndReport(computePlan, dryRun) {
215
216
  plan = computePlan();
216
217
  }
217
218
  catch (error) {
218
- reportError(error instanceof Error ? error.message : String(error));
219
+ reportError(describeError(error));
219
220
  process.stderr.write('No files were written; the working tree is unchanged.\n');
220
221
  process.exit(1);
221
222
  }
@@ -228,7 +229,7 @@ function runAndReport(computePlan, dryRun) {
228
229
  applyReleasePlan(plan);
229
230
  }
230
231
  catch (error) {
231
- reportError(error instanceof Error ? error.message : String(error));
232
+ reportError(describeError(error));
232
233
  process.exit(1);
233
234
  }
234
235
  const formatError = runFormatCommand(plan.formatCommand);
@@ -249,7 +250,7 @@ function runFormatCommand(formatCommand) {
249
250
  return undefined;
250
251
  }
251
252
  catch (error) {
252
- return error instanceof Error ? error.message : String(error);
253
+ return describeError(error);
253
254
  }
254
255
  }
255
256
  function reportPlanFiles(plan, dryRun) {
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { basename } from 'node:path';
3
3
  import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
4
+ import { describeError } from '@williamthorsen/toolbelt.errors';
4
5
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
5
6
  import { detectUndeclaredTagPrefixes } from "./detectUndeclaredTagPrefixes.js";
6
7
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
@@ -49,7 +50,7 @@ function buildPreviewRow(workspacePath, overridesByDir) {
49
50
  derivedPrefix = deriveWorkspaceConfig(workspacePath).tagPrefix;
50
51
  }
51
52
  catch (error) {
52
- derivationError = error instanceof Error ? error.message : String(error);
53
+ derivationError = describeError(error);
53
54
  }
54
55
  const derivedTagCount = derivedPrefix === null ? 0 : countTagsMatching(derivedPrefix);
55
56
  const declaredIdentities = overridesByDir.get(dir) ?? [];
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
4
+ import { describeError } from '@williamthorsen/toolbelt.errors';
4
5
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
5
6
  import { detectPackageManager } from "./detectPackageManager.js";
6
7
  import { formatPrivateSkip } from "./formatPrivateSkip.js";
@@ -23,7 +24,7 @@ export async function publishCommand(argv) {
23
24
  assertCleanWorkingTree();
24
25
  }
25
26
  catch (error) {
26
- reportError(error instanceof Error ? error.message : String(error));
27
+ reportError(describeError(error));
27
28
  process.exit(1);
28
29
  }
29
30
  }
@@ -73,7 +74,7 @@ export async function publishCommand(argv) {
73
74
  console.warn(` ${t}`);
74
75
  }
75
76
  }
76
- reportError(error instanceof Error ? error.message : String(error));
77
+ reportError(describeError(error));
77
78
  process.exit(1);
78
79
  }
79
80
  }
@@ -1,4 +1,5 @@
1
1
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
3
  import { parseRequestedTags } from "./parseRequestedTags.js";
3
4
  import { pushRelease } from "./pushRelease.js";
4
5
  import { resolveCommandTags } from "./resolveCommandTags.js";
@@ -34,7 +35,7 @@ export async function pushCommand(argv) {
34
35
  }
35
36
  }
36
37
  catch (error) {
37
- reportError(error instanceof Error ? error.message : String(error));
38
+ reportError(describeError(error));
38
39
  process.exit(1);
39
40
  }
40
41
  }
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
3
  function hasVersionField(value) {
3
4
  return typeof value === 'object' && value !== null && 'version' in value && typeof value.version === 'string';
4
5
  }
@@ -11,7 +12,7 @@ export function readCurrentVersion(filePath) {
11
12
  }
12
13
  }
13
14
  catch (error) {
14
- console.warn(`Failed to read current version from ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
15
+ console.warn(`Failed to read current version from ${filePath}: ${describeError(error)}`);
15
16
  }
16
17
  return undefined;
17
18
  }
@@ -1,5 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
+ import { hasErrnoCode } from '@williamthorsen/nmr-core';
4
+ import { describeError } from '@williamthorsen/toolbelt.errors';
3
5
  export const RELEASE_TAGS_FILE = 'tmp/.release-tags';
4
6
  export const RELEASE_SUMMARY_FILE = 'tmp/.release-summary';
5
7
  export function resolveReleaseTagsPath() {
@@ -20,15 +22,9 @@ export function readReleaseTags() {
20
22
  }
21
23
  function describeTagsReadFailure(error) {
22
24
  const tagsPath = resolveReleaseTagsPath();
23
- if (getErrnoCode(error) === 'ENOENT') {
25
+ if (hasErrnoCode(error, 'ENOENT')) {
24
26
  return `No tags file found at ${tagsPath}. Run \`release-kit prepare\` first.`;
25
27
  }
26
- const detail = error instanceof Error ? error.message : String(error);
28
+ const detail = describeError(error);
27
29
  return `Cannot read the tags file at ${tagsPath}: ${detail}`;
28
30
  }
29
- function getErrnoCode(error) {
30
- if (error instanceof Error && 'code' in error && typeof error.code === 'string') {
31
- return error.code;
32
- }
33
- return undefined;
34
- }
@@ -1,4 +1,5 @@
1
1
  import { join as joinPath } from 'node:path';
2
+ import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
2
3
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
3
4
  import { buildDependencyGraph } from "./buildDependencyGraph.js";
4
5
  import { buildEmptyReleaseEntry } from "./buildEmptyReleaseEntry.js";
@@ -402,16 +403,12 @@ function planFormatCommand(config, tags, modifiedFiles) {
402
403
  function findWorkspace(workspaces, dir) {
403
404
  return workspaces.find((w) => w.dir === dir);
404
405
  }
405
- function wrapStageError(stageLabel, error) {
406
- const message = error instanceof Error ? error.message : String(error);
407
- return new Error(`${stageLabel}: ${message}`, { cause: error });
408
- }
409
406
  function tryStage(stageLabel, fn) {
410
407
  try {
411
408
  return fn();
412
409
  }
413
410
  catch (error) {
414
- throw wrapStageError(stageLabel, error);
411
+ throw chainError(stageLabel, error);
415
412
  }
416
413
  }
417
414
  function workspaceStageLabel(dir) {
@@ -25,7 +25,7 @@ export function releasePrepareProject(args) {
25
25
  const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
26
26
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
27
27
  const breakingPolicies = config.breakingPolicies ?? DEFAULT_BREAKING_POLICIES;
28
- const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
28
+ const contributingPaths = project.paths;
29
29
  const { tag, commits } = getCommitsSinceTarget([project.tagPrefix], contributingPaths);
30
30
  const since = tag === undefined ? '(no previous release found)' : `since ${tag}`;
31
31
  const collector = createPolicyViolationCollector();
@@ -1,4 +1,5 @@
1
1
  import { reportError } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
3
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
3
4
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
4
5
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
@@ -8,7 +9,7 @@ export async function resolveCommandTags(tags) {
8
9
  discoveredPaths = await discoverWorkspaces();
9
10
  }
10
11
  catch (error) {
11
- reportError(`Failed to discover workspaces: ${error instanceof Error ? error.message : String(error)}`);
12
+ reportError(`Failed to discover workspaces: ${describeError(error)}`);
12
13
  process.exit(1);
13
14
  }
14
15
  let workspaces;
@@ -22,7 +23,7 @@ export async function resolveCommandTags(tags) {
22
23
  }
23
24
  }
24
25
  catch (error) {
25
- reportError(`Failed to resolve workspaces: ${error instanceof Error ? error.message : String(error)}`);
26
+ reportError(`Failed to resolve workspaces: ${describeError(error)}`);
26
27
  process.exit(1);
27
28
  }
28
29
  let resolvedTags;
@@ -1,4 +1,5 @@
1
1
  import { reportError } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
3
  import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
3
4
  import { loadConfig, resolveWorkTypes } from "./loadConfig.js";
4
5
  import { validateConfig } from "./validateConfig.js";
@@ -9,7 +10,7 @@ export async function resolveReleaseNotesConfig(options = {}) {
9
10
  rawConfig = await loadConfig();
10
11
  }
11
12
  catch (error) {
12
- const message = error instanceof Error ? error.message : String(error);
13
+ const message = describeError(error);
13
14
  if (strictLoad) {
14
15
  reportError(`Failed to load config: ${message}`);
15
16
  process.exit(1);
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { reportError } from '@williamthorsen/nmr-core';
4
+ import { describeError } from '@williamthorsen/toolbelt.errors';
4
5
  import { stringify } from 'yaml';
5
6
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
6
7
  import { loadValidatedConfig } from "../loadValidatedConfig.js";
@@ -46,7 +47,7 @@ export async function generateCommand({ check = false } = {}) {
46
47
  labels = resolveLabels(repoLabels);
47
48
  }
48
49
  catch (error) {
49
- const message = error instanceof Error ? error.message : String(error);
50
+ const message = describeError(error);
50
51
  reportError(`Failed to resolve labels: ${message}`);
51
52
  return 1;
52
53
  }
@@ -64,7 +65,7 @@ export async function generateCommand({ check = false } = {}) {
64
65
  writeFileSync(LABELS_OUTPUT_PATH, content, 'utf8');
65
66
  }
66
67
  catch (error) {
67
- const message = error instanceof Error ? error.message : String(error);
68
+ const message = describeError(error);
68
69
  reportError(`Failed to write ${LABELS_OUTPUT_PATH}: ${message}`);
69
70
  return 1;
70
71
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { reportWriteResult, writeFileWithCheck } from '@williamthorsen/nmr-core';
3
+ import { describeError } from '@williamthorsen/toolbelt.errors';
3
4
  import { discoverWorkspaces } from "../discoverWorkspaces.js";
4
5
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
5
6
  import { loadValidatedConfig } from "../loadValidatedConfig.js";
@@ -20,7 +21,7 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
20
21
  workspacePaths = await discoverWorkspaces();
21
22
  }
22
23
  catch (error) {
23
- const message = error instanceof Error ? error.message : String(error);
24
+ const message = describeError(error);
24
25
  process.stderr.write(` Failed to discover workspaces: ${message}\n`);
25
26
  return 1;
26
27
  }
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { findPackageRoot } from '@williamthorsen/nmr-core';
5
+ import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
5
6
  import { parse } from 'yaml';
6
7
  import { isRecord } from "../typeGuards.js";
7
8
  export function hashPresetFile(presetName) {
@@ -22,8 +23,7 @@ export function loadPreset(presetName) {
22
23
  content = readFileSync(presetPath, 'utf8');
23
24
  }
24
25
  catch (error) {
25
- const message = error instanceof Error ? error.message : String(error);
26
- throw new Error(`Failed to read preset "${presetName}": ${message}`, { cause: error });
26
+ throw chainError(`Failed to read preset "${presetName}"`, error);
27
27
  }
28
28
  const parsed = parse(content);
29
29
  if (!Array.isArray(parsed)) {
@@ -1,6 +1,7 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { GIT_OUTPUT_LIMIT, reportError } from '@williamthorsen/nmr-core';
4
+ import { describeError } from '@williamthorsen/toolbelt.errors';
4
5
  import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
5
6
  const WORKFLOW_FILE = '.github/workflows/sync-labels.yaml';
6
7
  function checkGhAvailable() {
@@ -28,7 +29,7 @@ export function syncLabelsCommand() {
28
29
  execSync('gh workflow run sync-labels.yaml', { stdio: 'inherit' });
29
30
  }
30
31
  catch (error) {
31
- const message = error instanceof Error ? error.message : String(error);
32
+ const message = describeError(error);
32
33
  reportError(`Failed to trigger workflow: ${message}`);
33
34
  return 1;
34
35
  }
@@ -2,9 +2,10 @@ import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { formatErrorLine } from '@williamthorsen/nmr-core';
5
+ import { describeError } from '@williamthorsen/toolbelt.errors';
5
6
  import { UPSTREAM_WORK_TYPES_URL } from "./checkWorkTypesDrift.js";
6
7
  import { isRecord } from "./typeGuards.js";
7
- import { buildFetchInit, errorMessage, hasExpectedTopLevelShape } from "./workTypesUtils.js";
8
+ import { buildFetchInit, hasExpectedTopLevelShape } from "./workTypesUtils.js";
8
9
  function resolveDefaultLocalPath() {
9
10
  const moduleDir = dirname(fileURLToPath(import.meta.url));
10
11
  return resolve(moduleDir, 'work-types.json');
@@ -35,7 +36,7 @@ export async function syncWorkTypes(dependencies = {}) {
35
36
  catch (error) {
36
37
  return {
37
38
  exitCode: 2,
38
- message: formatErrorLine(`Failed to fetch upstream work-types.json: ${errorMessage(error)}`),
39
+ message: formatErrorLine(`Failed to fetch upstream work-types.json: ${describeError(error)}`),
39
40
  };
40
41
  }
41
42
  if (!response.ok) {
@@ -52,7 +53,7 @@ export async function syncWorkTypes(dependencies = {}) {
52
53
  catch (error) {
53
54
  return {
54
55
  exitCode: 3,
55
- message: `Upstream work-types.json is not valid JSON: ${errorMessage(error)}`,
56
+ message: `Upstream work-types.json is not valid JSON: ${describeError(error)}`,
56
57
  };
57
58
  }
58
59
  if (!hasExpectedTopLevelShape(upstreamJson)) {
@@ -83,7 +84,7 @@ export async function syncWorkTypes(dependencies = {}) {
83
84
  catch (error) {
84
85
  return {
85
86
  exitCode: 2,
86
- message: formatErrorLine(`Failed to write ${localPath}: ${errorMessage(error)}`),
87
+ message: formatErrorLine(`Failed to write ${localPath}: ${describeError(error)}`),
87
88
  };
88
89
  }
89
90
  return {
@@ -1,4 +1,5 @@
1
1
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
3
  import { createTags } from "./createTags.js";
3
4
  const tagFlagSchema = {
4
5
  dryRun: { long: '--dry-run', type: 'boolean' },
@@ -10,7 +11,7 @@ export function tagCommand(argv) {
10
11
  createTags({ dryRun, noGitChecks });
11
12
  }
12
13
  catch (error) {
13
- reportError(error instanceof Error ? error.message : String(error));
14
+ reportError(describeError(error));
14
15
  process.exit(1);
15
16
  }
16
17
  }
@@ -33,6 +33,7 @@ export interface ReleaseNotesConfig {
33
33
  shouldInjectIntoReadme: boolean;
34
34
  }
35
35
  export interface ResolvedProjectConfig {
36
+ paths: string[];
36
37
  tagPrefix: string;
37
38
  }
38
39
  export interface PropagationSource {
@@ -142,6 +143,7 @@ export declare const workspaceOverrideSchema: z.ZodObject<{
142
143
  }, z.core.$strict>;
143
144
  export type WorkspaceOverride = z.infer<typeof workspaceOverrideSchema>;
144
145
  export declare const projectConfigSchema: z.ZodObject<{
146
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
145
147
  tagPrefix: z.ZodOptional<z.ZodString>;
146
148
  }, z.core.$strict>;
147
149
  export type ProjectConfig = z.infer<typeof projectConfigSchema>;
@@ -193,6 +195,7 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
193
195
  cliffConfigPath: z.ZodOptional<z.ZodString>;
194
196
  formatCommand: z.ZodOptional<z.ZodString>;
195
197
  project: z.ZodOptional<z.ZodObject<{
198
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
196
199
  tagPrefix: z.ZodOptional<z.ZodString>;
197
200
  }, z.core.$strict>>;
198
201
  releaseNotes: z.ZodOptional<z.ZodObject<{
package/dist/esm/types.js CHANGED
@@ -21,6 +21,7 @@ export const workspaceOverrideSchema = z
21
21
  .strict();
22
22
  export const projectConfigSchema = z
23
23
  .object({
24
+ paths: z.array(z.string().min(1)).min(1).optional(),
24
25
  tagPrefix: z.string().min(1).optional(),
25
26
  })
26
27
  .strict();
@@ -1,4 +1,5 @@
1
1
  import { formatErrorLine } from '@williamthorsen/nmr-core';
2
+ import { describeError } from '@williamthorsen/toolbelt.errors';
2
3
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
3
4
  import { resolveOverridePath, validateAllChangelogOverrides, } from "./changelogOverrides.js";
4
5
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
@@ -16,21 +17,21 @@ export async function validateOverridesCommand(dependencies = {}) {
16
17
  rawConfig = await load();
17
18
  }
18
19
  catch (error) {
19
- return { exitCode: 2, message: formatErrorLine(`Failed to load config: ${errorMessage(error)}`) };
20
+ return { exitCode: 2, message: formatErrorLine(`Failed to load config: ${describeError(error)}`) };
20
21
  }
21
22
  let userConfig;
22
23
  try {
23
24
  userConfig = validateLoadedConfig(rawConfig);
24
25
  }
25
26
  catch (error) {
26
- return { exitCode: 2, message: errorMessage(error) };
27
+ return { exitCode: 2, message: describeError(error) };
27
28
  }
28
29
  let discoveredPaths;
29
30
  try {
30
31
  discoveredPaths = await discover();
31
32
  }
32
33
  catch (error) {
33
- return { exitCode: 2, message: formatErrorLine(`Failed to discover workspaces: ${errorMessage(error)}`) };
34
+ return { exitCode: 2, message: formatErrorLine(`Failed to discover workspaces: ${describeError(error)}`) };
34
35
  }
35
36
  let inputs;
36
37
  try {
@@ -40,7 +41,7 @@ export async function validateOverridesCommand(dependencies = {}) {
40
41
  : buildMonorepoInputs(discoveredPaths, userConfig, buildEntries);
41
42
  }
42
43
  catch (error) {
43
- return { exitCode: 2, message: formatErrorLine(`Failed to resolve overrides scope: ${errorMessage(error)}`) };
44
+ return { exitCode: 2, message: formatErrorLine(`Failed to resolve overrides scope: ${describeError(error)}`) };
44
45
  }
45
46
  const result = validate(inputs);
46
47
  return formatValidateOverridesResult(result);
@@ -70,9 +71,6 @@ function formatSummaryLine(errorCount, warningCount) {
70
71
  function pluralize(count, noun) {
71
72
  return count === 1 ? `${count} ${noun}` : `${count} ${noun}s`;
72
73
  }
73
- function errorMessage(error) {
74
- return error instanceof Error ? error.message : String(error);
75
- }
76
74
  function defaultBuildEntries(config, tagPattern, includePaths) {
77
75
  const options = {};
78
76
  if (tagPattern !== undefined) {
@@ -128,9 +126,8 @@ function buildMonorepoInputs(discoveredPaths, userConfig, buildEntries) {
128
126
  filePath: resolveOverridePath('.'),
129
127
  };
130
128
  if (project !== undefined) {
131
- const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
132
129
  const projectTagPattern = buildTagPattern([project.tagPrefix]);
133
- projectScope.hashes = flattenEntriesToHashes(buildEntries(config, projectTagPattern, contributingPaths));
130
+ projectScope.hashes = flattenEntriesToHashes(buildEntries(config, projectTagPattern, project.paths));
134
131
  }
135
132
  return { project: projectScope, workspaces };
136
133
  }
@@ -2,5 +2,4 @@ export declare function hasExpectedTopLevelShape(value: unknown): value is {
2
2
  tiers: unknown[];
3
3
  types: unknown[];
4
4
  };
5
- export declare function errorMessage(error: unknown): string;
6
5
  export declare function buildFetchInit(): RequestInit | undefined;
@@ -7,9 +7,6 @@ export function hasExpectedTopLevelShape(value) {
7
7
  }
8
8
  return Array.isArray(value.tiers) && Array.isArray(value.types);
9
9
  }
10
- export function errorMessage(error) {
11
- return error instanceof Error ? error.message : String(error);
12
- }
13
10
  export function buildFetchInit() {
14
11
  const token = process.env['GITHUB_TOKEN'];
15
12
  if (token === undefined || token === '') {
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "10.3.1",
3
+ "version": "10.4.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
- "keywords": [],
5
+ "keywords": [
6
+ "changelog",
7
+ "monorepo",
8
+ "release",
9
+ "semver",
10
+ "versioning"
11
+ ],
6
12
  "homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/release-kit#readme",
7
13
  "bugs": {
8
14
  "url": "https://github.com/williamthorsen/node-monorepo-tools/issues"
@@ -14,6 +20,9 @@
14
20
  },
15
21
  "license": "ISC",
16
22
  "author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
23
+ "sideEffects": [
24
+ "./dist/esm/bin/*.js"
25
+ ],
17
26
  "type": "module",
18
27
  "exports": {
19
28
  ".": {
@@ -39,16 +48,19 @@
39
48
  "CHANGELOG.md"
40
49
  ],
41
50
  "dependencies": {
51
+ "@williamthorsen/toolbelt.errors": "0.5.0",
42
52
  "glob": "13.0.6",
43
53
  "json-stringify-pretty-compact": "4.0.0",
44
54
  "semver": "7.8.5",
45
55
  "yaml": "2.9.0",
46
56
  "zod": "4.4.3",
47
- "@williamthorsen/nmr-core": "0.9.3"
57
+ "@williamthorsen/nmr-core": "0.10.1"
48
58
  },
49
59
  "devDependencies": {
50
60
  "@types/semver": "7.8.0",
51
- "readyup": "0.26.0",
61
+ "@williamthorsen/toolbelt.testing": "0.3.0",
62
+ "@williamthorsen/toolbelt.vitest": "0.4.0",
63
+ "readyup": "0.28.0",
52
64
  "smol-toml": "1.7.1"
53
65
  },
54
66
  "engines": {