@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,36 +1,36 @@
1
- import { printError, printStep, printSuccess, reportWriteResult } from '@williamthorsen/nmr-core';
1
+ import { printError, printStep, printSuccess, reportWriteResult, } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { hasPackageJson, isGitRepo, usesPnpm } from "./checks.js";
4
4
  import { detectRepoType } from "./detectRepoType.js";
5
5
  import { scaffoldFiles } from "./scaffold.js";
6
- function runRequiredCheck(label, result) {
6
+ function runRequiredCheck(label, result, styles) {
7
7
  if (result.ok) {
8
- printSuccess(label);
8
+ printSuccess(label, styles.stdout);
9
9
  return true;
10
10
  }
11
- printError(result.message ?? `${label} failed`);
11
+ printError(result.message ?? `${label} failed`, styles.stderr);
12
12
  return false;
13
13
  }
14
- function checkEligibility() {
14
+ function checkEligibility(styles) {
15
15
  printStep('Checking eligibility');
16
- if (!runRequiredCheck('Git repository detected', isGitRepo()))
16
+ if (!runRequiredCheck('Git repository detected', isGitRepo(), styles))
17
17
  return false;
18
- if (!runRequiredCheck('package.json found', hasPackageJson()))
18
+ if (!runRequiredCheck('package.json found', hasPackageJson(), styles))
19
19
  return false;
20
- if (!runRequiredCheck('pnpm detected', usesPnpm()))
20
+ if (!runRequiredCheck('pnpm detected', usesPnpm(), styles))
21
21
  return false;
22
22
  return true;
23
23
  }
24
- export function initCommand({ dryRun, force, withConfig }) {
24
+ export function initCommand({ dryRun, force, styles, withConfig }) {
25
25
  if (dryRun) {
26
26
  console.info('[dry-run mode]');
27
27
  }
28
28
  let eligible;
29
29
  try {
30
- eligible = checkEligibility();
30
+ eligible = checkEligibility(styles);
31
31
  }
32
32
  catch (error) {
33
- printError(`Eligibility check failed: ${describeError(error)}`);
33
+ printError(`Eligibility check failed: ${describeError(error)}`, styles.stderr);
34
34
  return 1;
35
35
  }
36
36
  if (!eligible)
@@ -41,21 +41,21 @@ export function initCommand({ dryRun, force, withConfig }) {
41
41
  repoType = detectRepoType();
42
42
  }
43
43
  catch (error) {
44
- printError(`Failed to detect repo type: ${describeError(error)}`);
44
+ printError(`Failed to detect repo type: ${describeError(error)}`, styles.stderr);
45
45
  return 1;
46
46
  }
47
- printSuccess(`Detected: ${repoType}`);
47
+ printSuccess(`Detected: ${repoType}`, styles.stdout);
48
48
  printStep('Scaffolding files');
49
49
  let results;
50
50
  try {
51
51
  results = scaffoldFiles({ repoType, dryRun, overwrite: force, withConfig });
52
52
  }
53
53
  catch (error) {
54
- printError(`Failed to scaffold files: ${describeError(error)}`);
54
+ printError(`Failed to scaffold files: ${describeError(error)}`, styles.stderr);
55
55
  return 1;
56
56
  }
57
57
  for (const result of results) {
58
- reportWriteResult(result, dryRun);
58
+ reportWriteResult(result, dryRun, styles);
59
59
  }
60
60
  if (results.some((r) => r.outcome === 'failed')) {
61
61
  return 1;
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  import type { ReleaseKitConfig } from './types.js';
2
3
  /**
3
4
  * Outcome of loading and validating the consumer config file. `missing` is not reported
@@ -15,4 +16,4 @@ export type LoadValidatedConfigResult = {
15
16
  * Load `.config/release-kit.config.ts` and validate it against the shared schema,
16
17
  * reporting load failures and validation errors to stderr.
17
18
  */
18
- export declare function loadValidatedConfig(): Promise<LoadValidatedConfigResult>;
19
+ export declare function loadValidatedConfig(stderrStyle: OutputStyle): Promise<LoadValidatedConfigResult>;
@@ -1,8 +1,8 @@
1
- import { reportError } from '@williamthorsen/nmr-core';
1
+ import { printError, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { loadConfig } from "./loadConfig.js";
4
4
  import { validateConfig } from "./validateConfig.js";
5
- export async function loadValidatedConfig() {
5
+ export async function loadValidatedConfig(stderrStyle) {
6
6
  let raw;
7
7
  try {
8
8
  raw = await loadConfig();
@@ -19,7 +19,7 @@ export async function loadValidatedConfig() {
19
19
  if (errors.length > 0) {
20
20
  process.stderr.write('Invalid config:\n');
21
21
  for (const err of errors) {
22
- process.stderr.write(` ❌ ${err}\n`);
22
+ printError(err, stderrStyle);
23
23
  }
24
24
  return { status: 'invalid' };
25
25
  }
@@ -1,3 +1,4 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  import type { ReleaseType } from './types.js';
2
3
  export declare const prepareFlagSchema: {
3
4
  dryRun: {
@@ -49,4 +50,4 @@ export declare function parseArgs(argv: string[]): {
49
50
  * 4. Delegates to `releasePrepare` or `releasePrepareMono` to compute the release plan.
50
51
  * 5. Applies the plan, runs the format command, and prints the result via `reportPrepare`.
51
52
  */
52
- export declare function prepareCommand(argv: string[]): Promise<void>;
53
+ export declare function prepareCommand(argv: string[], styles: StreamStyles): Promise<void>;
@@ -1,11 +1,12 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
+ import { formatGlyphLine, formatStatusLine, parseArgsOrExit, printError, reportError, } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
5
5
  import { buildDependencyGraph } from "./buildDependencyGraph.js";
6
6
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
7
7
  import { dim } from "./format.js";
8
8
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
9
+ import { RELEASE_GLYPHS } from "./glyphs.js";
9
10
  import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
10
11
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
11
12
  import { applyReleasePlan } from "./releasePlan.js";
@@ -72,7 +73,7 @@ export function parseArgs(argv) {
72
73
  withReleaseNotes: flags.withReleaseNotes,
73
74
  };
74
75
  }
75
- export async function prepareCommand(argv) {
76
+ export async function prepareCommand(argv, styles) {
76
77
  const { dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv);
77
78
  const options = {
78
79
  force,
@@ -81,7 +82,7 @@ export async function prepareCommand(argv) {
81
82
  ...(withReleaseNotes && { withReleaseNotes: true }),
82
83
  };
83
84
  if (dryRun) {
84
- console.info('\n🔍 DRY RUN — no files will be modified\n');
85
+ console.info(`\n${formatGlyphLine(RELEASE_GLYPHS, styles.stdout, 'dryRun', 'DRY RUN — no files will be modified')}\n`);
85
86
  }
86
87
  if (!dryRun && !noGitChecks) {
87
88
  try {
@@ -92,7 +93,7 @@ export async function prepareCommand(argv) {
92
93
  process.exit(1);
93
94
  }
94
95
  }
95
- const userConfig = await loadAndValidateConfig();
96
+ const userConfig = await loadAndValidateConfig(styles.stderr);
96
97
  let discoveredPaths;
97
98
  try {
98
99
  discoveredPaths = await discoverWorkspaces();
@@ -102,13 +103,13 @@ export async function prepareCommand(argv) {
102
103
  process.exit(1);
103
104
  }
104
105
  if (discoveredPaths === undefined) {
105
- runSinglePackageMode(userConfig, options, only, dryRun);
106
+ runSinglePackageMode(userConfig, options, only, dryRun, styles.stdout);
106
107
  }
107
108
  else {
108
- runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun);
109
+ runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun, styles.stdout);
109
110
  }
110
111
  }
111
- function runSinglePackageMode(userConfig, options, only, dryRun) {
112
+ function runSinglePackageMode(userConfig, options, only, dryRun, style) {
112
113
  if (only !== undefined) {
113
114
  reportError('--only is only supported for monorepo configurations');
114
115
  process.exit(1);
@@ -119,9 +120,9 @@ function runSinglePackageMode(userConfig, options, only, dryRun) {
119
120
  process.exit(1);
120
121
  }
121
122
  const config = mergeSinglePackageConfig(userConfig);
122
- runAndReport(() => releasePrepare(config, options), dryRun);
123
+ runAndReport(() => releasePrepare(config, options), dryRun, style);
123
124
  }
124
- function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun) {
125
+ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun, style) {
125
126
  let config;
126
127
  try {
127
128
  const rootPackage = readRootPackageVersion();
@@ -177,9 +178,9 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
177
178
  process.exit(1);
178
179
  }
179
180
  }
180
- runAndReport(() => releasePrepareMono(config, { ...options, ...(only !== undefined && { only }) }), dryRun);
181
+ runAndReport(() => releasePrepareMono(config, { ...options, ...(only !== undefined && { only }) }), dryRun, style);
181
182
  }
182
- async function loadAndValidateConfig() {
183
+ async function loadAndValidateConfig(stderrStyle) {
183
184
  let rawConfig;
184
185
  try {
185
186
  rawConfig = await loadConfig();
@@ -195,16 +196,16 @@ async function loadAndValidateConfig() {
195
196
  if (errors.length > 0) {
196
197
  process.stderr.write('Invalid config:\n');
197
198
  for (const err of errors) {
198
- process.stderr.write(` ❌ ${err}\n`);
199
+ printError(err, stderrStyle);
199
200
  }
200
201
  process.exit(1);
201
202
  }
202
203
  for (const warning of warnings) {
203
- console.warn(` ⚠️ ${warning}`);
204
+ console.warn(` ${formatStatusLine(stderrStyle, 'warning', warning)}`);
204
205
  }
205
206
  return config;
206
207
  }
207
- function runAndReport(computePlan, dryRun) {
208
+ function runAndReport(computePlan, dryRun, style) {
208
209
  let plan;
209
210
  try {
210
211
  plan = computePlan();
@@ -215,7 +216,7 @@ function runAndReport(computePlan, dryRun) {
215
216
  process.exit(1);
216
217
  }
217
218
  if (dryRun) {
218
- process.stdout.write(reportPrepare(plan, { applied: false }) + '\n');
219
+ process.stdout.write(reportPrepare(plan, { applied: false, style }) + '\n');
219
220
  reportPlanFiles(plan, true);
220
221
  return;
221
222
  }
@@ -227,7 +228,7 @@ function runAndReport(computePlan, dryRun) {
227
228
  process.exit(1);
228
229
  }
229
230
  const formatError = runFormatCommand(plan.formatCommand);
230
- process.stdout.write(reportPrepare(plan, { applied: true, ...(formatError !== undefined && { formatError }) }) + '\n');
231
+ process.stdout.write(reportPrepare(plan, { applied: true, style, ...(formatError !== undefined && { formatError }) }) + '\n');
231
232
  reportPlanFiles(plan, false);
232
233
  if (formatError !== undefined) {
233
234
  reportError(`The format command failed, but the release is prepared and ${RELEASE_TAGS_FILE} is written. ` +
@@ -1,5 +1,6 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  /**
2
3
  * Orchestrate the CLI `publish` command: parse flags, discover workspaces, resolve tags from HEAD,
3
4
  * detect the package manager, validate `--tags`, and publish each tag with inject/restore lifecycle.
4
5
  */
5
- export declare function publishCommand(argv: string[]): Promise<void>;
6
+ export declare function publishCommand(argv: string[], styles: StreamStyles): Promise<void>;
@@ -16,7 +16,7 @@ const publishFlagSchema = {
16
16
  provenance: { long: '--provenance', type: 'boolean' },
17
17
  tags: { long: '--tags', type: 'string' },
18
18
  };
19
- export async function publishCommand(argv) {
19
+ export async function publishCommand(argv, styles) {
20
20
  const parsed = parseArgsOrExit(argv, publishFlagSchema);
21
21
  const { dryRun, noGitChecks, provenance } = parsed.flags;
22
22
  if (!dryRun && !noGitChecks) {
@@ -39,7 +39,7 @@ export async function publishCommand(argv) {
39
39
  return;
40
40
  }
41
41
  const packageManager = detectPackageManager();
42
- const { releaseNotes, changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig();
42
+ const { releaseNotes, changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig(styles.stderr);
43
43
  const shouldInject = releaseNotes.shouldInjectIntoReadme;
44
44
  console.info(dryRun ? '[dry-run] Would publish:' : 'Publishing:');
45
45
  for (const { tag, workspacePath } of publishableTags) {
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  import type { PrepareResult } from './types.js';
2
3
  /** How the plan was carried out, which decides the tense the report is rendered in. */
3
4
  export interface ReportPrepareOptions {
@@ -5,6 +6,7 @@ export interface ReportPrepareOptions {
5
6
  applied: boolean;
6
7
  /** The format command's failure message, when it ran and failed. */
7
8
  formatError?: string;
9
+ style: OutputStyle;
8
10
  }
9
11
  /**
10
12
  * Format a release plan into styled terminal output.
@@ -1,4 +1,6 @@
1
+ import { formatGlyphLine, formatStatusLine, measureGlyphColumn, STATUS_GLYPHS, } from '@williamthorsen/nmr-core';
1
2
  import { bold, dim, sectionHeader } from "./format.js";
3
+ import { RELEASE_GLYPHS } from "./glyphs.js";
2
4
  import { GIT_CLIFF_NPX_ARGS } from "./runGitCliff.js";
3
5
  const CLIFF_DRY_RUN_COMMAND = `npx ${GIT_CLIFF_NPX_ARGS.join(' ')} ...`;
4
6
  export function reportPrepare(plan, options) {
@@ -10,6 +12,7 @@ export function reportPrepare(plan, options) {
10
12
  }
11
13
  function formatSingleWorkspace(result, options) {
12
14
  const dryRun = !options.applied;
15
+ const { style } = options;
13
16
  const lines = [];
14
17
  const workspace = result.workspaces[0];
15
18
  if (workspace === undefined) {
@@ -20,11 +23,11 @@ function formatSingleWorkspace(result, options) {
20
23
  if (workspace.parsedCommitCount !== undefined) {
21
24
  lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
22
25
  }
23
- formatUnparseableWarning(lines, workspace);
24
- formatPolicyViolations(lines, workspace.policyViolations);
26
+ formatUnparseableWarning(lines, workspace, style);
27
+ formatPolicyViolations(lines, workspace.policyViolations, style);
25
28
  if (workspace.status === 'skipped') {
26
- lines.push(`⏭️ ${workspace.skipReason}`);
27
- formatWarnings(lines, result);
29
+ lines.push(formatStatusLine(style, 'skipped', workspace.skipReason));
30
+ formatWarnings(lines, result, style);
28
31
  return lines.join('\n');
29
32
  }
30
33
  if (workspace.setVersion !== undefined) {
@@ -40,49 +43,50 @@ function formatSingleWorkspace(result, options) {
40
43
  lines.push(dim(`Bumping versions (version override)...`));
41
44
  }
42
45
  if (workspace.setVersion !== undefined) {
43
- lines.push(`📦 ${workspace.currentVersion} → ${bold(workspace.newVersion)} (version override)`);
46
+ lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (version override)`));
44
47
  }
45
48
  else if (workspace.releaseType !== undefined) {
46
- lines.push(`📦 ${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType})`);
49
+ lines.push(formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType})`));
47
50
  }
48
51
  formatBumpFiles(lines, workspace, dryRun);
49
52
  lines.push(dim('Generating changelogs...'));
50
53
  formatChangelogFiles(lines, workspace, dryRun);
51
54
  formatPreviewFiles(lines, workspace, dryRun);
52
55
  formatFormatCommand(lines, result, options);
53
- formatWarnings(lines, result);
54
- lines.push(`✅ Release preparation complete.`, ` 🏷️ ${bold(workspace.tag)}`);
56
+ formatWarnings(lines, result, style);
57
+ lines.push(formatStatusLine(style, 'passed', 'Release preparation complete.'), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(workspace.tag))}`);
55
58
  return lines.join('\n');
56
59
  }
57
60
  function formatMultiWorkspace(result, options) {
58
61
  const dryRun = !options.applied;
62
+ const { style } = options;
59
63
  const lines = [];
60
64
  for (const workspace of result.workspaces) {
61
- formatWorkspaceSection(lines, workspace, dryRun);
65
+ formatWorkspaceSection(lines, workspace, dryRun, style);
62
66
  }
63
67
  if (result.project !== undefined) {
64
- formatProjectSection(lines, result.project, dryRun);
68
+ formatProjectSection(lines, result.project, dryRun, style);
65
69
  }
66
70
  formatFormatCommand(lines, result, options);
67
- formatWarnings(lines, result);
71
+ formatWarnings(lines, result, style);
68
72
  if (result.tags.length > 0) {
69
- lines.push(`\n✅ Release preparation complete.`);
73
+ lines.push(`\n${formatStatusLine(style, 'passed', 'Release preparation complete.')}`);
70
74
  for (const tag of result.tags) {
71
- lines.push(` 🏷️ ${bold(tag)}`);
75
+ lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
72
76
  }
73
77
  }
74
78
  else {
75
- lines.push(`\n⏭️ No workspaces had release-worthy changes.`);
79
+ lines.push(`\n${formatStatusLine(style, 'skipped', 'No workspaces had release-worthy changes.')}`);
76
80
  }
77
81
  return lines.join('\n');
78
82
  }
79
- function formatProjectSection(lines, project, dryRun) {
83
+ function formatProjectSection(lines, project, dryRun, style) {
80
84
  lines.push(`\n${sectionHeader('project')}`);
81
85
  const since = project.previousTag === undefined ? '(no previous release found)' : `since ${project.previousTag}`;
82
86
  lines.push(dim(` Found ${project.commitCount} commits ${since}`));
83
- formatPolicyViolations(lines, project.policyViolations, ' ');
87
+ formatPolicyViolations(lines, project.policyViolations, style, ' ');
84
88
  if (project.status === 'skipped') {
85
- lines.push(` ⏭️ ${project.skipReason}`);
89
+ lines.push(` ${formatStatusLine(style, 'skipped', project.skipReason)}`);
86
90
  return;
87
91
  }
88
92
  const { releaseType, currentVersion, newVersion, tag } = project;
@@ -92,8 +96,8 @@ function formatProjectSection(lines, project, dryRun) {
92
96
  if (project.bumpOverride !== undefined) {
93
97
  lines.push(` Using bump override: ${project.bumpOverride}`);
94
98
  }
95
- formatProjectUnparseable(lines, project);
96
- lines.push(dim(` Bumping versions (${releaseType})...`), ` 📦 ${currentVersion} → ${bold(newVersion)} (${releaseType})`);
99
+ formatProjectUnparseable(lines, project, style);
100
+ lines.push(dim(` Bumping versions (${releaseType})...`), ` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${currentVersion} → ${bold(newVersion)} (${releaseType})`)}`);
97
101
  for (const file of project.bumpedFiles) {
98
102
  if (dryRun) {
99
103
  lines.push(dim(` [dry-run] Would bump ${file}`));
@@ -112,9 +116,9 @@ function formatProjectSection(lines, project, dryRun) {
112
116
  }
113
117
  }
114
118
  formatPreviewFiles(lines, project, dryRun, ' ');
115
- lines.push(` 🏷️ ${bold(tag)}`);
119
+ lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(tag))}`);
116
120
  }
117
- function formatProjectUnparseable(lines, project) {
121
+ function formatProjectUnparseable(lines, project, style) {
118
122
  const unparseable = project.unparseableCommits;
119
123
  if (unparseable === undefined || unparseable.length === 0) {
120
124
  return;
@@ -122,35 +126,35 @@ function formatProjectUnparseable(lines, project) {
122
126
  const count = unparseable.length;
123
127
  const isPatchFloor = project.parsedCommitCount === 0;
124
128
  const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
125
- lines.push(` ⚠️ ${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`);
129
+ lines.push(` ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
126
130
  for (const commit of unparseable) {
127
131
  const shortHash = commit.hash.slice(0, 7);
128
132
  const truncatedMessage = commit.message.length > 72 ? `${commit.message.slice(0, 69)}...` : commit.message;
129
133
  lines.push(` · ${shortHash} ${truncatedMessage}`);
130
134
  }
131
135
  }
132
- function formatWorkspaceSection(lines, workspace, dryRun) {
136
+ function formatWorkspaceSection(lines, workspace, dryRun, style) {
133
137
  if (workspace.name !== undefined) {
134
138
  lines.push(`\n${sectionHeader(workspace.name)}`);
135
139
  }
136
140
  const since = workspace.previousTag === undefined ? '(no previous release found)' : `since ${workspace.previousTag}`;
137
141
  lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
138
142
  if (workspace.status === 'skipped') {
139
- lines.push(` ⏭️ ${workspace.skipReason}`);
143
+ lines.push(` ${formatStatusLine(style, 'skipped', workspace.skipReason)}`);
140
144
  return;
141
145
  }
142
146
  const { propagatedFrom } = workspace;
143
147
  const isPropagatedOnly = propagatedFrom !== undefined && workspace.commitCount === 0;
144
148
  formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
145
- formatUnparseableWarning(lines, workspace, ' ');
146
- formatPolicyViolations(lines, workspace.policyViolations, ' ');
149
+ formatUnparseableWarning(lines, workspace, style, ' ');
150
+ formatPolicyViolations(lines, workspace.policyViolations, style, ' ');
147
151
  formatBumpLabels(lines, workspace, isPropagatedOnly);
148
- formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly);
152
+ formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style);
149
153
  formatBumpFiles(lines, workspace, dryRun, ' ');
150
154
  lines.push(dim(' Generating changelogs...'));
151
155
  formatChangelogFiles(lines, workspace, dryRun, ' ');
152
156
  formatPreviewFiles(lines, workspace, dryRun, ' ');
153
- lines.push(` 🏷️ ${bold(workspace.tag)}`);
157
+ lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'tag', bold(workspace.tag))}`);
154
158
  }
155
159
  function formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly) {
156
160
  if (isPropagatedOnly && propagatedFrom !== undefined) {
@@ -175,13 +179,14 @@ function formatBumpLabels(lines, workspace, isPropagatedOnly) {
175
179
  lines.push(dim(` Bumping versions (version override)...`));
176
180
  }
177
181
  }
178
- function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly) {
182
+ function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly, style) {
183
+ const versions = `${workspace.currentVersion} → ${bold(workspace.newVersion)}`;
179
184
  if (workspace.setVersion !== undefined) {
180
- lines.push(` 📦 ${workspace.currentVersion} → ${bold(workspace.newVersion)} (version override)`);
185
+ lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (version override)`)}`);
181
186
  }
182
187
  else if (workspace.releaseType !== undefined) {
183
188
  const suffix = isPropagatedOnly ? formatPropagationSuffix(propagatedFrom) : '';
184
- lines.push(` 📦 ${workspace.currentVersion} → ${bold(workspace.newVersion)} (${workspace.releaseType}${suffix})`);
189
+ lines.push(` ${formatGlyphLine(RELEASE_GLYPHS, style, 'bump', `${versions} (${workspace.releaseType}${suffix})`)}`);
185
190
  }
186
191
  }
187
192
  function formatBumpFiles(lines, workspace, dryRun, indent = '') {
@@ -215,7 +220,7 @@ function formatPreviewFiles(lines, release, dryRun, indent = '') {
215
220
  }
216
221
  }
217
222
  }
218
- function formatUnparseableWarning(lines, workspace, indent = '') {
223
+ function formatUnparseableWarning(lines, workspace, style, indent = '') {
219
224
  const unparseable = workspace.unparseableCommits;
220
225
  if (unparseable === undefined || unparseable.length === 0) {
221
226
  return;
@@ -223,19 +228,19 @@ function formatUnparseableWarning(lines, workspace, indent = '') {
223
228
  const count = unparseable.length;
224
229
  const isPatchFloor = workspace.parsedCommitCount === 0;
225
230
  const suffix = isPatchFloor ? ' (defaulting to patch bump)' : '';
226
- lines.push(`${indent} ⚠️ ${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`);
231
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} commit${count === 1 ? '' : 's'} could not be parsed${suffix}`)}`);
227
232
  for (const commit of unparseable) {
228
233
  const shortHash = commit.hash.slice(0, 7);
229
234
  const truncatedMessage = commit.message.length > 72 ? `${commit.message.slice(0, 69)}...` : commit.message;
230
235
  lines.push(`${indent} · ${shortHash} ${truncatedMessage}`);
231
236
  }
232
237
  }
233
- function formatPolicyViolations(lines, violations, indent = '') {
238
+ function formatPolicyViolations(lines, violations, style, indent = '') {
234
239
  if (violations === undefined || violations.length === 0) {
235
240
  return;
236
241
  }
237
242
  const count = violations.length;
238
- lines.push(`${indent} ⚠️ ${count} policy violation${count === 1 ? '' : 's'}:`);
243
+ lines.push(`${indent} ${formatStatusLine(style, 'warning', `${count} policy violation${count === 1 ? '' : 's'}:`)}`);
239
244
  for (const violation of violations) {
240
245
  const shortHash = violation.commitHash.slice(0, 7);
241
246
  const subject = violation.commitSubject;
@@ -250,14 +255,14 @@ function formatPropagationSuffix(propagatedFrom) {
250
255
  const names = propagatedFrom.map((p) => p.packageName).join(', ');
251
256
  return `, dependency: ${names}`;
252
257
  }
253
- function formatWarnings(lines, result) {
258
+ function formatWarnings(lines, result, style) {
254
259
  const { warnings } = result;
255
260
  if (warnings === undefined || warnings.length === 0) {
256
261
  return;
257
262
  }
258
263
  lines.push('');
259
264
  for (const warning of warnings) {
260
- lines.push(`⚠️ ${warning}`);
265
+ lines.push(formatStatusLine(style, 'warning', warning));
261
266
  }
262
267
  }
263
268
  function formatFormatCommand(lines, result, options) {
@@ -272,5 +277,6 @@ function formatFormatCommand(lines, result, options) {
272
277
  lines.push(dim(`\n Running format command: ${result.formatCommand.command}`));
273
278
  return;
274
279
  }
275
- lines.push(`\n ⚠️ Format command failed: ${result.formatCommand.command}`, ` ${options.formatError}`);
280
+ const messageIndent = ' '.repeat(2 + measureGlyphColumn(STATUS_GLYPHS[options.style]) + 1);
281
+ lines.push(`\n ${formatStatusLine(options.style, 'warning', `Format command failed: ${result.formatCommand.command}`)}`, `${messageIndent}${options.formatError}`);
276
282
  }
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  import type { ReleaseNotesConfig } from './types.js';
2
3
  export interface ResolvedReleaseNotesConfig {
3
4
  releaseNotes: ReleaseNotesConfig;
@@ -20,7 +21,7 @@ export interface ResolveReleaseNotesConfigOptions {
20
21
  * By default, falls back to defaults when `loadConfig()` rejects (legacy publish behavior).
21
22
  * When `strictLoad` is `true`, a load failure prints an error and calls `process.exit(1)`.
22
23
  */
23
- export declare function resolveReleaseNotesConfig(options?: ResolveReleaseNotesConfigOptions): Promise<ResolvedReleaseNotesConfig>;
24
+ export declare function resolveReleaseNotesConfig(stderrStyle: OutputStyle, options?: ResolveReleaseNotesConfigOptions): Promise<ResolvedReleaseNotesConfig>;
24
25
  /** Extract section headers in declaration order from a merged workTypes record. */
25
26
  export declare function deriveSectionOrder(workTypes: Record<string, {
26
27
  header: string;
@@ -1,9 +1,9 @@
1
- import { reportError } from '@williamthorsen/nmr-core';
1
+ import { formatStatusLine, printError, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
3
  import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
4
4
  import { loadConfig, resolveWorkTypes } from "./loadConfig.js";
5
5
  import { validateConfig } from "./validateConfig.js";
6
- export async function resolveReleaseNotesConfig(options = {}) {
6
+ export async function resolveReleaseNotesConfig(stderrStyle, options = {}) {
7
7
  const { strictLoad = false } = options;
8
8
  let rawConfig;
9
9
  try {
@@ -28,12 +28,12 @@ export async function resolveReleaseNotesConfig(options = {}) {
28
28
  if (errors.length > 0) {
29
29
  process.stderr.write('Invalid config:\n');
30
30
  for (const err of errors) {
31
- process.stderr.write(` ❌ ${err}\n`);
31
+ printError(err, stderrStyle);
32
32
  }
33
33
  process.exit(1);
34
34
  }
35
35
  for (const warning of warnings) {
36
- console.warn(` ⚠️ ${warning}`);
36
+ console.warn(` ${formatStatusLine(stderrStyle, 'warning', warning)}`);
37
37
  }
38
38
  return {
39
39
  releaseNotes: {
@@ -0,0 +1,10 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
2
+ /** The environment variable holding the caller's standing style preference: `auto`, `plain`, or `rich`. */
3
+ export declare const OUTPUT_STYLE_ENV_VAR = "RELEASE_KIT_OUTPUT_STYLE";
4
+ export interface ResolveStylesOptions {
5
+ readonly env: Readonly<Record<string, string | undefined>>;
6
+ readonly stderrIsTty: boolean;
7
+ readonly stdoutIsTty: boolean;
8
+ }
9
+ /** Resolves the style of each output stream, exiting with a usage error when the variable names no setting. */
10
+ export declare function resolveStylesOrExit(options: ResolveStylesOptions): StreamStyles;
@@ -0,0 +1,10 @@
1
+ import { describeInvalidOutputStyle, reportError, resolveStreamStyles, } from '@williamthorsen/nmr-core';
2
+ export const OUTPUT_STYLE_ENV_VAR = 'RELEASE_KIT_OUTPUT_STYLE';
3
+ export function resolveStylesOrExit(options) {
4
+ const { invalid, styles } = resolveStreamStyles({ ...options, envVar: OUTPUT_STYLE_ENV_VAR });
5
+ if (invalid !== undefined) {
6
+ reportError(describeInvalidOutputStyle(invalid));
7
+ process.exit(1);
8
+ }
9
+ return styles;
10
+ }
@@ -1,3 +1,4 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  /**
2
3
  * Orchestrate the CLI `show-tag-prefixes` command.
3
4
  *
@@ -9,4 +10,4 @@
9
10
  *
10
11
  * @returns The exit code the caller should use.
11
12
  */
12
- export declare function showTagPrefixesCommand(): Promise<number>;
13
+ export declare function showTagPrefixesCommand(styles: StreamStyles): Promise<number>;