@williamthorsen/release-kit 10.8.0 → 11.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 (69) hide show
  1. package/.meta/changelog.json +100 -3
  2. package/.readyup/kits/default.js +3 -3
  3. package/.readyup/kits/npm-auto-publish.js +43 -23
  4. package/.readyup/manifest.json +9 -9
  5. package/CHANGELOG.md +82 -3
  6. package/README.md +46 -11
  7. package/dist/esm/bin/release-kit.js +46 -120
  8. package/dist/esm/buildChangelogEntries.js +1 -0
  9. package/dist/esm/configFlagSchema.d.ts +12 -0
  10. package/dist/esm/configFlagSchema.js +3 -0
  11. package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
  12. package/dist/esm/createGithubReleaseCommand.js +9 -3
  13. package/dist/esm/createTags.d.ts +2 -0
  14. package/dist/esm/createTags.js +5 -4
  15. package/dist/esm/discoverWorkspaces.d.ts +53 -8
  16. package/dist/esm/discoverWorkspaces.js +57 -37
  17. package/dist/esm/format.d.ts +5 -5
  18. package/dist/esm/format.js +5 -4
  19. package/dist/esm/glyphs.d.ts +3 -0
  20. package/dist/esm/glyphs.js +6 -0
  21. package/dist/esm/help/prepareHelp.d.ts +1 -1
  22. package/dist/esm/help/prepareHelp.js +1 -0
  23. package/dist/esm/init/initCommand.d.ts +3 -1
  24. package/dist/esm/init/initCommand.js +15 -15
  25. package/dist/esm/loadConfig.d.ts +7 -4
  26. package/dist/esm/loadConfig.js +6 -3
  27. package/dist/esm/loadValidatedConfig.d.ts +47 -5
  28. package/dist/esm/loadValidatedConfig.js +32 -15
  29. package/dist/esm/prepareCommand.d.ts +7 -1
  30. package/dist/esm/prepareCommand.js +37 -45
  31. package/dist/esm/previewTagPrefixes.d.ts +9 -6
  32. package/dist/esm/previewTagPrefixes.js +12 -24
  33. package/dist/esm/publishCommand.d.ts +4 -3
  34. package/dist/esm/publishCommand.js +9 -3
  35. package/dist/esm/pushCommand.d.ts +1 -1
  36. package/dist/esm/pushCommand.js +2 -2
  37. package/dist/esm/reportPrepare.d.ts +2 -0
  38. package/dist/esm/reportPrepare.js +51 -41
  39. package/dist/esm/resolveCommandTags.d.ts +3 -2
  40. package/dist/esm/resolveCommandTags.js +10 -6
  41. package/dist/esm/resolveReleaseNotesConfig.d.ts +10 -11
  42. package/dist/esm/resolveReleaseNotesConfig.js +10 -29
  43. package/dist/esm/resolveStylesOrExit.d.ts +10 -0
  44. package/dist/esm/resolveStylesOrExit.js +10 -0
  45. package/dist/esm/showTagPrefixesCommand.d.ts +5 -2
  46. package/dist/esm/showTagPrefixesCommand.js +31 -11
  47. package/dist/esm/sync-labels/generateCommand.d.ts +18 -8
  48. package/dist/esm/sync-labels/generateCommand.js +12 -11
  49. package/dist/esm/sync-labels/initCommand.d.ts +11 -4
  50. package/dist/esm/sync-labels/initCommand.js +24 -18
  51. package/dist/esm/sync-labels/retiredConfig.d.ts +4 -2
  52. package/dist/esm/sync-labels/retiredConfig.js +2 -2
  53. package/dist/esm/tagCommand.d.ts +2 -1
  54. package/dist/esm/tagCommand.js +2 -2
  55. package/dist/esm/validateOverridesCommand.d.ts +13 -7
  56. package/dist/esm/validateOverridesCommand.js +28 -36
  57. package/docs/configuration.md +35 -1
  58. package/docs/labels.md +2 -0
  59. package/docs/work-types.md +11 -8
  60. package/package.json +13 -14
  61. package/presets/labels/common.yaml +2 -2
  62. package/dist/esm/checkWorkTypesDrift.d.ts +0 -35
  63. package/dist/esm/checkWorkTypesDrift.js +0 -117
  64. package/dist/esm/syncWorkTypes.d.ts +0 -28
  65. package/dist/esm/syncWorkTypes.js +0 -94
  66. package/dist/esm/work-types.json +0 -149
  67. package/dist/esm/work-types.schema.json +0 -110
  68. package/dist/esm/workTypesUtils.d.ts +0 -22
  69. package/dist/esm/workTypesUtils.js +0 -16
@@ -1,15 +1,22 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  /** Options for the `sync-labels init` subcommand. */
2
3
  interface InitOptions {
4
+ /** Config file to read, relative to the working directory. Defaults to `CONFIG_FILE_PATH`. */
5
+ configPath?: string;
3
6
  dryRun: boolean;
4
7
  force: boolean;
8
+ styles: StreamStyles;
5
9
  }
6
10
  /**
7
11
  * Run the `sync-labels init` subcommand.
8
12
  *
9
13
  * Discovers workspaces and retired packages, scaffolds the caller workflow, then seeds the
10
- * `repoLabels` block: into a new `.config/release-kit.config.ts` when none exists, or
11
- * printed to stdout for manual paste when the file is already there — an existing,
12
- * hand-authored config is never rewritten. Returns 0 on success, 1 on failure.
14
+ * `repoLabels` block: into a new config file when none exists, or printed to stdout for manual
15
+ * paste when the file is already there — an existing, hand-authored config is never rewritten.
16
+ * Returns 0 on success, 1 on failure.
17
+ *
18
+ * `configPath` names the config to read. Naming one that does not exist fails the command, so the
19
+ * scaffolding branch is reached only under the default path and always writes to `CONFIG_FILE_PATH`.
13
20
  */
14
- export declare function syncLabelsInitCommand({ dryRun, force }: InitOptions): Promise<number>;
21
+ export declare function syncLabelsInitCommand({ configPath, dryRun, force, styles }: InitOptions): Promise<number>;
15
22
  export {};
@@ -1,47 +1,52 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { reportWriteResult, writeFileWithCheck } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
- import { discoverWorkspaces } from "../discoverWorkspaces.js";
4
+ import { describeEmptyWorkspace, discoverWorkspaces } from "../discoverWorkspaces.js";
5
5
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
6
- import { loadValidatedConfig } from "../loadValidatedConfig.js";
6
+ import { loadValidatedConfig, reportConfigProblem } from "../loadValidatedConfig.js";
7
7
  import { generateCommand, LABELS_OUTPUT_PATH } from "./generateCommand.js";
8
8
  import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
9
9
  import { buildScopeLabels, renderRepoLabelsBlock, repoLabelsConfigScript, syncLabelsWorkflow } from "./templates.js";
10
10
  const WORKFLOW_PATH = '.github/workflows/sync-labels.yaml';
11
- export async function syncLabelsInitCommand({ dryRun, force }) {
12
- if (checkRetiredSyncLabelsConfig()) {
11
+ export async function syncLabelsInitCommand({ configPath, dryRun, force, styles }) {
12
+ const configFilePath = configPath ?? CONFIG_FILE_PATH;
13
+ if (checkRetiredSyncLabelsConfig(configPath)) {
13
14
  return 1;
14
15
  }
15
16
  if (dryRun) {
16
17
  console.info('[dry-run mode]');
17
18
  }
18
19
  console.info('\n> Discovering workspaces');
19
- let workspacePaths;
20
+ let workspace;
20
21
  try {
21
- workspacePaths = await discoverWorkspaces();
22
+ workspace = discoverWorkspaces();
22
23
  }
23
24
  catch (error) {
24
25
  const message = describeError(error);
25
26
  process.stderr.write(` Failed to discover workspaces: ${message}\n`);
26
27
  return 1;
27
28
  }
28
- if (workspacePaths === undefined) {
29
+ if (workspace.kind === 'empty') {
30
+ process.stderr.write(` No workspace package to label. ${describeEmptyWorkspace(workspace)}\n`);
31
+ return 1;
32
+ }
33
+ if (workspace.kind === 'single-package') {
29
34
  console.info(' No pnpm workspaces found (single-package repo)');
30
35
  }
31
36
  else {
32
- console.info(` Found ${String(workspacePaths.length)} workspaces`);
37
+ console.info(` Found ${String(workspace.packageDirs.length)} workspaces`);
33
38
  }
34
- const configExists = existsSync(CONFIG_FILE_PATH);
35
- const retiredNames = configExists ? await loadRetiredPackageNames() : [];
39
+ const configExists = configPath !== undefined || existsSync(CONFIG_FILE_PATH);
40
+ const retiredNames = configExists ? await loadRetiredPackageNames(styles, configPath) : [];
36
41
  if (retiredNames === undefined) {
37
42
  return 1;
38
43
  }
39
- const scopeLabels = workspacePaths === undefined ? [] : buildScopeLabels(workspacePaths, retiredNames);
44
+ const scopeLabels = workspace.kind === 'single-package' ? [] : buildScopeLabels(workspace.packageDirs, retiredNames);
40
45
  console.info('\n> Scaffolding files');
41
46
  const workflowResult = writeFileWithCheck(WORKFLOW_PATH, syncLabelsWorkflow(), { dryRun, overwrite: force });
42
- reportWriteResult(workflowResult, dryRun);
47
+ reportWriteResult(workflowResult, dryRun, styles);
43
48
  if (configExists) {
44
- console.info(`\n> ${CONFIG_FILE_PATH} already exists; add this block to the object passed to defineConfig:\n`);
49
+ console.info(`\n> ${configFilePath} already exists; add this block to the object passed to defineConfig:\n`);
45
50
  console.info(renderRepoLabelsBlock(scopeLabels));
46
51
  if (workflowResult.outcome === 'failed') {
47
52
  process.stderr.write('Failed to scaffold one or more files.\n');
@@ -49,7 +54,7 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
49
54
  }
50
55
  console.info(`
51
56
  > Next steps
52
- 1. Paste the block above into ${CONFIG_FILE_PATH}.
57
+ 1. Paste the block above into ${configFilePath}.
53
58
  2. Run \`release-kit sync-labels generate\` to produce ${LABELS_OUTPUT_PATH}.
54
59
  3. Commit the changes.
55
60
  4. Run \`release-kit sync-labels sync\` to apply labels to your GitHub repo.
@@ -60,7 +65,7 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
60
65
  dryRun,
61
66
  overwrite: force,
62
67
  });
63
- reportWriteResult(configResult, dryRun);
68
+ reportWriteResult(configResult, dryRun, styles);
64
69
  if (workflowResult.outcome === 'failed' || configResult.outcome === 'failed') {
65
70
  process.stderr.write('Failed to scaffold one or more files.\n');
66
71
  return 1;
@@ -70,7 +75,7 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
70
75
  }
71
76
  else {
72
77
  console.info('\n> Generating labels');
73
- const generateExitCode = await generateCommand();
78
+ const generateExitCode = await generateCommand({ styles });
74
79
  if (generateExitCode !== 0) {
75
80
  return generateExitCode;
76
81
  }
@@ -87,9 +92,10 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
87
92
  `);
88
93
  return 0;
89
94
  }
90
- async function loadRetiredPackageNames() {
91
- const result = await loadValidatedConfig();
95
+ async function loadRetiredPackageNames(styles, configPath) {
96
+ const result = await loadValidatedConfig(configPath);
92
97
  if (result.status === 'invalid') {
98
+ reportConfigProblem(result.problem, styles.stderr);
93
99
  return undefined;
94
100
  }
95
101
  if (result.status === 'missing') {
@@ -5,6 +5,8 @@ export declare const RETIRED_SYNC_LABELS_CONFIG_PATH = ".config/sync-labels.conf
5
5
  *
6
6
  * Returns `true` when the file exists, in which case the caller must abort. The check is
7
7
  * unconditional on config content: file presence alone triggers it, so a half-migrated
8
- * repo cannot silently lose its custom labels.
8
+ * repo cannot silently lose its custom labels. `configPath` names the config to move the labels
9
+ * into; it defaults to `CONFIG_FILE_PATH`, which `sync-labels sync` relies on because it takes
10
+ * no `--config`.
9
11
  */
10
- export declare function checkRetiredSyncLabelsConfig(): boolean;
12
+ export declare function checkRetiredSyncLabelsConfig(configPath?: string): boolean;
@@ -2,10 +2,10 @@ import { existsSync } from 'node:fs';
2
2
  import { reportError } from '@williamthorsen/nmr-core';
3
3
  import { CONFIG_FILE_PATH } from "../loadConfig.js";
4
4
  export const RETIRED_SYNC_LABELS_CONFIG_PATH = '.config/sync-labels.config.ts';
5
- export function checkRetiredSyncLabelsConfig() {
5
+ export function checkRetiredSyncLabelsConfig(configPath = CONFIG_FILE_PATH) {
6
6
  if (!existsSync(RETIRED_SYNC_LABELS_CONFIG_PATH)) {
7
7
  return false;
8
8
  }
9
- reportError(`${RETIRED_SYNC_LABELS_CONFIG_PATH} is no longer read. Move its labels into the \`repoLabels\` block of ${CONFIG_FILE_PATH} (presets move to \`repoLabels.extends\`; each label becomes a \`'name': { color, description }\` entry under \`repoLabels.labels\`), then delete the file.`);
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
10
  return true;
11
11
  }
@@ -1,2 +1,3 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  /** Orchestrate the CLI `tag` command: parse flags and delegate to `createTags`. */
2
- export declare function tagCommand(argv: string[]): void;
3
+ export declare function tagCommand(argv: string[], styles: StreamStyles): void;
@@ -5,10 +5,10 @@ const tagFlagSchema = {
5
5
  dryRun: { long: '--dry-run', type: 'boolean' },
6
6
  noGitChecks: { long: '--no-git-checks', type: 'boolean' },
7
7
  };
8
- export function tagCommand(argv) {
8
+ export function tagCommand(argv, styles) {
9
9
  const { dryRun, noGitChecks } = parseArgsOrExit(argv, tagFlagSchema).flags;
10
10
  try {
11
- createTags({ dryRun, noGitChecks });
11
+ createTags({ dryRun, noGitChecks, style: styles.stdout });
12
12
  }
13
13
  catch (error) {
14
14
  reportError(describeError(error));
@@ -1,9 +1,11 @@
1
+ import { type OutputStyle, type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  import { type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult } from './changelogOverrides.js';
3
+ import { type WorkspaceDiscovery } from './discoverWorkspaces.js';
4
+ import { type LoadValidatedConfigResult } from './loadValidatedConfig.js';
2
5
  import type { ChangelogEntry, ReleaseConfig } from './types.js';
3
6
  /**
4
7
  * Result of {@link validateOverridesCommand}: tiered exit code paired with a human-readable
5
- * message. Mirrors the shape of `checkWorkTypesDrift` so the CLI dispatch layer can stay
6
- * uniformly thin.
8
+ * message, which keeps the CLI dispatch layer thin.
7
9
  *
8
10
  * Exit codes:
9
11
  * - `0` — clean: no errors, no warnings.
@@ -16,8 +18,8 @@ export interface ValidateOverridesCommandResult {
16
18
  }
17
19
  /** Injection seams for unit testing. Production callers leave defaults; tests substitute deterministic fakes. */
18
20
  export interface ValidateOverridesCommandDependencies {
19
- discoverWorkspaces?: () => Promise<string[] | undefined>;
20
- loadConfig?: () => Promise<unknown>;
21
+ discoverWorkspaces?: () => WorkspaceDiscovery;
22
+ loadValidatedConfig?: () => Promise<LoadValidatedConfigResult>;
21
23
  /**
22
24
  * Build changelog entries for a scope. Defaults to `buildChangelogEntries`, the same path
23
25
  * `release-kit prepare` uses — anchoring `validate`'s hash universe to `prepare`'s by
@@ -34,11 +36,15 @@ export interface ValidateOverridesCommandDependencies {
34
36
  * {@link validateAllChangelogOverrides}.
35
37
  *
36
38
  * Single-package and monorepo modes are handled uniformly: single-package collapses to one
37
- * project scope; monorepo expands to a project scope plus one scope per workspace.
39
+ * project scope; monorepo expands to a project scope plus one scope per workspace. A workspace declaring
40
+ * patterns that resolve to no package is neither, and exits `2`.
41
+ *
42
+ * `configPath` names the config file to read, relative to the working directory; it defaults to
43
+ * `CONFIG_FILE_PATH`.
38
44
  */
39
- export declare function validateOverridesCommand(dependencies?: ValidateOverridesCommandDependencies): Promise<ValidateOverridesCommandResult>;
45
+ export declare function validateOverridesCommand(styles: StreamStyles, configPath?: string, dependencies?: ValidateOverridesCommandDependencies): Promise<ValidateOverridesCommandResult>;
40
46
  /**
41
47
  * Pure formatter — take an aggregated validation result, return the tiered exit code and a
42
48
  * rendered message. Exported for unit testing without going through the full discovery path.
43
49
  */
44
- export declare function formatValidateOverridesResult(result: ValidateAllChangelogOverridesResult): ValidateOverridesCommandResult;
50
+ export declare function formatValidateOverridesResult(result: ValidateAllChangelogOverridesResult, style: OutputStyle): ValidateOverridesCommandResult;
@@ -1,60 +1,57 @@
1
- import { formatErrorLine } from '@williamthorsen/nmr-core';
1
+ import { formatErrorLine, formatStatusLine } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { buildChangelogEntries } from "./buildChangelogEntries.js";
4
4
  import { resolveOverridePath, validateAllChangelogOverrides, } from "./changelogOverrides.js";
5
- import { discoverWorkspaces } from "./discoverWorkspaces.js";
5
+ import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
6
6
  import { buildTagPattern, getAllTagPrefixes } from "./generateChangelogs.js";
7
- import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
8
- import { validateConfig } from "./validateConfig.js";
7
+ import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
8
+ import { loadValidatedConfig } from "./loadValidatedConfig.js";
9
9
  const SYNTHETIC_VALIDATE_TAG = 'validate-only';
10
- export async function validateOverridesCommand(dependencies = {}) {
10
+ export async function validateOverridesCommand(styles, configPath, dependencies = {}) {
11
11
  const discover = dependencies.discoverWorkspaces ?? discoverWorkspaces;
12
- const load = dependencies.loadConfig ?? loadConfig;
12
+ const load = dependencies.loadValidatedConfig ?? (() => loadValidatedConfig(configPath));
13
13
  const buildEntries = dependencies.buildEntries ?? defaultBuildEntries;
14
14
  const validate = dependencies.validate ?? validateAllChangelogOverrides;
15
- let rawConfig;
16
- try {
17
- rawConfig = await load();
18
- }
19
- catch (error) {
20
- return { exitCode: 2, message: formatErrorLine(`Failed to load config: ${describeError(error)}`) };
21
- }
22
- let userConfig;
23
- try {
24
- userConfig = validateLoadedConfig(rawConfig);
25
- }
26
- catch (error) {
27
- return { exitCode: 2, message: describeError(error) };
15
+ const configResult = await load();
16
+ if (configResult.status === 'invalid') {
17
+ return { exitCode: 2, message: formatConfigProblem(configResult.problem) };
28
18
  }
29
- let discoveredPaths;
19
+ const userConfig = configResult.status === 'ok' ? configResult.config : undefined;
20
+ let workspace;
30
21
  try {
31
- discoveredPaths = await discover();
22
+ workspace = discover();
32
23
  }
33
24
  catch (error) {
34
25
  return { exitCode: 2, message: formatErrorLine(`Failed to discover workspaces: ${describeError(error)}`) };
35
26
  }
27
+ if (workspace.kind === 'empty') {
28
+ return {
29
+ exitCode: 2,
30
+ message: formatErrorLine(`No workspace package to validate. ${describeEmptyWorkspace(workspace)}`),
31
+ };
32
+ }
36
33
  let inputs;
37
34
  try {
38
35
  inputs =
39
- discoveredPaths === undefined
36
+ workspace.kind === 'single-package'
40
37
  ? buildSinglePackageInputs(userConfig, buildEntries)
41
- : buildMonorepoInputs(discoveredPaths, userConfig, buildEntries);
38
+ : buildMonorepoInputs(workspace.packageDirs, userConfig, buildEntries);
42
39
  }
43
40
  catch (error) {
44
41
  return { exitCode: 2, message: formatErrorLine(`Failed to resolve overrides scope: ${describeError(error)}`) };
45
42
  }
46
43
  const result = validate(inputs);
47
- return formatValidateOverridesResult(result);
44
+ return formatValidateOverridesResult(result, styles.stderr);
48
45
  }
49
- export function formatValidateOverridesResult(result) {
46
+ export function formatValidateOverridesResult(result, style) {
50
47
  const { errors, warnings } = result;
51
48
  if (errors.length === 0 && warnings.length === 0) {
52
49
  return { exitCode: 0, message: 'All override files are valid (no errors, no stale keys).' };
53
50
  }
54
51
  const exitCode = errors.length > 0 ? 2 : 1;
55
52
  const summary = formatSummaryLine(errors.length, warnings.length);
56
- const errorLines = errors.map((message) => ` ❌ ${message}`);
57
- const warningLines = warnings.map((message) => ` ⚠️ ${message}`);
53
+ const errorLines = errors.map((message) => ` ${formatStatusLine(style, 'failed', message)}`);
54
+ const warningLines = warnings.map((message) => ` ${formatStatusLine(style, 'warning', message)}`);
58
55
  const message = [summary, '', ...errorLines, ...warningLines].join('\n');
59
56
  return { exitCode, message };
60
57
  }
@@ -94,15 +91,10 @@ function flattenEntriesToHashes(entries) {
94
91
  }
95
92
  return hashes;
96
93
  }
97
- function validateLoadedConfig(rawConfig) {
98
- if (rawConfig === undefined) {
99
- return undefined;
100
- }
101
- const { config, errors } = validateConfig(rawConfig);
102
- if (errors.length > 0) {
103
- throw new Error(`Invalid config:\n - ${errors.join('\n - ')}`);
104
- }
105
- return config;
94
+ function formatConfigProblem(problem) {
95
+ return problem.kind === 'load'
96
+ ? formatErrorLine(`Failed to load config: ${problem.message}`)
97
+ : `Invalid config:\n - ${problem.errors.join('\n - ')}`;
106
98
  }
107
99
  function buildSinglePackageInputs(userConfig, buildEntries) {
108
100
  const config = mergeSinglePackageConfig(userConfig);
@@ -1,9 +1,19 @@
1
1
  # Configuration
2
2
 
3
- The fields of `.config/release-kit.config.ts`, how a workspace declares its prior identities or a retired package, the rule against colliding tag prefixes, and the patterns that decide a bump.
3
+ The fields of `.config/release-kit.config.ts`, how a workspace declares its prior identities or a retired package, the rule against colliding tag prefixes, the patterns that decide a bump, and the environment variable that sets the output style.
4
4
 
5
5
  A minimal config file is in the [README](../README.md#configuration).
6
6
 
7
+ ## Config file location
8
+
9
+ release-kit reads `.config/release-kit.config.ts`, resolved against the working directory. A run reads a different file when the subcommand is given `--config <path>`, which is also resolved against the working directory.
10
+
11
+ The flag is accepted by every subcommand that reads a config: `prepare`, `publish`, `create-github-release`, `show-tag-prefixes`, `overrides validate`, `sync-labels init`, and `sync-labels generate`. `commit`, `tag`, `push`, and `sync-labels sync` read no config and reject it.
12
+
13
+ The two paths differ in what an absent file means. An absent default path means the repo declares no config, and the run proceeds on discovered defaults. An absent `--config` path fails the command with an error naming the resolved path, because a caller who names a file expects that file to be read. A file that exists and fails validation is an error on both paths alike, and so is one that exists and fails to load.
14
+
15
+ `--config` names a file to read, never a write target. `release-kit init --with-config` scaffolds to `.config/release-kit.config.ts` and takes no `--config`, and `sync-labels init` writes its seeded `repoLabels` block to that same path.
16
+
7
17
  ## `ReleaseKitConfig` reference
8
18
 
9
19
  | Field | Type | Description |
@@ -89,6 +99,8 @@ Print a per-workspace table of derived tag prefixes, tag counts, and declared le
89
99
 
90
100
  Exits `0` when every workspace derives a prefix and there are no cross-workspace collisions; exits `1` on any derivation failure or collision. Undeclared candidates do not affect the exit code — they surface as a warning via the `legacy tag prefixes are declared` readyup check.
91
101
 
102
+ A config file that exists and either fails to load or fails schema validation reports the failure on stderr and exits `1`, printing no preview, because a config that cannot be read leaves the declared-versus-undeclared verdict wrong in the one dimension the command exists to report on. That holds on the default path and under `--config`, and in single-package mode as well as monorepo mode. An absent default config is a supported state and still previews against derived defaults.
103
+
92
104
  In single-package mode, prints a single row with `workspacePath = .` and `derivedPrefix = v`; legacy entries and undeclared-candidate scanning are not applicable.
93
105
 
94
106
  ## `VersionPatterns`
@@ -103,3 +115,25 @@ interface VersionPatterns {
103
115
  ```
104
116
 
105
117
  Default: `{ major: ['!'], minor: ['feat'] }`
118
+
119
+ ## Output style
120
+
121
+ release-kit prints in one of two styles. `rich` marks each status with an emoji, such as ✅ or 🟠. `plain` replaces each emoji with a word, such as `PASS` or `WARN`, which a reader of a log can search for. The style is decided separately for stdout and stderr, so redirecting one stream to a file leaves the other rich at a terminal.
122
+
123
+ The `RELEASE_KIT_OUTPUT_STYLE` environment variable sets the style for every command, `init` and `sync-labels init` included:
124
+
125
+ | Value | Effect |
126
+ | ------- | ------------------------------------------------------------------------ |
127
+ | `auto` | Detect the style of each stream. The same as leaving the variable unset. |
128
+ | `plain` | Print plain on both streams. |
129
+ | `rich` | Print rich on both streams, even in a pipe or under CI. |
130
+
131
+ Any other value is a usage error: release-kit reports it and exits with code 1.
132
+
133
+ The variable outranks detection. Under `auto`, release-kit defers to detection, which reads `CI`, then the stream, then `TERM`. A stream is plain when any of these holds, and rich otherwise:
134
+
135
+ 1. `CI` is set to anything other than an empty string or `false`.
136
+ 2. The stream is not a terminal, as when its output is piped or redirected.
137
+ 3. `TERM` is `linux`, the Linux virtual console, which draws no emoji.
138
+
139
+ The style governs emoji alone. Changelog section emoji are file content rather than terminal output, and they are unaffected.
package/docs/labels.md CHANGED
@@ -8,6 +8,8 @@ Manage GitHub label definitions via the `repoLabels` block of `.config/release-k
8
8
 
9
9
  `init` scaffolds the `.github/workflows/sync-labels.yaml` caller workflow and seeds a `repoLabels` block with scope labels discovered from workspaces and declared `retiredPackages`. When `.config/release-kit.config.ts` does not exist, `init` writes it; when it does, `init` prints the block for manual paste — a hand-authored config is never rewritten. `generate` resolves the block and writes `.github/labels.yaml`; with `--check` it regenerates in memory and exits non-zero if the committed file is stale or missing, writing nothing. `sync` triggers the workflow remotely — it requires the `gh` CLI and an existing workflow file.
10
10
 
11
+ `init` and `generate` accept `--config <path>` to read a config elsewhere; `sync` reads none and rejects the flag. Under `--config`, `init` reads the named file and prints the block for manual paste, and fails when that file does not exist rather than scaffolding one, so its write target stays `.config/release-kit.config.ts`. See [Config file location](configuration.md#config-file-location).
12
+
11
13
  Every `sync-labels` subcommand refuses to run while the retired `.config/sync-labels.config.ts` is present, so custom labels cannot be silently dropped mid-migration, and the refusal names what to move and where.
12
14
 
13
15
  ### Label configuration
@@ -1,8 +1,8 @@
1
1
  # Work types and tiers
2
2
 
3
- The taxonomy against which release-kit parses commits: its tiers, the breaking-change policy, section markers, customization, and the commands that compare it with its upstream.
3
+ The taxonomy against which release-kit parses commits: its tiers, the breaking-change policy, section markers, customization, and the maintainer scripts that keep release-kit's copy level with its upstream.
4
4
 
5
- release-kit bundles a copy of the codeassembly canonical taxonomy in `packages/release-kit/src/work-types.json`, kept level by `release-kit work-types sync`. The taxonomy is split into three tiers that drive section rendering and audience classification.
5
+ release-kit bundles a copy of the codeassembly canonical taxonomy in `packages/release-kit/src/work-types.json`, kept level by the [maintainer scripts](#maintaining-the-bundled-taxonomy). The taxonomy is split into three tiers that drive section rendering and audience classification.
6
6
 
7
7
  | Tier | Key | Header | Aliases | `!` policy |
8
8
  | -------- | ----------- | ------------------------- | ------------- | ------------ |
@@ -53,7 +53,7 @@ The release-prepare orchestrators (`releasePrepare`, `releasePrepareMono`, `rele
53
53
  ```
54
54
  arrays
55
55
  Found 1 commits since arrays-v1.0.0
56
- ⚠️ 1 policy violation:
56
+ 🟠 1 policy violation:
57
57
  · def5678 'internal!: refactor cache' — type 'internal' at prefix surface
58
58
  Bumping versions (patch)...
59
59
  📦 1.0.0 → 1.0.1 (patch)
@@ -103,14 +103,17 @@ Work types from your config are merged with these defaults by key — your entri
103
103
 
104
104
  The default `devOnlySections` (excluded from public release notes but still written to `CHANGELOG.md`) are derived from the `internal` and `process` tiers (excluding `fmt`). Override via `changelogJson.devOnlySections` in your config; matching is decorator-tolerant, so a bare-name override like `['Internal features']` keeps working against the emoji-prefixed and prefix-decorated default titles.
105
105
 
106
- ## `release-kit work-types`
106
+ ## Maintaining the bundled taxonomy
107
107
 
108
- Manage the canonical work-types taxonomy used by changelog and release-notes generation.
108
+ `src/workTypesData.ts` mirrors `src/work-types.json` for runtime use. Two maintainer scripts of the release-kit package compare that JSON with its codeassembly upstream and update it. They exist only in a checkout of this repository and are not part of the published `release-kit` CLI.
109
109
 
110
- `check` exits 0 on a match, 1 on drift, 2 when the upstream fetch fails, and 3 when either file is not valid JSON or the upstream fails the shape check. When the upstream URL returns 404, `check` exits 0 with a warning.
110
+ - `nmr work-types:check` compares the content of `src/work-types.json` with the upstream, ignoring the local `$schema` hint.
111
+ - `nmr work-types:sync` overwrites `src/work-types.json` with the upstream when their content differs.
112
+
113
+ Both run from `packages/release-kit`, or from anywhere in the repository as `nmr -F @williamthorsen/release-kit work-types:check` and `nmr -F @williamthorsen/release-kit work-types:sync`. After `sync` writes the file, update `src/workTypesData.ts` to match; `workTypesData.unit.test.ts` fails until the two agree.
111
114
 
112
- These commands are also exposed as `nmr work-types:check` / `nmr work-types:sync` from any package directory.
115
+ `check` exits 0 on a match, 1 on drift, 2 when the upstream fetch fails, and 3 when either file is not valid JSON or the upstream fails the shape check. When the upstream URL returns 404, `check` exits 0 with a warning.
113
116
 
114
117
  ### Authenticated fetches
115
118
 
116
- The upstream codeassembly repo is public, so `check` and `sync` need no token. When `GITHUB_TOKEN` is set in the environment, both commands send it as `Authorization: Bearer <token>`.
119
+ The upstream codeassembly repo is public, so `check` and `sync` need no token. When `GITHUB_TOKEN` is set in the environment, both scripts send it as `Authorization: Bearer <token>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/release-kit",
3
- "version": "10.8.0",
3
+ "version": "11.0.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -51,34 +51,33 @@
51
51
  "schemas/**"
52
52
  ],
53
53
  "dependencies": {
54
- "@williamthorsen/nmr-core": "0.11.1",
55
- "@williamthorsen/toolbelt.errors": "0.7.0",
56
- "glob": "13.0.6",
54
+ "@williamthorsen/nmr-core": "0.13.0",
55
+ "@williamthorsen/toolbelt.errors": "0.8.1",
57
56
  "json-stringify-pretty-compact": "4.0.0",
58
57
  "semver": "7.8.5",
59
- "yaml": "2.9.0",
60
- "zod": "4.5.4"
58
+ "yaml": "2.9.1",
59
+ "zod": "4.6.5"
61
60
  },
62
61
  "devDependencies": {
63
62
  "@types/semver": "7.8.0",
64
- "@williamthorsen/toolbelt.filesystem": "0.8.3",
65
- "@williamthorsen/toolbelt.testing": "0.5.1",
66
- "@williamthorsen/toolbelt.vitest": "0.9.0",
67
- "readyup": "0.36.0",
63
+ "@williamthorsen/toolbelt.filesystem": "0.9.1",
64
+ "@williamthorsen/toolbelt.testing": "0.6.1",
65
+ "@williamthorsen/toolbelt.vitest": "0.10.1",
66
+ "readyup": "0.38.0",
68
67
  "smol-toml": "1.8.0",
69
68
  "vitest": "4.1.11"
70
69
  },
71
70
  "engines": {
72
- "node": ">=24"
71
+ "node": ">=24.16.0"
73
72
  },
74
73
  "publishConfig": {
75
74
  "access": "public",
76
75
  "registry": "https://registry.npmjs.org"
77
76
  },
78
77
  "scripts": {
79
- "build:post": "node scripts/copy-work-types.js && rdy compile",
78
+ "build:post": "rdy compile",
80
79
  "build:pre": "rdy verify",
81
- "work-types:check": "pnpm exec release-kit work-types check",
82
- "work-types:sync": "pnpm exec release-kit work-types sync"
80
+ "work-types:check": "node scripts/check-work-types.ts",
81
+ "work-types:sync": "node scripts/sync-work-types.ts"
83
82
  }
84
83
  }
@@ -4,7 +4,7 @@
4
4
  # Main categories
5
5
  - name: ai
6
6
  color: a2eeef
7
- description: Documentation and configs used by agents
7
+ description: Guidance on working in this repo, such as its AGENTS.md or a repo-local skill, or its agent config
8
8
  - name: breaking
9
9
  color: b60205
10
10
  description: Breaking API change or removal of functionality
@@ -16,7 +16,7 @@
16
16
  description: Marks functionality for future removal
17
17
  - name: documentation
18
18
  color: a2eeef
19
- description: Improvements or additions to documentation
19
+ description: Documentation for human readers, such as a README, a guide, or a code comment
20
20
  - name: feature
21
21
  color: '0075ca'
22
22
  description: Added or improved external functionality
@@ -1,35 +0,0 @@
1
- /** URL of the upstream canonical `work-types.json` published by codeassembly. */
2
- export declare const UPSTREAM_WORK_TYPES_URL = "https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json";
3
- /** Outcome of a drift check, paired with a human-readable message. */
4
- export interface DriftCheckResult {
5
- /**
6
- * Process exit code semantics:
7
- * - `0` — match (or upstream not found, with a warning).
8
- * - `1` — drift detected.
9
- * - `2` — network error.
10
- * - `3` — schema mismatch (upstream JSON does not parse or fails the schema invariants).
11
- */
12
- exitCode: 0 | 1 | 2 | 3;
13
- message: string;
14
- }
15
- /** Minimal injection seam so unit tests can substitute a deterministic fetcher. */
16
- export interface CheckWorkTypesDriftDependencies {
17
- /** Absolute path of the local `work-types.json`. Defaults to the bundled file. */
18
- localPath?: string;
19
- /** HTTP fetcher. Defaults to global `fetch` (Node 18+). */
20
- fetch?: typeof globalThis.fetch;
21
- /** Override the upstream URL (used by tests; production callers should leave default). */
22
- upstreamUrl?: string;
23
- }
24
- /**
25
- * Compare the bundled `work-types.json` against the upstream codeassembly canonical and
26
- * report drift.
27
- *
28
- * Failure modes:
29
- * - Network error → exit 2 with a diagnostic.
30
- * - Upstream 404 → exit 0 with a warning.
31
- * - Schema mismatch → exit 3 with a diagnostic.
32
- * - Drift → exit 1 with a unified-diff-style message.
33
- * - Match → exit 0.
34
- */
35
- export declare function checkWorkTypesDrift(dependencies?: CheckWorkTypesDriftDependencies): Promise<DriftCheckResult>;