@williamthorsen/release-kit 10.8.0 → 10.9.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 (51) hide show
  1. package/.meta/changelog.json +43 -3
  2. package/.readyup/kits/default.js +3 -3
  3. package/.readyup/kits/npm-auto-publish.js +1 -1
  4. package/.readyup/manifest.json +9 -9
  5. package/CHANGELOG.md +34 -3
  6. package/README.md +25 -9
  7. package/dist/esm/bin/release-kit.js +20 -102
  8. package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
  9. package/dist/esm/createGithubReleaseCommand.js +4 -2
  10. package/dist/esm/createTags.d.ts +2 -0
  11. package/dist/esm/createTags.js +5 -4
  12. package/dist/esm/format.d.ts +5 -5
  13. package/dist/esm/format.js +5 -4
  14. package/dist/esm/glyphs.d.ts +3 -0
  15. package/dist/esm/glyphs.js +6 -0
  16. package/dist/esm/init/initCommand.d.ts +3 -1
  17. package/dist/esm/init/initCommand.js +15 -15
  18. package/dist/esm/loadValidatedConfig.d.ts +2 -1
  19. package/dist/esm/loadValidatedConfig.js +3 -3
  20. package/dist/esm/prepareCommand.d.ts +2 -1
  21. package/dist/esm/prepareCommand.js +17 -16
  22. package/dist/esm/publishCommand.d.ts +2 -1
  23. package/dist/esm/publishCommand.js +2 -2
  24. package/dist/esm/reportPrepare.d.ts +2 -0
  25. package/dist/esm/reportPrepare.js +44 -38
  26. package/dist/esm/resolveReleaseNotesConfig.d.ts +2 -1
  27. package/dist/esm/resolveReleaseNotesConfig.js +4 -4
  28. package/dist/esm/resolveStylesOrExit.d.ts +10 -0
  29. package/dist/esm/resolveStylesOrExit.js +10 -0
  30. package/dist/esm/showTagPrefixesCommand.d.ts +2 -1
  31. package/dist/esm/showTagPrefixesCommand.js +16 -10
  32. package/dist/esm/sync-labels/generateCommand.d.ts +4 -2
  33. package/dist/esm/sync-labels/generateCommand.js +4 -4
  34. package/dist/esm/sync-labels/initCommand.d.ts +3 -1
  35. package/dist/esm/sync-labels/initCommand.js +7 -7
  36. package/dist/esm/tagCommand.d.ts +2 -1
  37. package/dist/esm/tagCommand.js +2 -2
  38. package/dist/esm/validateOverridesCommand.d.ts +4 -4
  39. package/dist/esm/validateOverridesCommand.js +6 -6
  40. package/docs/configuration.md +23 -1
  41. package/docs/work-types.md +11 -8
  42. package/package.json +11 -11
  43. package/presets/labels/common.yaml +2 -2
  44. package/dist/esm/checkWorkTypesDrift.d.ts +0 -35
  45. package/dist/esm/checkWorkTypesDrift.js +0 -117
  46. package/dist/esm/syncWorkTypes.d.ts +0 -28
  47. package/dist/esm/syncWorkTypes.js +0 -94
  48. package/dist/esm/work-types.json +0 -149
  49. package/dist/esm/work-types.schema.json +0 -110
  50. package/dist/esm/workTypesUtils.d.ts +0 -22
  51. package/dist/esm/workTypesUtils.js +0 -16
@@ -1,12 +1,13 @@
1
+ import { formatStatusLine } from '@williamthorsen/nmr-core';
1
2
  import { detectRepoType } from "./init/detectRepoType.js";
2
3
  import { previewTagPrefixes } from "./previewTagPrefixes.js";
3
- export async function showTagPrefixesCommand() {
4
+ export async function showTagPrefixesCommand(styles) {
4
5
  if (detectRepoType() === 'single-package') {
5
6
  process.stdout.write(renderSinglePackage());
6
7
  return 0;
7
8
  }
8
9
  const preview = await previewTagPrefixes();
9
- process.stdout.write(renderMonorepo(preview));
10
+ process.stdout.write(renderMonorepo(preview, styles.stdout));
10
11
  return computeExitCode(preview);
11
12
  }
12
13
  function renderSinglePackage() {
@@ -17,13 +18,13 @@ function renderSinglePackage() {
17
18
  ];
18
19
  return lines.join('\n');
19
20
  }
20
- function renderMonorepo(preview) {
21
+ function renderMonorepo(preview, style) {
21
22
  const lines = ['Workspace tag prefixes:', ''];
22
23
  for (const row of preview.workspaces) {
23
- lines.push(...renderWorkspaceRow(row));
24
+ lines.push(...renderWorkspaceRow(row, style));
24
25
  }
25
26
  if (preview.collisions.length > 0) {
26
- lines.push('', ...preview.collisions.map((collision) => `⛔ tag prefix collision: '${collision.tagPrefix}' used by ${collision.workspacePaths.join(', ')}`));
27
+ lines.push('', ...preview.collisions.map((collision) => formatStatusLine(style, 'failed', `tag prefix collision: '${collision.tagPrefix}' used by ${collision.workspacePaths.join(', ')}`)));
27
28
  }
28
29
  if (preview.undeclaredCandidates.length > 0) {
29
30
  lines.push('', 'Undeclared tag prefixes:', '', ...preview.undeclaredCandidates.map((candidate) => ` '${candidate.prefix}' — ${candidate.tagCount} tags (e.g., ${candidate.exampleTags.join(', ')})`), '', 'Suggested config snippet (adjust `dir` to match your workspace if the guess is wrong, and replace the `name` placeholder with the legacy npm name):', '', renderSuggestedSnippet(preview.undeclaredCandidates), '', "If the suggested `dir` does not match your workspace, adjust before pasting. Each legacy identity requires a `name` — replace the `TODO-fill-in-legacy-npm-name` placeholder with the package's prior npm name.");
@@ -31,20 +32,25 @@ function renderMonorepo(preview) {
31
32
  lines.push('');
32
33
  return lines.join('\n');
33
34
  }
34
- function renderWorkspaceRow(row) {
35
+ function renderWorkspaceRow(row, style) {
35
36
  const lines = [];
36
37
  if (row.derivedPrefix === null) {
37
- lines.push(` ${row.workspacePath} — ⛔ derivation failed: ${row.derivationError ?? 'unknown error'}`);
38
+ const failure = formatStatusLine(style, 'failed', `derivation failed: ${row.derivationError ?? 'unknown error'}`);
39
+ lines.push(` ${row.workspacePath} — ${failure}`);
38
40
  return lines;
39
41
  }
40
- const statusMarker = row.derivedTagCount > 0 ? `✅ ${row.derivedTagCount} tags` : '⚠️ no existing tags';
42
+ const statusMarker = row.derivedTagCount > 0
43
+ ? formatStatusLine(style, 'passed', `${row.derivedTagCount} tags`)
44
+ : formatStatusLine(style, 'warning', 'no existing tags');
41
45
  lines.push(` ${row.workspacePath} — derived prefix '${row.derivedPrefix}', ${statusMarker}`);
42
46
  for (const entry of row.legacyEntries) {
43
47
  if (entry.tagCount > 0) {
44
- lines.push(` ✅ ${entry.tagCount} legacy tags with '${entry.prefix}' prefix (recognized)`);
48
+ const recognized = `${entry.tagCount} legacy tags with '${entry.prefix}' prefix (recognized)`;
49
+ lines.push(` ${formatStatusLine(style, 'passed', recognized)}`);
45
50
  }
46
51
  else {
47
- lines.push(` ⚠️ recorded legacy prefix '${entry.prefix}' has no tags`);
52
+ const tagless = `recorded legacy prefix '${entry.prefix}' has no tags`;
53
+ lines.push(` ${formatStatusLine(style, 'warning', tagless)}`);
48
54
  }
49
55
  }
50
56
  return lines;
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle, type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  import type { RepoLabelsConfig } from '../types.js';
2
3
  import type { LabelDefinition } from './types.js';
3
4
  /** Output path for the generated labels file. */
@@ -10,11 +11,12 @@ export declare function formatLabelsYaml(labels: LabelDefinition[], presetHashes
10
11
  * Reports an actionable error and returns `undefined` when the config file is missing,
11
12
  * fails to load or validate, or carries no `repoLabels` block.
12
13
  */
13
- export declare function loadRepoLabelsConfig(): Promise<RepoLabelsConfig | undefined>;
14
+ export declare function loadRepoLabelsConfig(stderrStyle: OutputStyle): Promise<RepoLabelsConfig | undefined>;
14
15
  /** Options for the `sync-labels generate` subcommand. */
15
16
  interface GenerateOptions {
16
17
  /** Compare the regenerated content against the committed file instead of writing. */
17
18
  check?: boolean;
19
+ styles: StreamStyles;
18
20
  }
19
21
  /**
20
22
  * Runs the `sync-labels generate` command.
@@ -23,5 +25,5 @@ interface GenerateOptions {
23
25
  * and writes `.github/labels.yaml` -- or, with `check`, regenerates in memory and reports whether the committed file
24
26
  * is stale, writing nothing. Returns 0 on success, 1 on failure.
25
27
  */
26
- export declare function generateCommand({ check }?: GenerateOptions): Promise<number>;
28
+ export declare function generateCommand({ check, styles }: GenerateOptions): Promise<number>;
27
29
  export {};
@@ -19,8 +19,8 @@ export function formatLabelsYaml(labels, presetHashes) {
19
19
  const yamlBody = stringify(declared, { singleQuote: true, lineWidth: 0 });
20
20
  return headerLines.join('\n') + '\n' + yamlBody;
21
21
  }
22
- export async function loadRepoLabelsConfig() {
23
- const result = await loadValidatedConfig();
22
+ export async function loadRepoLabelsConfig(stderrStyle) {
23
+ const result = await loadValidatedConfig(stderrStyle);
24
24
  if (result.status === 'missing') {
25
25
  reportError(`No config file found at ${CONFIG_FILE_PATH}. Run \`release-kit sync-labels init\` first.`);
26
26
  return undefined;
@@ -34,11 +34,11 @@ export async function loadRepoLabelsConfig() {
34
34
  }
35
35
  return result.config.repoLabels;
36
36
  }
37
- export async function generateCommand({ check = false } = {}) {
37
+ export async function generateCommand({ check = false, styles }) {
38
38
  if (checkRetiredSyncLabelsConfig()) {
39
39
  return 1;
40
40
  }
41
- const repoLabels = await loadRepoLabelsConfig();
41
+ const repoLabels = await loadRepoLabelsConfig(styles.stderr);
42
42
  if (repoLabels === undefined) {
43
43
  return 1;
44
44
  }
@@ -1,7 +1,9 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  /** Options for the `sync-labels init` subcommand. */
2
3
  interface InitOptions {
3
4
  dryRun: boolean;
4
5
  force: boolean;
6
+ styles: StreamStyles;
5
7
  }
6
8
  /**
7
9
  * Run the `sync-labels init` subcommand.
@@ -11,5 +13,5 @@ interface InitOptions {
11
13
  * printed to stdout for manual paste when the file is already there — an existing,
12
14
  * hand-authored config is never rewritten. Returns 0 on success, 1 on failure.
13
15
  */
14
- export declare function syncLabelsInitCommand({ dryRun, force }: InitOptions): Promise<number>;
16
+ export declare function syncLabelsInitCommand({ dryRun, force, styles }: InitOptions): Promise<number>;
15
17
  export {};
@@ -8,7 +8,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 }) {
11
+ export async function syncLabelsInitCommand({ dryRun, force, styles }) {
12
12
  if (checkRetiredSyncLabelsConfig()) {
13
13
  return 1;
14
14
  }
@@ -32,14 +32,14 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
32
32
  console.info(` Found ${String(workspacePaths.length)} workspaces`);
33
33
  }
34
34
  const configExists = existsSync(CONFIG_FILE_PATH);
35
- const retiredNames = configExists ? await loadRetiredPackageNames() : [];
35
+ const retiredNames = configExists ? await loadRetiredPackageNames(styles) : [];
36
36
  if (retiredNames === undefined) {
37
37
  return 1;
38
38
  }
39
39
  const scopeLabels = workspacePaths === undefined ? [] : buildScopeLabels(workspacePaths, retiredNames);
40
40
  console.info('\n> Scaffolding files');
41
41
  const workflowResult = writeFileWithCheck(WORKFLOW_PATH, syncLabelsWorkflow(), { dryRun, overwrite: force });
42
- reportWriteResult(workflowResult, dryRun);
42
+ reportWriteResult(workflowResult, dryRun, styles);
43
43
  if (configExists) {
44
44
  console.info(`\n> ${CONFIG_FILE_PATH} already exists; add this block to the object passed to defineConfig:\n`);
45
45
  console.info(renderRepoLabelsBlock(scopeLabels));
@@ -60,7 +60,7 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
60
60
  dryRun,
61
61
  overwrite: force,
62
62
  });
63
- reportWriteResult(configResult, dryRun);
63
+ reportWriteResult(configResult, dryRun, styles);
64
64
  if (workflowResult.outcome === 'failed' || configResult.outcome === 'failed') {
65
65
  process.stderr.write('Failed to scaffold one or more files.\n');
66
66
  return 1;
@@ -70,7 +70,7 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
70
70
  }
71
71
  else {
72
72
  console.info('\n> Generating labels');
73
- const generateExitCode = await generateCommand();
73
+ const generateExitCode = await generateCommand({ styles });
74
74
  if (generateExitCode !== 0) {
75
75
  return generateExitCode;
76
76
  }
@@ -87,8 +87,8 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
87
87
  `);
88
88
  return 0;
89
89
  }
90
- async function loadRetiredPackageNames() {
91
- const result = await loadValidatedConfig();
90
+ async function loadRetiredPackageNames(styles) {
91
+ const result = await loadValidatedConfig(styles.stderr);
92
92
  if (result.status === 'invalid') {
93
93
  return undefined;
94
94
  }
@@ -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,9 @@
1
+ import { type OutputStyle, type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  import { type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult } from './changelogOverrides.js';
2
3
  import type { ChangelogEntry, ReleaseConfig } from './types.js';
3
4
  /**
4
5
  * 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.
6
+ * message, which keeps the CLI dispatch layer thin.
7
7
  *
8
8
  * Exit codes:
9
9
  * - `0` — clean: no errors, no warnings.
@@ -36,9 +36,9 @@ export interface ValidateOverridesCommandDependencies {
36
36
  * Single-package and monorepo modes are handled uniformly: single-package collapses to one
37
37
  * project scope; monorepo expands to a project scope plus one scope per workspace.
38
38
  */
39
- export declare function validateOverridesCommand(dependencies?: ValidateOverridesCommandDependencies): Promise<ValidateOverridesCommandResult>;
39
+ export declare function validateOverridesCommand(styles: StreamStyles, dependencies?: ValidateOverridesCommandDependencies): Promise<ValidateOverridesCommandResult>;
40
40
  /**
41
41
  * Pure formatter — take an aggregated validation result, return the tiered exit code and a
42
42
  * rendered message. Exported for unit testing without going through the full discovery path.
43
43
  */
44
- export declare function formatValidateOverridesResult(result: ValidateAllChangelogOverridesResult): ValidateOverridesCommandResult;
44
+ export declare function formatValidateOverridesResult(result: ValidateAllChangelogOverridesResult, style: OutputStyle): ValidateOverridesCommandResult;
@@ -1,4 +1,4 @@
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";
@@ -7,7 +7,7 @@ import { buildTagPattern, getAllTagPrefixes } from "./generateChangelogs.js";
7
7
  import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
8
8
  import { validateConfig } from "./validateConfig.js";
9
9
  const SYNTHETIC_VALIDATE_TAG = 'validate-only';
10
- export async function validateOverridesCommand(dependencies = {}) {
10
+ export async function validateOverridesCommand(styles, dependencies = {}) {
11
11
  const discover = dependencies.discoverWorkspaces ?? discoverWorkspaces;
12
12
  const load = dependencies.loadConfig ?? loadConfig;
13
13
  const buildEntries = dependencies.buildEntries ?? defaultBuildEntries;
@@ -44,17 +44,17 @@ export async function validateOverridesCommand(dependencies = {}) {
44
44
  return { exitCode: 2, message: formatErrorLine(`Failed to resolve overrides scope: ${describeError(error)}`) };
45
45
  }
46
46
  const result = validate(inputs);
47
- return formatValidateOverridesResult(result);
47
+ return formatValidateOverridesResult(result, styles.stderr);
48
48
  }
49
- export function formatValidateOverridesResult(result) {
49
+ export function formatValidateOverridesResult(result, style) {
50
50
  const { errors, warnings } = result;
51
51
  if (errors.length === 0 && warnings.length === 0) {
52
52
  return { exitCode: 0, message: 'All override files are valid (no errors, no stale keys).' };
53
53
  }
54
54
  const exitCode = errors.length > 0 ? 2 : 1;
55
55
  const summary = formatSummaryLine(errors.length, warnings.length);
56
- const errorLines = errors.map((message) => ` ❌ ${message}`);
57
- const warningLines = warnings.map((message) => ` ⚠️ ${message}`);
56
+ const errorLines = errors.map((message) => ` ${formatStatusLine(style, 'failed', message)}`);
57
+ const warningLines = warnings.map((message) => ` ${formatStatusLine(style, 'warning', message)}`);
58
58
  const message = [summary, '', ...errorLines, ...warningLines].join('\n');
59
59
  return { exitCode, message };
60
60
  }
@@ -1,6 +1,6 @@
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
 
@@ -103,3 +103,25 @@ interface VersionPatterns {
103
103
  ```
104
104
 
105
105
  Default: `{ major: ['!'], minor: ['feat'] }`
106
+
107
+ ## Output style
108
+
109
+ 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.
110
+
111
+ The `RELEASE_KIT_OUTPUT_STYLE` environment variable sets the style for every command, `init` and `sync-labels init` included:
112
+
113
+ | Value | Effect |
114
+ | ------- | ------------------------------------------------------------------------ |
115
+ | `auto` | Detect the style of each stream. The same as leaving the variable unset. |
116
+ | `plain` | Print plain on both streams. |
117
+ | `rich` | Print rich on both streams, even in a pipe or under CI. |
118
+
119
+ Any other value is a usage error: release-kit reports it and exits with code 1.
120
+
121
+ 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:
122
+
123
+ 1. `CI` is set to anything other than an empty string or `false`.
124
+ 2. The stream is not a terminal, as when its output is piped or redirected.
125
+ 3. `TERM` is `linux`, the Linux virtual console, which draws no emoji.
126
+
127
+ The style governs emoji alone. Changelog section emoji are file content rather than terminal output, and they are unaffected.
@@ -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": "10.9.0",
4
4
  "description": "Version-bumping and changelog-generation toolkit for release workflows",
5
5
  "keywords": [
6
6
  "changelog",
@@ -51,20 +51,20 @@
51
51
  "schemas/**"
52
52
  ],
53
53
  "dependencies": {
54
- "@williamthorsen/nmr-core": "0.11.1",
55
- "@williamthorsen/toolbelt.errors": "0.7.0",
54
+ "@williamthorsen/nmr-core": "0.12.0",
55
+ "@williamthorsen/toolbelt.errors": "0.8.1",
56
56
  "glob": "13.0.6",
57
57
  "json-stringify-pretty-compact": "4.0.0",
58
58
  "semver": "7.8.5",
59
59
  "yaml": "2.9.0",
60
- "zod": "4.5.4"
60
+ "zod": "4.6.2"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@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",
64
+ "@williamthorsen/toolbelt.filesystem": "0.9.1",
65
+ "@williamthorsen/toolbelt.testing": "0.6.1",
66
+ "@williamthorsen/toolbelt.vitest": "0.10.1",
67
+ "readyup": "0.38.0",
68
68
  "smol-toml": "1.8.0",
69
69
  "vitest": "4.1.11"
70
70
  },
@@ -76,9 +76,9 @@
76
76
  "registry": "https://registry.npmjs.org"
77
77
  },
78
78
  "scripts": {
79
- "build:post": "node scripts/copy-work-types.js && rdy compile",
79
+ "build:post": "rdy compile",
80
80
  "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"
81
+ "work-types:check": "node scripts/check-work-types.ts",
82
+ "work-types:sync": "node scripts/sync-work-types.ts"
83
83
  }
84
84
  }
@@ -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>;
@@ -1,117 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { formatErrorLine } from '@williamthorsen/nmr-core';
5
- import { describeError } from '@williamthorsen/toolbelt.errors';
6
- import { buildFetchInit, hasExpectedTopLevelShape } from "./workTypesUtils.js";
7
- export const UPSTREAM_WORK_TYPES_URL = 'https://raw.githubusercontent.com/williamthorsen/codeassembly/main/packages/agents/content/skills/_data/work-types.json';
8
- function resolveDefaultLocalPath() {
9
- const moduleDir = dirname(fileURLToPath(import.meta.url));
10
- return resolve(moduleDir, 'work-types.json');
11
- }
12
- export async function checkWorkTypesDrift(dependencies = {}) {
13
- const localPath = dependencies.localPath ?? resolveDefaultLocalPath();
14
- const fetcher = dependencies.fetch ?? globalThis.fetch;
15
- const url = dependencies.upstreamUrl ?? UPSTREAM_WORK_TYPES_URL;
16
- const localContent = readFileSync(localPath, 'utf8');
17
- let localJson;
18
- try {
19
- localJson = JSON.parse(localContent);
20
- }
21
- catch (error) {
22
- return {
23
- exitCode: 3,
24
- message: `Local work-types.json is not valid JSON: ${describeError(error)}`,
25
- };
26
- }
27
- const fetchInit = buildFetchInit();
28
- let response;
29
- try {
30
- response = fetchInit === undefined ? await fetcher(url) : await fetcher(url, fetchInit);
31
- }
32
- catch (error) {
33
- return {
34
- exitCode: 2,
35
- message: formatErrorLine(`Failed to fetch upstream work-types.json: ${describeError(error)}`),
36
- };
37
- }
38
- if (response.status === 404) {
39
- return {
40
- exitCode: 0,
41
- message: `Upstream work-types.json not found at ${url}; skipping drift check.`,
42
- };
43
- }
44
- if (!response.ok) {
45
- return {
46
- exitCode: 2,
47
- message: formatErrorLine(`Failed to fetch upstream work-types.json: HTTP ${response.status} ${response.statusText}`),
48
- };
49
- }
50
- const upstreamText = await response.text();
51
- let upstreamJson;
52
- try {
53
- upstreamJson = JSON.parse(upstreamText);
54
- }
55
- catch (error) {
56
- return {
57
- exitCode: 3,
58
- message: `Upstream work-types.json is not valid JSON: ${describeError(error)}`,
59
- };
60
- }
61
- if (!hasExpectedTopLevelShape(upstreamJson)) {
62
- return {
63
- exitCode: 3,
64
- message: 'Upstream work-types.json does not match the expected schema shape (missing `tiers` or `types`).',
65
- };
66
- }
67
- const normalisedLocal = stripLocalOnlyFields(localJson);
68
- if (deepEqual(normalisedLocal, upstreamJson)) {
69
- return {
70
- exitCode: 0,
71
- message: 'Local work-types.json matches upstream.',
72
- };
73
- }
74
- return {
75
- exitCode: 1,
76
- message: `Drift detected. Local and upstream work-types.json differ. Run \`nmr work-types:sync\` to update from upstream.\nLocal: ${localPath}\nUpstream: ${url}`,
77
- };
78
- }
79
- function stripLocalOnlyFields(value) {
80
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
81
- return value;
82
- }
83
- const record = { ...value };
84
- delete record['$schema'];
85
- return record;
86
- }
87
- function deepEqual(a, b) {
88
- if (a === b)
89
- return true;
90
- if (typeof a !== typeof b)
91
- return false;
92
- if (a === null || b === null)
93
- return a === b;
94
- if (Array.isArray(a) && Array.isArray(b)) {
95
- if (a.length !== b.length)
96
- return false;
97
- for (let i = 0; i < a.length; i++) {
98
- if (!deepEqual(a[i], b[i]))
99
- return false;
100
- }
101
- return true;
102
- }
103
- if (typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
104
- const aRecord = { ...a };
105
- const bRecord = { ...b };
106
- const aKeys = Object.keys(aRecord);
107
- const bKeys = Object.keys(bRecord);
108
- if (aKeys.length !== bKeys.length)
109
- return false;
110
- for (const key of aKeys) {
111
- if (!deepEqual(aRecord[key], bRecord[key]))
112
- return false;
113
- }
114
- return true;
115
- }
116
- return false;
117
- }
@@ -1,28 +0,0 @@
1
- /** Outcome of a sync operation. */
2
- export interface SyncResult {
3
- /**
4
- * Process exit code semantics:
5
- * - `0` — sync succeeded (file may be unchanged).
6
- * - `2` — network error or local write failure.
7
- * - `3` — schema mismatch (upstream JSON does not parse or fails the shape check).
8
- */
9
- exitCode: 0 | 2 | 3;
10
- message: string;
11
- }
12
- /** Minimal injection seam so unit tests can substitute a deterministic fetcher. */
13
- export interface SyncWorkTypesDependencies {
14
- /** Absolute path of the local `work-types.json`. Defaults to the bundled file. */
15
- localPath?: string;
16
- /** HTTP fetcher. Defaults to global `fetch`. */
17
- fetch?: typeof globalThis.fetch;
18
- /** Override the upstream URL (used by tests). */
19
- upstreamUrl?: string;
20
- }
21
- /**
22
- * Fetch the upstream `work-types.json`, validate its top-level shape, and overwrite the
23
- * locally-bundled copy with the formatted upstream contents.
24
- *
25
- * Output is reformatted to match the local file's conventions (2-space indent, trailing
26
- * newline) so subsequent diffs are content-driven, not whitespace-driven.
27
- */
28
- export declare function syncWorkTypes(dependencies?: SyncWorkTypesDependencies): Promise<SyncResult>;