@williamthorsen/release-kit 16.1.2 → 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 (86) hide show
  1. package/.meta/changelog.json +62 -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 +22 -0
  6. package/README.md +7 -2
  7. package/bin/release-kit.js +1 -1
  8. package/dist/esm/assertReleasesOnNpm.js +1 -2
  9. package/dist/esm/assertTarballsComplete.js +2 -3
  10. package/dist/esm/bin.d.ts +2 -0
  11. package/dist/esm/bin.js +1 -0
  12. package/dist/esm/changelogJsonUtils.js +2 -1
  13. package/dist/esm/cli/commandKit.d.ts +18 -0
  14. package/dist/esm/cli/commandKit.js +27 -0
  15. package/dist/esm/cli/maintenanceCommands.d.ts +6 -0
  16. package/dist/esm/cli/maintenanceCommands.js +129 -0
  17. package/dist/esm/cli/releaseCommands.d.ts +7 -0
  18. package/dist/esm/cli/releaseCommands.js +156 -0
  19. package/dist/esm/cli.js +4 -3
  20. package/dist/esm/commitCommand.d.ts +4 -1
  21. package/dist/esm/commitCommand.js +2 -6
  22. package/dist/esm/createGithubReleaseCommand.d.ts +10 -4
  23. package/dist/esm/createGithubReleaseCommand.js +2 -15
  24. package/dist/esm/deriveDirectoryConfig.js +1 -2
  25. package/dist/esm/deriveWorkspaceConfig.js +1 -2
  26. package/dist/esm/detectPackageManager.js +1 -1
  27. package/dist/esm/findMissingManifestPaths.js +1 -1
  28. package/dist/esm/hasPrettierConfig.js +1 -1
  29. package/dist/esm/init/parseJsonRecord.js +1 -1
  30. package/dist/esm/isPublishableManifest.js +1 -1
  31. package/dist/esm/loadChangelogOverrides.js +1 -1
  32. package/dist/esm/loadConfig.js +1 -2
  33. package/dist/esm/loadValidatedConfig.d.ts +6 -1
  34. package/dist/esm/loadValidatedConfig.js +11 -3
  35. package/dist/esm/npmRegistry.js +1 -1
  36. package/dist/esm/packFromCleanCheckout.js +1 -2
  37. package/dist/esm/parseChangeRecordBlock.js +1 -1
  38. package/dist/esm/prepareArgs.d.ts +1 -49
  39. package/dist/esm/prepareArgs.js +4 -51
  40. package/dist/esm/prepareCommand.d.ts +15 -3
  41. package/dist/esm/prepareCommand.js +15 -23
  42. package/dist/esm/publishCommand.d.ts +12 -3
  43. package/dist/esm/publishCommand.js +1 -16
  44. package/dist/esm/pushCommand.d.ts +12 -4
  45. package/dist/esm/pushCommand.js +2 -16
  46. package/dist/esm/resolveCommandTags.d.ts +3 -3
  47. package/dist/esm/resolveCommandTags.js +5 -7
  48. package/dist/esm/{resolveStylesOrExit.d.ts → resolveStyles.d.ts} +2 -2
  49. package/dist/esm/resolveStyles.js +10 -0
  50. package/dist/esm/route.d.ts +2 -0
  51. package/dist/esm/route.js +39 -0
  52. package/dist/esm/showTagPrefixesCommand.js +2 -1
  53. package/dist/esm/sync-labels/generateCommand.js +7 -6
  54. package/dist/esm/sync-labels/presets.js +1 -2
  55. package/dist/esm/sync-labels/retiredConfig.js +2 -1
  56. package/dist/esm/sync-labels/syncCommand.js +4 -3
  57. package/dist/esm/tagCommand.d.ts +8 -3
  58. package/dist/esm/tagCommand.js +1 -7
  59. package/dist/esm/typeGuards.d.ts +0 -2
  60. package/dist/esm/typeGuards.js +0 -3
  61. package/dist/esm/validateConfig.js +1 -1
  62. package/dist/esm/validateOverridesCommand.js +5 -4
  63. package/docs/configuration.md +1 -1
  64. package/docs/editorial-overrides.md +2 -0
  65. package/docs/labels.md +2 -0
  66. package/docs/releasing.md +1 -1
  67. package/package.json +3 -3
  68. package/dist/esm/configFlagSchema.d.ts +0 -12
  69. package/dist/esm/configFlagSchema.js +0 -3
  70. package/dist/esm/help/initHelp.d.ts +0 -4
  71. package/dist/esm/help/initHelp.js +0 -31
  72. package/dist/esm/help/overridesHelp.d.ts +0 -4
  73. package/dist/esm/help/overridesHelp.js +0 -32
  74. package/dist/esm/help/prepareHelp.d.ts +0 -7
  75. package/dist/esm/help/prepareHelp.js +0 -30
  76. package/dist/esm/help/releaseStepsHelp.d.ts +0 -12
  77. package/dist/esm/help/releaseStepsHelp.js +0 -91
  78. package/dist/esm/help/syncLabelsHelp.d.ts +0 -8
  79. package/dist/esm/help/syncLabelsHelp.js +0 -55
  80. package/dist/esm/help/usage.d.ts +0 -2
  81. package/dist/esm/help/usage.js +0 -29
  82. package/dist/esm/resolveConfigFlag.d.ts +0 -7
  83. package/dist/esm/resolveConfigFlag.js +0 -4
  84. package/dist/esm/resolveStylesOrExit.js +0 -10
  85. package/dist/esm/runCli.d.ts +0 -5
  86. package/dist/esm/runCli.js +0 -224
@@ -1,46 +1,5 @@
1
- import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
- import { configFlagSchema } from "./configFlagSchema.js";
3
- const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
1
+ import { UsageError } from '@williamthorsen/nmr-core/cli';
4
2
  export const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
5
- export const prepareFlagSchema = {
6
- ...configFlagSchema,
7
- isDryRun: { long: '--dry-run', type: 'boolean' },
8
- shouldForce: { long: '--force', type: 'boolean' },
9
- shouldSkipGitChecks: {
10
- long: '--no-git-checks',
11
- type: 'boolean',
12
- short: '-n',
13
- },
14
- bump: { long: '--bump', type: 'string' },
15
- setVersion: { long: '--set-version', type: 'string' },
16
- only: { long: '--only', type: 'string' },
17
- shouldIncludeReleaseNotes: { long: '--with-release-notes', type: 'boolean' },
18
- };
19
- export function parseArgs(argv) {
20
- const { flags } = parseArgsOrExit(argv, prepareFlagSchema);
21
- let bumpOverride;
22
- if (flags.bump !== undefined) {
23
- if (!isReleaseType(flags.bump)) {
24
- reportError(`Invalid bump type "${flags.bump}". Must be one of: ${VALID_BUMP_TYPES.join(', ')}`);
25
- process.exit(1);
26
- }
27
- bumpOverride = flags.bump;
28
- }
29
- let onlyNames;
30
- if (flags.only !== undefined) {
31
- onlyNames = flags.only.split(',');
32
- }
33
- return {
34
- configPath: flags.config,
35
- isDryRun: flags.isDryRun,
36
- shouldForce: flags.shouldForce,
37
- shouldSkipGitChecks: flags.shouldSkipGitChecks,
38
- bumpOverride,
39
- onlyNames,
40
- setVersion: flags.setVersion,
41
- shouldIncludeReleaseNotes: flags.shouldIncludeReleaseNotes,
42
- };
43
- }
44
3
  export function parseSetVersions(value, knownNames) {
45
4
  const setVersions = new Map();
46
5
  for (const entry of value.split(',')) {
@@ -48,22 +7,16 @@ export function parseSetVersions(value, knownNames) {
48
7
  const name = entry.slice(0, Math.max(0, separatorIndex));
49
8
  const version = entry.slice(separatorIndex + 1);
50
9
  if (separatorIndex <= 0 || !CANONICAL_SEMVER_PATTERN.test(version)) {
51
- reportError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <target>@N.N.N ` +
10
+ throw new UsageError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <target>@N.N.N ` +
52
11
  '(canonical semver, no pre-release suffix), e.g. --set-version=arrays@1.0.0.');
53
- process.exit(1);
54
12
  }
55
13
  if (!knownNames.includes(name)) {
56
- reportError(`Unknown release target "${name}" in --set-version. Known targets: ${knownNames.join(', ')}`);
57
- process.exit(1);
14
+ throw new UsageError(`Unknown release target "${name}" in --set-version. Known targets: ${knownNames.join(', ')}`);
58
15
  }
59
16
  if (setVersions.has(name)) {
60
- reportError(`--set-version names target "${name}" more than once`);
61
- process.exit(1);
17
+ throw new UsageError(`--set-version names target "${name}" more than once`);
62
18
  }
63
19
  setVersions.set(name, version);
64
20
  }
65
21
  return setVersions;
66
22
  }
67
- function isReleaseType(value) {
68
- return VALID_BUMP_TYPES.includes(value);
69
- }
@@ -1,8 +1,20 @@
1
1
  import { type StreamStyles } from '@williamthorsen/nmr-core';
2
+ import type { ReleaseType } from './types.js';
3
+ export interface PrepareCommandOptions {
4
+ bumpOverride: ReleaseType | undefined;
5
+ configPath: string | undefined;
6
+ isDryRun: boolean;
7
+ /** The release targets that `--only` names, or `undefined` to release every target that has changes. */
8
+ onlyNames: string[] | undefined;
9
+ /** The raw `--set-version` value, whose accepted form depends on the mode. */
10
+ setVersion: string | undefined;
11
+ shouldForce: boolean;
12
+ shouldIncludeReleaseNotes: boolean;
13
+ shouldSkipGitChecks: boolean;
14
+ styles: StreamStyles;
15
+ }
2
16
  /**
3
17
  * Orchestrates the CLI `prepare` command: checks the working tree, loads and validates the config, discovers the
4
18
  * workspaces, and prepares the release over the targets of the repo shape.
5
- *
6
- * A relative `--config` resolves against `invocationDir`.
7
19
  */
8
- export declare function prepareCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
20
+ export declare function prepareCommand({ bumpOverride, configPath, isDryRun, onlyNames, setVersion, shouldForce, shouldIncludeReleaseNotes, shouldSkipGitChecks, styles, }: PrepareCommandOptions): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { formatGlyphLine, isAnticipatedFailure, reportError, UserError, } from '@williamthorsen/nmr-core';
2
+ import { formatGlyphLine, isAnticipatedFailure, UserError, } from '@williamthorsen/nmr-core';
3
+ import { UsageError } from '@williamthorsen/nmr-core/cli';
3
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
5
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
5
6
  import { assertReleasesOnNpm } from "./assertReleasesOnNpm.js";
@@ -7,18 +8,16 @@ import { assertTarballsComplete } from "./assertTarballsComplete.js";
7
8
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
8
9
  import { dim } from "./format.js";
9
10
  import { RELEASE_GLYPHS } from "./glyphs.js";
10
- import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
11
- import { CANONICAL_SEMVER_PATTERN, parseArgs, parseSetVersions } from "./prepareArgs.js";
11
+ import { ConfigProblemError, loadValidatedConfig, reportConfigWarnings } from "./loadValidatedConfig.js";
12
+ import { CANONICAL_SEMVER_PATTERN, parseSetVersions } from "./prepareArgs.js";
12
13
  import { prepareReleaseTargets } from "./prepareReleaseTargets.js";
13
14
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
14
15
  import { applyReleasePlan } from "./releasePlan.js";
15
16
  import { reportPrepare } from "./reportPrepare.js";
16
- import { resolveConfigFlag } from "./resolveConfigFlag.js";
17
17
  import { isSinglePackageTargets, PROJECT_TARGET_ID, resolveReleaseTargets, SINGLE_PACKAGE_TARGET_ID, } from "./resolveReleaseTargets.js";
18
18
  import { prefixUserError } from "./user-error-chaining.js";
19
19
  const UNCHANGED_TREE_NOTE = 'The command did not write any files; the working tree is unchanged.';
20
- export async function prepareCommand(argv, styles, invocationDir) {
21
- const { configPath: configFlag, isDryRun, shouldForce, shouldSkipGitChecks, bumpOverride, onlyNames, setVersion, shouldIncludeReleaseNotes, } = parseArgs(argv);
20
+ export async function prepareCommand({ bumpOverride, configPath, isDryRun, onlyNames, setVersion, shouldForce, shouldIncludeReleaseNotes, shouldSkipGitChecks, styles, }) {
22
21
  const options = {
23
22
  shouldForce,
24
23
  ...(bumpOverride !== undefined && { bumpOverride }),
@@ -30,10 +29,9 @@ export async function prepareCommand(argv, styles, invocationDir) {
30
29
  if (!isDryRun && !shouldSkipGitChecks) {
31
30
  assertCleanWorkingTree();
32
31
  }
33
- const configResult = await loadValidatedConfig(resolveConfigFlag(configFlag, invocationDir));
32
+ const configResult = await loadValidatedConfig(configPath);
34
33
  if (configResult.status === 'invalid') {
35
- reportConfigProblem(configResult.problem, styles.stderr);
36
- process.exit(1);
34
+ throw new ConfigProblemError(configResult.problem);
37
35
  }
38
36
  let userConfig;
39
37
  if (configResult.status === 'ok') {
@@ -48,10 +46,9 @@ export async function prepareCommand(argv, styles, invocationDir) {
48
46
  throw prefixUserError('Failed to discover workspaces', error);
49
47
  }
50
48
  if (workspaceDiscovery.kind === 'empty') {
51
- reportError(`No workspace package to release. ${describeEmptyWorkspace(workspaceDiscovery)}`);
52
- process.exit(1);
49
+ throw new UserError(`No workspace package to release. ${describeEmptyWorkspace(workspaceDiscovery)}`);
53
50
  }
54
- const config = resolveTargetsOrExit(userConfig, workspaceDiscovery, onlyNames, setVersion);
51
+ const config = resolveSelectedTargets(userConfig, workspaceDiscovery, onlyNames, setVersion);
55
52
  const targetOptions = {
56
53
  ...options,
57
54
  ...(onlyNames !== undefined && { onlyTargetIds: onlyNames }),
@@ -69,15 +66,13 @@ function findReleasedPackagePaths(plan, targets) {
69
66
  const releasedIds = new Set(plan.targets.flatMap((result) => result.status === 'released' && result.kind === 'package' ? result.targetId : []));
70
67
  return targets.flatMap((target) => target.kind === 'package' && releasedIds.has(target.id) ? target.packagePath : []);
71
68
  }
72
- function resolveTargetsOrExit(userConfig, discovery, onlyNames, setVersion) {
69
+ function resolveSelectedTargets(userConfig, discovery, onlyNames, setVersion) {
73
70
  if (discovery.kind === 'single-package') {
74
71
  if (onlyNames !== undefined) {
75
- reportError('--only is only supported for monorepo configurations');
76
- process.exit(1);
72
+ throw new UsageError('--only is only supported for monorepo configurations');
77
73
  }
78
74
  if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
79
- reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
80
- process.exit(1);
75
+ throw new UsageError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
81
76
  }
82
77
  return resolveReleaseTargets(userConfig, discovery);
83
78
  }
@@ -89,10 +84,9 @@ function resolveTargetsOrExit(userConfig, discovery, onlyNames, setVersion) {
89
84
  throw prefixUserError('Failed to resolve workspaces', error);
90
85
  }
91
86
  if (setVersion !== undefined && config.targets.some((target) => target.kind === 'composite')) {
92
- reportError('--set-version cannot be combined with a project release. ' +
87
+ throw new UsageError('--set-version cannot be combined with a project release. ' +
93
88
  "--set-version sets individual workspaces' versions; a project release rolls up every " +
94
89
  'contributing workspace. To use --set-version, run on a config without a `project` block or a `releases` composite.');
95
- process.exit(1);
96
90
  }
97
91
  const knownNames = listSelectableIds(config.targets);
98
92
  const requestedNames = onlyNames ?? [];
@@ -100,8 +94,7 @@ function resolveTargetsOrExit(userConfig, discovery, onlyNames, setVersion) {
100
94
  if (knownNames.includes(name)) {
101
95
  continue;
102
96
  }
103
- reportError(`Unknown release target "${name}". Known targets: ${knownNames.join(', ')}`);
104
- process.exit(1);
97
+ throw new UsageError(`Unknown release target "${name}". Known targets: ${knownNames.join(', ')}`);
105
98
  }
106
99
  return config;
107
100
  }
@@ -141,9 +134,8 @@ function runAndReport(computePlan, isDryRun, style) {
141
134
  if (formatError === undefined) {
142
135
  return;
143
136
  }
144
- reportError(`The format command failed, but the release is prepared and ${RELEASE_TAGS_FILE} is written. ` +
137
+ throw new UserError(`The format command failed, but the release is prepared and ${RELEASE_TAGS_FILE} is written. ` +
145
138
  `Format the release files and run 'release-kit commit'.`);
146
- process.exit(1);
147
139
  }
148
140
  function runFormatCommand(formatCommand) {
149
141
  if (formatCommand === undefined) {
@@ -1,7 +1,16 @@
1
- import { type StreamStyles } from '@williamthorsen/nmr-core';
1
+ import type { StreamStyles } from '@williamthorsen/nmr-core';
2
+ export interface PublishCommandOptions {
3
+ configPath: string | undefined;
4
+ isDryRun: boolean;
5
+ shouldGenerateProvenance: boolean;
6
+ shouldSkipGitChecks: boolean;
7
+ /** The tag names that `--tags` selects, or `undefined` to publish every publishable release tag on HEAD. */
8
+ tags: string[] | undefined;
9
+ styles: StreamStyles;
10
+ }
2
11
  /**
3
12
  * Orchestrates the CLI `publish` command: checks for a clean working tree, resolves the tags on HEAD, checks the
4
13
  * tarballs of the publishable ones from a clean checkout, and publishes each one, injecting release notes into its
5
- * README for the duration of the publish. A relative `--config` resolves against `invocationDir`.
14
+ * README for the duration of the publish.
6
15
  */
7
- export declare function publishCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
16
+ export declare function publishCommand({ configPath, isDryRun, shouldGenerateProvenance, shouldSkipGitChecks, tags: requestedTags, styles, }: PublishCommandOptions): Promise<void>;
@@ -1,9 +1,7 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { parseArgsOrExit } from '@williamthorsen/nmr-core';
4
3
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
5
4
  import { assertTarballsComplete } from "./assertTarballsComplete.js";
6
- import { configFlagSchema } from "./configFlagSchema.js";
7
5
  import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
8
6
  import { detectPackageManager } from "./detectPackageManager.js";
9
7
  import { formatDirectoryPublishSkip } from "./formatDirectoryPublishSkip.js";
@@ -11,26 +9,13 @@ import { formatPrivateSkip } from "./formatPrivateSkip.js";
11
9
  import { formatProjectPublishSkip } from "./formatProjectPublishSkip.js";
12
10
  import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
13
11
  import { loadUsableConfig } from "./loadValidatedConfig.js";
14
- import { parseRequestedTags } from "./parseRequestedTags.js";
15
12
  import { publishPackage } from "./publish.js";
16
13
  import { resolveCommandTags } from "./resolveCommandTags.js";
17
- import { resolveConfigFlag } from "./resolveConfigFlag.js";
18
- const publishFlagSchema = {
19
- ...configFlagSchema,
20
- isDryRun: { long: '--dry-run', type: 'boolean' },
21
- shouldSkipGitChecks: { long: '--no-git-checks', type: 'boolean' },
22
- shouldGenerateProvenance: { long: '--provenance', type: 'boolean' },
23
- tags: { long: '--tags', type: 'string' },
24
- };
25
- export async function publishCommand(argv, styles, invocationDir) {
26
- const parsedArgs = parseArgsOrExit(argv, publishFlagSchema);
27
- const configPath = resolveConfigFlag(parsedArgs.flags.config, invocationDir);
28
- const { isDryRun, shouldSkipGitChecks, shouldGenerateProvenance } = parsedArgs.flags;
14
+ export async function publishCommand({ configPath, isDryRun, shouldGenerateProvenance, shouldSkipGitChecks, tags: requestedTags, styles, }) {
29
15
  const userConfig = await loadUsableConfig(styles.stderr, configPath);
30
16
  if (!isDryRun && !shouldSkipGitChecks) {
31
17
  assertCleanWorkingTree();
32
18
  }
33
- const requestedTags = parseRequestedTags(parsedArgs.flags.tags);
34
19
  const resolvedTags = resolveCommandTags(requestedTags, userConfig);
35
20
  if (resolvedTags.length === 0) {
36
21
  return;
@@ -1,6 +1,14 @@
1
- import { type StreamStyles } from '@williamthorsen/nmr-core';
1
+ import type { StreamStyles } from '@williamthorsen/nmr-core';
2
+ export interface PushCommandOptions {
3
+ configPath: string | undefined;
4
+ isDryRun: boolean;
5
+ /** The tag names that `--tags` selects, or `undefined` to push every release tag on HEAD. */
6
+ tags: string[] | undefined;
7
+ tagsOnly: boolean;
8
+ styles: StreamStyles;
9
+ }
2
10
  /**
3
- * Orchestrates the CLI `push` command: parses flags, loads the config, resolves tags from HEAD, and pushes
4
- * the release commit and each tag individually. A relative `--config` resolves against `invocationDir`.
11
+ * Orchestrates the CLI `push` command: loads the config, resolves tags from HEAD, and pushes the release commit and
12
+ * each tag individually.
5
13
  */
6
- export declare function pushCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
14
+ export declare function pushCommand({ configPath, isDryRun, tags, tagsOnly, styles }: PushCommandOptions): Promise<void>;
@@ -1,23 +1,9 @@
1
- import { parseArgsOrExit } from '@williamthorsen/nmr-core';
2
- import { configFlagSchema } from "./configFlagSchema.js";
3
1
  import { loadUsableConfig } from "./loadValidatedConfig.js";
4
- import { parseRequestedTags } from "./parseRequestedTags.js";
5
2
  import { pushRelease } from "./pushRelease.js";
6
3
  import { resolveCommandTags } from "./resolveCommandTags.js";
7
- import { resolveConfigFlag } from "./resolveConfigFlag.js";
8
- const pushFlagSchema = {
9
- ...configFlagSchema,
10
- isDryRun: { long: '--dry-run', type: 'boolean' },
11
- tags: { long: '--tags', type: 'string' },
12
- tagsOnly: { long: '--tags-only', type: 'boolean' },
13
- };
14
- export async function pushCommand(argv, styles, invocationDir) {
15
- const parsedArgs = parseArgsOrExit(argv, pushFlagSchema);
16
- const configPath = resolveConfigFlag(parsedArgs.flags.config, invocationDir);
17
- const { isDryRun, tagsOnly } = parsedArgs.flags;
4
+ export async function pushCommand({ configPath, isDryRun, tags, tagsOnly, styles }) {
18
5
  const userConfig = await loadUsableConfig(styles.stderr, configPath);
19
- const requestedTags = parseRequestedTags(parsedArgs.flags.tags);
20
- const resolvedTags = resolveCommandTags(requestedTags, userConfig);
6
+ const resolvedTags = resolveCommandTags(tags, userConfig);
21
7
  if (resolvedTags.length === 0) {
22
8
  return;
23
9
  }
@@ -3,9 +3,9 @@ import { type ResolvedTag } from './resolveReleaseTags.js';
3
3
  /**
4
4
  * Discovers workspaces, resolves release tags from HEAD, validates `--tags` names against the full resolved tag
5
5
  * names (e.g., `nmr-core-v1.3.0`), and returns the filtered tag list. Throws a `UserError` when workspace discovery
6
- * fails, when a workspace's `package.json` does not have a `name` field, and when `mergeMonorepoConfig` rejects the
7
- * config. Exits with an error message on any other validation failure, including a workspace that does not resolve to
8
- * any package, which is not single-package mode.
6
+ * fails, when a workspace's `package.json` does not have a `name` field, when `mergeMonorepoConfig` rejects the
7
+ * config, when the workspace does not resolve to any package (which is not single-package mode), and when HEAD does
8
+ * not have any release tags; throws a `UsageError` for a `--tags` name that HEAD lacks.
9
9
  *
10
10
  * In monorepo mode the workspace list is `userConfig` merged over the discovered workspaces, as `prepare` builds it,
11
11
  * and each directory target and a declared composite (a `project` block or a `releases` composite) contribute their
@@ -1,4 +1,5 @@
1
- import { reportError } from '@williamthorsen/nmr-core';
1
+ import { UserError } from '@williamthorsen/nmr-core';
2
+ import { UsageError } from '@williamthorsen/nmr-core/cli';
2
3
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
3
4
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
4
5
  import { formatExcludedSkip } from "./formatExcludedSkip.js";
@@ -15,8 +16,7 @@ export function resolveCommandTags(tags, userConfig) {
15
16
  throw prefixUserError('Failed to discover workspaces', error);
16
17
  }
17
18
  if (workspace.kind === 'empty') {
18
- reportError(`No workspace package to tag. ${describeEmptyWorkspace(workspace)}`);
19
- process.exit(1);
19
+ throw new UserError(`No workspace package to tag. ${describeEmptyWorkspace(workspace)}`);
20
20
  }
21
21
  let workspaces;
22
22
  let directories = [];
@@ -50,8 +50,7 @@ export function resolveCommandTags(tags, userConfig) {
50
50
  throw new Error('resolveCommandTags: invariant violated; neither workspaces nor singleWorkspace was derived');
51
51
  }
52
52
  if (resolvedTags.length === 0) {
53
- reportError('HEAD does not have any release tags. Create tags with `release-kit tag` first.');
54
- process.exit(1);
53
+ throw new UserError('HEAD does not have any release tags. Create tags with `release-kit tag` first.');
55
54
  }
56
55
  if (tags !== undefined) {
57
56
  const availableTagNames = resolvedTags.map((t) => t.tag);
@@ -59,8 +58,7 @@ export function resolveCommandTags(tags, userConfig) {
59
58
  if (availableTagNames.includes(name)) {
60
59
  continue;
61
60
  }
62
- reportError(`Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(', ')}`);
63
- process.exit(1);
61
+ throw new UsageError(`Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(', ')}`);
64
62
  }
65
63
  resolvedTags = resolvedTags.filter((t) => tags.includes(t.tag));
66
64
  }
@@ -6,5 +6,5 @@ export interface ResolveStylesOptions {
6
6
  readonly stderrIsTty: boolean;
7
7
  readonly stdoutIsTty: boolean;
8
8
  }
9
- /** Resolves the style of each output stream, exiting with a usage error when the variable does not name a setting. */
10
- export declare function resolveStylesOrExit(options: ResolveStylesOptions): StreamStyles;
9
+ /** Resolves the style of each output stream, throwing a `UsageError` when the variable doesn't name a setting. */
10
+ export declare function resolveStyles(options: ResolveStylesOptions): StreamStyles;
@@ -0,0 +1,10 @@
1
+ import { describeInvalidOutputStyle, resolveStreamStyles } from '@williamthorsen/nmr-core';
2
+ import { UsageError } from '@williamthorsen/nmr-core/cli';
3
+ export const OUTPUT_STYLE_ENV_VAR = 'RELEASE_KIT_OUTPUT_STYLE';
4
+ export function resolveStyles(options) {
5
+ const { invalid, styles } = resolveStreamStyles({ ...options, envVar: OUTPUT_STYLE_ENV_VAR });
6
+ if (invalid !== undefined) {
7
+ throw new UsageError(describeInvalidOutputStyle(invalid));
8
+ }
9
+ return styles;
10
+ }
@@ -0,0 +1,2 @@
1
+ /** Runs the `release-kit` command tree against the arguments and resolves to the exit code. */
2
+ export declare function routeCommand(args: string[]): Promise<number>;
@@ -0,0 +1,39 @@
1
+ import process from 'node:process';
2
+ import { resolveSelfVersion } from '@williamthorsen/nmr-core';
3
+ import { createCli, runCli } from '@williamthorsen/nmr-core/cli';
4
+ import { BIN } from "./bin.js";
5
+ import { init, overrides, showTagPrefixes, syncLabels, updateTemplates } from "./cli/maintenanceCommands.js";
6
+ import { commit, createGithubRelease, prepare, publish, push, tag } from "./cli/releaseCommands.js";
7
+ import { OUTPUT_STYLE_ENV_VAR, resolveStyles } from "./resolveStyles.js";
8
+ const VERSION = resolveSelfVersion(import.meta.url);
9
+ export async function routeCommand(args) {
10
+ return runCli(args, ROOT, {
11
+ name: BIN,
12
+ context: { env: process.env, stderrIsTty: process.stderr.isTTY, stdoutIsTty: process.stdout.isTTY },
13
+ stdout: process.stdout,
14
+ stderr: process.stderr,
15
+ version: () => VERSION,
16
+ });
17
+ }
18
+ const { defineGroup } = createCli();
19
+ const ROOT = defineGroup({
20
+ summary: 'Version-bumping and changelog-generation toolkit for release workflows',
21
+ deriveContext: (_flags, context) => ({ styles: resolveStyles(context) }),
22
+ commands: {
23
+ prepare,
24
+ commit,
25
+ tag,
26
+ push,
27
+ publish,
28
+ 'create-github-release': createGithubRelease,
29
+ 'show-tag-prefixes': showTagPrefixes,
30
+ init,
31
+ 'update-templates': updateTemplates,
32
+ overrides,
33
+ 'sync-labels': syncLabels,
34
+ },
35
+ epilog: `Environment:
36
+ ${OUTPUT_STYLE_ENV_VAR}
37
+ Output style: auto (default), plain, or rich. auto prints plain, without emoji,
38
+ when CI is set or the stream is not a terminal.`,
39
+ });
@@ -1,4 +1,5 @@
1
1
  import { formatStatusLine, isAnticipatedFailure, reportError, } from '@williamthorsen/nmr-core';
2
+ import { BIN } from "./bin.js";
2
3
  import { detectRepoType } from "./init/detectRepoType.js";
3
4
  import { loadValidatedConfig, reportConfigProblem } from "./loadValidatedConfig.js";
4
5
  import { previewTagPrefixes } from "./previewTagPrefixes.js";
@@ -20,7 +21,7 @@ export async function showTagPrefixesCommand(styles, configPath) {
20
21
  if (!isAnticipatedFailure(error)) {
21
22
  throw error;
22
23
  }
23
- reportError(error.message);
24
+ reportError(BIN, error.message);
24
25
  return 1;
25
26
  }
26
27
  process.stdout.write(renderMonorepo(preview, styles.stdout));
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { isAnticipatedFailure, reportError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { stringify } from 'yaml';
6
+ import { BIN } from "../bin.js";
6
7
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
7
8
  import { loadValidatedConfig, reportConfigProblem } from "../loadValidatedConfig.js";
8
9
  import { hashPreset } from "./presets.js";
@@ -28,7 +29,7 @@ export function formatLabelsYaml(labels, presetHashes, configFilePath = CONFIG_F
28
29
  export async function loadRepoLabelsConfig(stderrStyle, configPath) {
29
30
  const result = await loadValidatedConfig(configPath);
30
31
  if (result.status === 'missing') {
31
- reportError(`Config file not found at ${result.configFilePath}. Run \`release-kit sync-labels init\` first.`);
32
+ reportError(BIN, `Config file not found at ${result.configFilePath}. Run \`release-kit sync-labels init\` first.`);
32
33
  return undefined;
33
34
  }
34
35
  if (result.status === 'invalid') {
@@ -36,7 +37,7 @@ export async function loadRepoLabelsConfig(stderrStyle, configPath) {
36
37
  return undefined;
37
38
  }
38
39
  if (result.config.repoLabels === undefined) {
39
- reportError(`The \`repoLabels\` block is missing from ${result.configFilePath}. Run \`release-kit sync-labels init\` to seed one.`);
40
+ reportError(BIN, `The \`repoLabels\` block is missing from ${result.configFilePath}. Run \`release-kit sync-labels init\` to seed one.`);
40
41
  return undefined;
41
42
  }
42
43
  return result.config.repoLabels;
@@ -58,7 +59,7 @@ export async function generateCommand({ shouldCheck = false, configPath, styles
58
59
  throw error;
59
60
  }
60
61
  const message = describeError(error);
61
- reportError(`Failed to resolve labels: ${message}`);
62
+ reportError(BIN, `Failed to resolve labels: ${message}`);
62
63
  return 1;
63
64
  }
64
65
  const presetHashes = new Map();
@@ -76,7 +77,7 @@ export async function generateCommand({ shouldCheck = false, configPath, styles
76
77
  }
77
78
  catch (error) {
78
79
  const message = describeError(error);
79
- reportError(`Failed to write ${LABELS_OUTPUT_PATH}: ${message}`);
80
+ reportError(BIN, `Failed to write ${LABELS_OUTPUT_PATH}: ${message}`);
80
81
  return 1;
81
82
  }
82
83
  console.info(`Generated ${LABELS_OUTPUT_PATH} with ${String(labels.length)} labels.`);
@@ -88,11 +89,11 @@ function compareAgainstCommittedFile(expectedContent) {
88
89
  existingContent = readFileSync(LABELS_OUTPUT_PATH, 'utf8');
89
90
  }
90
91
  catch {
91
- reportError(`${LABELS_OUTPUT_PATH} is missing. Run \`release-kit sync-labels generate\` and commit the result.`);
92
+ reportError(BIN, `${LABELS_OUTPUT_PATH} is missing. Run \`release-kit sync-labels generate\` and commit the result.`);
92
93
  return 1;
93
94
  }
94
95
  if (existingContent !== expectedContent) {
95
- reportError(`${LABELS_OUTPUT_PATH} is stale. Run \`release-kit sync-labels generate\` and commit the result.`);
96
+ reportError(BIN, `${LABELS_OUTPUT_PATH} is stale. Run \`release-kit sync-labels generate\` and commit the result.`);
96
97
  return 1;
97
98
  }
98
99
  console.info(`${LABELS_OUTPUT_PATH} is up to date.`);
@@ -2,9 +2,8 @@ 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, UserError } from '@williamthorsen/nmr-core';
5
+ import { findPackageRoot, isRecord, UserError } from '@williamthorsen/nmr-core';
6
6
  import { parse } from 'yaml';
7
- import { isRecord } from "../typeGuards.js";
8
7
  import { chainUserError } from "../user-error-chaining.js";
9
8
  import { deriveTypeLabels } from "./typeLabels.js";
10
9
  const TYPE_LABELS_PRESET = 'common';
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { reportError } from '@williamthorsen/nmr-core';
3
+ import { BIN } from "../bin.js";
3
4
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
4
5
  export const RETIRED_SYNC_LABELS_CONFIG_PATH = '.config/sync-labels.config.ts';
5
6
  export function checkRetiredSyncLabelsConfig(configPath = CONFIG_FILE_PATH) {
6
7
  if (!existsSync(RETIRED_SYNC_LABELS_CONFIG_PATH)) {
7
8
  return false;
8
9
  }
9
- reportError(`${RETIRED_SYNC_LABELS_CONFIG_PATH} is no longer read. Move its labels into the \`repoLabels\` block of ${configPath} (presets move to \`repoLabels.extends\`; each label becomes a \`'name': { color, description }\` entry under \`repoLabels.labels\`), then delete the file.`);
10
+ reportError(BIN, `${RETIRED_SYNC_LABELS_CONFIG_PATH} is no longer read. Move its labels into the \`repoLabels\` block of ${configPath} (presets move to \`repoLabels.extends\`; each label becomes a \`'name': { color, description }\` entry under \`repoLabels.labels\`), then delete the file.`);
10
11
  return true;
11
12
  }
@@ -2,6 +2,7 @@ import { execSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { GIT_OUTPUT_LIMIT_BYTES, reportError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
+ import { BIN } from "../bin.js";
5
6
  import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
6
7
  const WORKFLOW_FILE = '.github/workflows/sync-labels.yaml';
7
8
  function checkGhAvailable() {
@@ -18,11 +19,11 @@ export function syncLabelsCommand() {
18
19
  return 1;
19
20
  }
20
21
  if (!checkGhAvailable()) {
21
- reportError('The `gh` CLI is not installed or not in PATH. Install it from https://cli.github.com/');
22
+ reportError(BIN, 'The `gh` CLI is not installed or not in PATH. Install it from https://cli.github.com/');
22
23
  return 1;
23
24
  }
24
25
  if (!existsSync(WORKFLOW_FILE)) {
25
- reportError(`${WORKFLOW_FILE} not found. Run \`release-kit sync-labels init\` first.`);
26
+ reportError(BIN, `${WORKFLOW_FILE} not found. Run \`release-kit sync-labels init\` first.`);
26
27
  return 1;
27
28
  }
28
29
  try {
@@ -30,7 +31,7 @@ export function syncLabelsCommand() {
30
31
  }
31
32
  catch (error) {
32
33
  const message = describeError(error);
33
- reportError(`Failed to trigger workflow: ${message}`);
34
+ reportError(BIN, `Failed to trigger workflow: ${message}`);
34
35
  return 1;
35
36
  }
36
37
  console.info('Workflow triggered successfully. View runs at: gh run list --workflow=sync-labels.yaml');
@@ -1,3 +1,8 @@
1
- import { type StreamStyles } from '@williamthorsen/nmr-core';
2
- /** Runs the CLI `tag` command: parses flags and delegates to `createTags`. */
3
- export declare function tagCommand(argv: string[], styles: StreamStyles): void;
1
+ import type { StreamStyles } from '@williamthorsen/nmr-core';
2
+ export interface TagCommandOptions {
3
+ isDryRun: boolean;
4
+ shouldSkipGitChecks: boolean;
5
+ styles: StreamStyles;
6
+ }
7
+ /** Runs the CLI `tag` command by delegating to `createTags`. */
8
+ export declare function tagCommand({ isDryRun, shouldSkipGitChecks, styles }: TagCommandOptions): void;
@@ -1,10 +1,4 @@
1
- import { parseArgsOrExit } from '@williamthorsen/nmr-core';
2
1
  import { createTags } from "./createTags.js";
3
- const tagFlagSchema = {
4
- isDryRun: { long: '--dry-run', type: 'boolean' },
5
- shouldSkipGitChecks: { long: '--no-git-checks', type: 'boolean' },
6
- };
7
- export function tagCommand(argv, styles) {
8
- const { isDryRun, shouldSkipGitChecks } = parseArgsOrExit(argv, tagFlagSchema).flags;
2
+ export function tagCommand({ isDryRun, shouldSkipGitChecks, styles }) {
9
3
  createTags({ isDryRun, shouldSkipGitChecks, style: styles.stdout });
10
4
  }
@@ -1,4 +1,2 @@
1
- /** Checks whether a value is a plain object (non-null, non-array). */
2
- export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
1
  /** Narrows an `unknown` value to `unknown[]` via `Array.isArray`, avoiding `any[]`. */
4
2
  export declare function isUnknownArray(value: unknown): value is unknown[];
@@ -1,6 +1,3 @@
1
- export function isRecord(value) {
2
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3
- }
4
1
  export function isUnknownArray(value) {
5
2
  return Array.isArray(value);
6
3
  }
@@ -1,5 +1,5 @@
1
+ import { isRecord } from '@williamthorsen/nmr-core';
1
2
  import { releaseKitConfigSchema } from "./configSchema.js";
2
- import { isRecord } from "./typeGuards.js";
3
3
  export function validateConfig(rawConfig) {
4
4
  if (!isRecord(rawConfig)) {
5
5
  return { config: {}, errors: ['Config must be an object'], warnings: [] };