@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,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgsOrExit, readPackageVersion, reportError } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
- import { checkWorkTypesDrift } from "../checkWorkTypesDrift.js";
5
4
  import { commitCommand } from "../commitCommand.js";
5
+ import { configFlagSchema } from "../configFlagSchema.js";
6
6
  import { createGithubReleaseCommand } from "../createGithubReleaseCommand.js";
7
7
  import { showPrepareHelp } from "../help/prepareHelp.js";
8
8
  import { initCommand } from "../init/initCommand.js";
9
9
  import { prepareCommand } from "../prepareCommand.js";
10
10
  import { publishCommand } from "../publishCommand.js";
11
11
  import { pushCommand } from "../pushCommand.js";
12
+ import { OUTPUT_STYLE_ENV_VAR, resolveStylesOrExit } from "../resolveStylesOrExit.js";
12
13
  import { showTagPrefixesCommand } from "../showTagPrefixesCommand.js";
13
14
  import { generateCommand } from "../sync-labels/generateCommand.js";
14
15
  import { syncLabelsInitCommand } from "../sync-labels/initCommand.js";
15
16
  import { syncLabelsCommand } from "../sync-labels/syncCommand.js";
16
- import { syncWorkTypes } from "../syncWorkTypes.js";
17
17
  import { tagCommand } from "../tagCommand.js";
18
18
  import { validateOverridesCommand } from "../validateOverridesCommand.js";
19
19
  const VERSION = readPackageVersion(import.meta.url);
@@ -32,12 +32,16 @@ Commands:
32
32
  init Initialize release-kit in the current repository
33
33
  overrides Manage editorial changelog overrides
34
34
  sync-labels Manage GitHub label synchronization
35
- work-types Check for or sync work-type taxonomy drift against the upstream canonical
36
35
 
37
36
  Options:
38
37
  --dry-run Preview changes without writing files
39
38
  --help, -h Show this help message
40
39
  --version, -V Show the version number
40
+
41
+ Environment:
42
+ ${OUTPUT_STYLE_ENV_VAR}
43
+ Output style: auto (default), plain, or rich. auto prints plain,
44
+ without emoji, when CI is set or the stream is not a terminal.
41
45
  `);
42
46
  }
43
47
  function showSyncLabelsHelp() {
@@ -65,9 +69,10 @@ retired packages, then generate .github/labels.yaml. When the config file alread
65
69
  exists, print the block for manual paste instead of rewriting the file.
66
70
 
67
71
  Options:
68
- --dry-run Preview changes without writing files
69
- --force Overwrite existing files instead of skipping them
70
- --help, -h Show this help message
72
+ --config <path> Path to config file (default: .config/release-kit.config.ts)
73
+ --dry-run Preview changes without writing files
74
+ --force Overwrite existing files instead of skipping them
75
+ --help, -h Show this help message
71
76
  `);
72
77
  }
73
78
  function showSyncLabelsGenerateHelp() {
@@ -77,8 +82,9 @@ Usage: release-kit sync-labels generate [options]
77
82
  Regenerate .github/labels.yaml from the repoLabels block of .config/release-kit.config.ts.
78
83
 
79
84
  Options:
80
- --check Report whether .github/labels.yaml is stale instead of writing it
81
- --help, -h Show this help message
85
+ --check Report whether .github/labels.yaml is stale instead of writing it
86
+ --config <path> Path to config file (default: .config/release-kit.config.ts)
87
+ --help, -h Show this help message
82
88
  `);
83
89
  }
84
90
  function showSyncLabelsSyncHelp() {
@@ -151,6 +157,7 @@ Create GitHub Releases from changelog.json for tags on HEAD. Private
151
157
  (package.json#private) workspaces are skipped with a warning and get no Release.
152
158
 
153
159
  Options:
160
+ --config <path> Path to config file (default: .config/release-kit.config.ts)
154
161
  --dry-run Preview without creating releases
155
162
  --tags=tag1,tag2 Only create releases for the named tags (comma-separated, full tag names)
156
163
  --help, -h Show this help message
@@ -166,7 +173,8 @@ derived prefix nor declared in \`legacyIdentities\`, with a copy-pasteable
166
173
  config snippet.
167
174
 
168
175
  Options:
169
- --help, -h Show this help message
176
+ --config <path> Path to config file (default: .config/release-kit.config.ts)
177
+ --help, -h Show this help message
170
178
  `);
171
179
  }
172
180
  function showOverridesHelp() {
@@ -195,50 +203,8 @@ Exit codes:
195
203
  2 Schema/parse or ambiguous-prefix errors (errors dominate)
196
204
 
197
205
  Options:
198
- --help, -h Show this help message
199
- `);
200
- }
201
- function showWorkTypesHelp() {
202
- console.info(`
203
- Usage: release-kit work-types <subcommand>
204
-
205
- Manage the canonical work-types taxonomy used by changelog and release-notes generation.
206
-
207
- Subcommands:
208
- check Compare the local work-types.json against the upstream codeassembly canonical
209
- sync Overwrite the local work-types.json with the upstream contents
210
-
211
- Exit codes (check):
212
- 0 Match (or upstream not found, with a warning)
213
- 1 Drift detected
214
- 2 Network error
215
- 3 Schema mismatch
216
-
217
- Options:
218
- --help, -h Show this help message
219
- `);
220
- }
221
- function showWorkTypesCheckHelp() {
222
- console.info(`
223
- Usage: release-kit work-types check
224
-
225
- Compare the local work-types.json against the upstream codeassembly canonical and report
226
- drift. Exit 0 on match, 1 on drift, 0 + warning when upstream is not found, 2 on network
227
- error, 3 on schema mismatch.
228
-
229
- Options:
230
- --help, -h Show this help message
231
- `);
232
- }
233
- function showWorkTypesSyncHelp() {
234
- console.info(`
235
- Usage: release-kit work-types sync
236
-
237
- Fetch the upstream work-types.json, validate its top-level shape, and overwrite the local
238
- copy with the upstream content (formatted with 2-space indent + trailing newline).
239
-
240
- Options:
241
- --help, -h Show this help message
206
+ --config <path> Path to config file (default: .config/release-kit.config.ts)
207
+ --help, -h Show this help message
242
208
  `);
243
209
  }
244
210
  function showPublishHelp() {
@@ -251,6 +217,7 @@ silently filtered out. With --tags, an unpublishable tag is skipped with a warni
251
217
  any publishable tags still publish.
252
218
 
253
219
  Options:
220
+ --config <path> Path to config file (default: .config/release-kit.config.ts)
254
221
  --dry-run Preview without publishing
255
222
  --no-git-checks Skip the clean-working-tree check
256
223
  --tags=tag1,tag2 Only publish the named tags (comma-separated, full tag names)
@@ -269,12 +236,17 @@ if (command === undefined || ['--help', '-h'].includes(command)) {
269
236
  process.exit(0);
270
237
  }
271
238
  const flags = args.slice(1);
239
+ const styles = resolveStylesOrExit({
240
+ env: process.env,
241
+ stderrIsTty: process.stderr.isTTY,
242
+ stdoutIsTty: process.stdout.isTTY,
243
+ });
272
244
  if (command === 'prepare') {
273
245
  if (flags.some((f) => f === '--help' || f === '-h')) {
274
246
  showPrepareHelp();
275
247
  process.exit(0);
276
248
  }
277
- await prepareCommand(flags);
249
+ await prepareCommand(flags, styles);
278
250
  process.exit(0);
279
251
  }
280
252
  if (command === 'commit') {
@@ -296,7 +268,7 @@ if (command === 'tag') {
296
268
  showTagHelp();
297
269
  process.exit(0);
298
270
  }
299
- tagCommand(flags);
271
+ tagCommand(flags, styles);
300
272
  process.exit(0);
301
273
  }
302
274
  if (command === 'push') {
@@ -304,7 +276,7 @@ if (command === 'push') {
304
276
  showPushHelp();
305
277
  process.exit(0);
306
278
  }
307
- await pushCommand(flags);
279
+ pushCommand(flags);
308
280
  process.exit(0);
309
281
  }
310
282
  if (command === 'create-github-release') {
@@ -312,7 +284,7 @@ if (command === 'create-github-release') {
312
284
  showCreateGithubReleaseHelp();
313
285
  process.exit(0);
314
286
  }
315
- await createGithubReleaseCommand(flags);
287
+ await createGithubReleaseCommand(flags, styles);
316
288
  process.exit(0);
317
289
  }
318
290
  if (command === 'publish') {
@@ -320,7 +292,7 @@ if (command === 'publish') {
320
292
  showPublishHelp();
321
293
  process.exit(0);
322
294
  }
323
- await publishCommand(flags);
295
+ await publishCommand(flags, styles);
324
296
  process.exit(0);
325
297
  }
326
298
  if (command === 'show-tag-prefixes') {
@@ -328,11 +300,8 @@ if (command === 'show-tag-prefixes') {
328
300
  showShowTagPrefixesHelp();
329
301
  process.exit(0);
330
302
  }
331
- if (flags.length > 0) {
332
- reportError(`Unknown option: ${flags[0]}`);
333
- process.exit(1);
334
- }
335
- const exitCode = await showTagPrefixesCommand();
303
+ const { config } = parseArgsOrExit(flags, configFlagSchema).flags;
304
+ const exitCode = await showTagPrefixesCommand(styles, config);
336
305
  process.exit(exitCode);
337
306
  }
338
307
  if (command === 'init') {
@@ -346,7 +315,7 @@ if (command === 'init') {
346
315
  withConfig: { long: '--with-config', type: 'boolean' },
347
316
  };
348
317
  const { dryRun, force, withConfig } = parseArgsOrExit(flags, initFlagSchema).flags;
349
- const exitCode = initCommand({ dryRun, force, withConfig });
318
+ const exitCode = initCommand({ dryRun, force, styles, withConfig });
350
319
  process.exit(exitCode);
351
320
  }
352
321
  if (command === 'sync-labels') {
@@ -362,11 +331,17 @@ if (command === 'sync-labels') {
362
331
  process.exit(0);
363
332
  }
364
333
  const syncLabelsInitFlagSchema = {
334
+ ...configFlagSchema,
365
335
  dryRun: { long: '--dry-run', type: 'boolean' },
366
336
  force: { long: '--force', type: 'boolean' },
367
337
  };
368
- const { dryRun, force } = parseArgsOrExit(subflags, syncLabelsInitFlagSchema).flags;
369
- const exitCode = await syncLabelsInitCommand({ dryRun, force });
338
+ const { config, dryRun, force } = parseArgsOrExit(subflags, syncLabelsInitFlagSchema).flags;
339
+ const exitCode = await syncLabelsInitCommand({
340
+ ...(config !== undefined && { configPath: config }),
341
+ dryRun,
342
+ force,
343
+ styles,
344
+ });
370
345
  process.exit(exitCode);
371
346
  }
372
347
  if (subcommand === 'generate') {
@@ -375,10 +350,11 @@ if (command === 'sync-labels') {
375
350
  process.exit(0);
376
351
  }
377
352
  const generateFlagSchema = {
353
+ ...configFlagSchema,
378
354
  check: { long: '--check', type: 'boolean' },
379
355
  };
380
- const { check } = parseArgsOrExit(subflags, generateFlagSchema).flags;
381
- const exitCode = await generateCommand({ check });
356
+ const { check, config } = parseArgsOrExit(subflags, generateFlagSchema).flags;
357
+ const exitCode = await generateCommand({ check, ...(config !== undefined && { configPath: config }), styles });
382
358
  process.exit(exitCode);
383
359
  }
384
360
  if (subcommand === 'sync') {
@@ -409,11 +385,8 @@ if (command === 'overrides') {
409
385
  showOverridesValidateHelp();
410
386
  process.exit(0);
411
387
  }
412
- if (subflags.length > 0) {
413
- reportError(`Unknown option: ${subflags[0]}`);
414
- process.exit(1);
415
- }
416
- const result = await validateOverridesCommand();
388
+ const { config } = parseArgsOrExit(subflags, configFlagSchema).flags;
389
+ const result = await validateOverridesCommand(styles, config);
417
390
  if (result.exitCode === 0) {
418
391
  console.info(result.message);
419
392
  }
@@ -426,53 +399,6 @@ if (command === 'overrides') {
426
399
  showOverridesHelp();
427
400
  process.exit(1);
428
401
  }
429
- if (command === 'work-types') {
430
- const subcommand = flags[0];
431
- if (subcommand === undefined || ['--help', '-h'].includes(subcommand)) {
432
- showWorkTypesHelp();
433
- process.exit(0);
434
- }
435
- const subflags = flags.slice(1);
436
- if (subcommand === 'check') {
437
- if (subflags.some((f) => f === '--help' || f === '-h')) {
438
- showWorkTypesCheckHelp();
439
- process.exit(0);
440
- }
441
- if (subflags.length > 0) {
442
- reportError(`Unknown option: ${subflags[0]}`);
443
- process.exit(1);
444
- }
445
- const result = await checkWorkTypesDrift();
446
- if (result.exitCode === 0) {
447
- console.info(result.message);
448
- }
449
- else {
450
- process.stderr.write(`${result.message}\n`);
451
- }
452
- process.exit(result.exitCode);
453
- }
454
- if (subcommand === 'sync') {
455
- if (subflags.some((f) => f === '--help' || f === '-h')) {
456
- showWorkTypesSyncHelp();
457
- process.exit(0);
458
- }
459
- if (subflags.length > 0) {
460
- reportError(`Unknown option: ${subflags[0]}`);
461
- process.exit(1);
462
- }
463
- const result = await syncWorkTypes();
464
- if (result.exitCode === 0) {
465
- console.info(result.message);
466
- }
467
- else {
468
- process.stderr.write(`${result.message}\n`);
469
- }
470
- process.exit(result.exitCode);
471
- }
472
- reportError(`Unknown subcommand: ${subcommand}`);
473
- showWorkTypesHelp();
474
- process.exit(1);
475
- }
476
402
  reportError(`Unknown command: ${command}`);
477
403
  showUsage();
478
404
  process.exit(1);
@@ -158,6 +158,7 @@ function extractDescription(message) {
158
158
  return firstLine;
159
159
  }
160
160
  const TRAILER_PATTERNS = [
161
+ /^Change:/i,
161
162
  /^Signed-off-by:/i,
162
163
  /^Co-authored-by:/i,
163
164
  /^(Closes|Fixes|Resolves)\s+#\d+\s*$/i,
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The `--config` flag, shared by every subcommand that reads the config file.
3
+ *
4
+ * Spread into a subcommand's own schema rather than repeated, so that seven subcommands cannot drift to seven
5
+ * spellings of one flag.
6
+ */
7
+ export declare const configFlagSchema: {
8
+ config: {
9
+ long: string;
10
+ type: "string";
11
+ };
12
+ };
@@ -0,0 +1,3 @@
1
+ export const configFlagSchema = {
2
+ config: { long: '--config', type: 'string' },
3
+ };
@@ -1,3 +1,4 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
1
2
  /**
2
3
  * Orchestrate the CLI `create-github-release` command: Create GitHub Releases from changelog.json
3
4
  * for tags on HEAD (or a comma-separated `--tags` subset), without requiring npm publish.
@@ -5,4 +6,4 @@
5
6
  * Private/unpublishable workspaces are skipped with a warning and never get a Release, matching
6
7
  * `release-kit publish`; an all-private tag set is a clean no-op.
7
8
  */
8
- export declare function createGithubReleaseCommand(argv: string[]): Promise<void>;
9
+ export declare function createGithubReleaseCommand(argv: string[], styles: StreamStyles): Promise<void>;
@@ -1,19 +1,23 @@
1
1
  import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
2
  import { describeError } from '@williamthorsen/toolbelt.errors';
3
+ import { configFlagSchema } from "./configFlagSchema.js";
3
4
  import { createGithubReleases } from "./createGithubRelease.js";
4
5
  import { formatPrivateSkip } from "./formatPrivateSkip.js";
6
+ import { assertConfigUsable } from "./loadValidatedConfig.js";
5
7
  import { parseRequestedTags } from "./parseRequestedTags.js";
6
8
  import { resolveCommandTags } from "./resolveCommandTags.js";
7
9
  import { resolveReleaseNotesConfig } from "./resolveReleaseNotesConfig.js";
8
10
  const createGithubReleaseFlagSchema = {
11
+ ...configFlagSchema,
9
12
  dryRun: { long: '--dry-run', type: 'boolean' },
10
13
  tags: { long: '--tags', type: 'string' },
11
14
  };
12
- export async function createGithubReleaseCommand(argv) {
15
+ export async function createGithubReleaseCommand(argv, styles) {
13
16
  const parsed = parseArgsOrExit(argv, createGithubReleaseFlagSchema);
14
17
  const { dryRun } = parsed.flags;
18
+ await assertConfigUsable(styles.stderr, parsed.flags.config);
15
19
  const requestedTags = parseRequestedTags(parsed.flags.tags);
16
- const resolvedTags = await resolveCommandTags(requestedTags);
20
+ const resolvedTags = resolveCommandTags(requestedTags);
17
21
  const publishableTags = resolvedTags.filter((resolvedTag) => resolvedTag.isPublishable);
18
22
  for (const resolvedTag of resolvedTags) {
19
23
  if (!resolvedTag.isPublishable) {
@@ -23,7 +27,9 @@ export async function createGithubReleaseCommand(argv) {
23
27
  if (publishableTags.length === 0) {
24
28
  return;
25
29
  }
26
- const { changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig({ strictLoad: true });
30
+ const { changelogJsonOutputPath, sectionOrder } = await resolveReleaseNotesConfig(styles.stderr, {
31
+ ...(parsed.flags.config !== undefined && { configPath: parsed.flags.config }),
32
+ });
27
33
  let outcome;
28
34
  try {
29
35
  outcome = createGithubReleases(publishableTags, changelogJsonOutputPath, dryRun, sectionOrder);
@@ -1,6 +1,8 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  export interface CreateTagsOptions {
2
3
  dryRun: boolean;
3
4
  noGitChecks: boolean;
5
+ style: OutputStyle;
4
6
  }
5
7
  /**
6
8
  * Read tag names from the tags file produced by `prepare` and create annotated git tags.
@@ -1,9 +1,10 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { GIT_OUTPUT_LIMIT, hasErrnoCode } from '@williamthorsen/nmr-core';
2
+ import { formatGlyphLine, GIT_OUTPUT_LIMIT, hasErrnoCode } from '@williamthorsen/nmr-core';
3
3
  import { deleteFileIfExists } from "./deleteFileIfExists.js";
4
+ import { RELEASE_GLYPHS } from "./glyphs.js";
4
5
  import { readReleaseTags, RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
5
6
  export function createTags(options) {
6
- const { dryRun, noGitChecks } = options;
7
+ const { dryRun, noGitChecks, style } = options;
7
8
  const tags = readReleaseTags();
8
9
  if (tags.length === 0) {
9
10
  return [];
@@ -14,7 +15,7 @@ export function createTags(options) {
14
15
  if (dryRun) {
15
16
  console.info('[dry-run] Would create tags:');
16
17
  for (const tag of tags) {
17
- console.info(`🏷️ ${tag}`);
18
+ console.info(formatGlyphLine(RELEASE_GLYPHS, style, 'tag', tag));
18
19
  }
19
20
  return tags;
20
21
  }
@@ -36,7 +37,7 @@ export function createTags(options) {
36
37
  }
37
38
  console.info('Created tags:');
38
39
  for (const tag of tags) {
39
- console.info(`🏷️ ${tag}`);
40
+ console.info(formatGlyphLine(RELEASE_GLYPHS, style, 'tag', tag));
40
41
  }
41
42
  deleteFileIfExists(RELEASE_TAGS_FILE);
42
43
  deleteFileIfExists(RELEASE_SUMMARY_FILE);
@@ -1,13 +1,58 @@
1
+ import { type EmptyWorkspaceCause } from '@williamthorsen/nmr-core/workspace';
2
+ export type { EmptyWorkspaceCause } from '@williamthorsen/nmr-core/workspace';
1
3
  /**
2
- * Reads `pnpm-workspace.yaml` and resolves its `packages` globs to discover workspace directories.
4
+ * What release-kit's workspace discovery produced: the repo releases as a single package, it resolves to a
5
+ * set of package directories, or it declares a workspace that resolves to none and the cause says which
6
+ * condition emptied it.
3
7
  *
4
- * Returns an array of workspace-relative directory paths (e.g., `['packages/arrays', 'packages/strings']`).
5
- * Only directories containing a `package.json` are included.
8
+ * Held apart from nmr-core's `WorkspaceResolution`, whose `packageDirs` are absolute, because these are
9
+ * relative to the monorepo root. The two carry incompatible path contracts, and a distinct `kind` for the
10
+ * single-package case keeps either from standing in for the other.
11
+ */
12
+ export type WorkspaceDiscovery = {
13
+ kind: 'empty';
14
+ cause: FailingWorkspaceCause;
15
+ patterns: string[];
16
+ } | {
17
+ kind: 'packages';
18
+ packageDirs: string[];
19
+ patterns: string[];
20
+ } | {
21
+ kind: 'single-package';
22
+ };
23
+ /** A workspace that resolved to no package directory, which every release-kit command treats as a failure. */
24
+ export type EmptyWorkspace = Extract<WorkspaceDiscovery, {
25
+ kind: 'empty';
26
+ }>;
27
+ /**
28
+ * The empty-workspace causes that reach a command as a failure.
29
+ *
30
+ * `no-packages-list` is absent because a manifest declaring nothing releases as a single package, so no
31
+ * command ever composes a message for it.
32
+ */
33
+ export type FailingWorkspaceCause = Exclude<EmptyWorkspaceCause, 'no-packages-list'>;
34
+ /**
35
+ * Returns the sentence naming which of the five conditions left a workspace holding no package directory, and
36
+ * the remedy for that one. Each quotes the `packages` list the manifest declares, apart from the two that
37
+ * reach the matcher with no list to quote.
38
+ *
39
+ * The `package.json` requirement is stated under `no-package` because it is a divergence from pnpm, which
40
+ * recognizes two further manifests, and the reader of a workspace that pnpm resolves has no way to infer it.
41
+ */
42
+ export declare function describeEmptyWorkspace(workspace: EmptyWorkspace): string;
43
+ /**
44
+ * Reads `pnpm-workspace.yaml` at `monorepoRoot` and resolves its `packages` patterns, applying pnpm's
45
+ * semantics — `!`-prefixed exclusions included.
6
46
  *
7
- * Note: The returned paths are full relative paths from the repo root (e.g., `packages/arrays`,
8
- * `libs/core`). These paths are passed directly to `deriveWorkspaceConfig()`, which uses them for all
9
- * file path construction and derives the display name via `path.basename()`.
47
+ * A directory holding no manifest, and one whose manifest declares no `packages` list, both release as a
48
+ * single package: pnpm resolves each to the root package alone, and a workspace file kept for `catalog:`,
49
+ * `overrides:`, or `onlyBuiltDependencies:` is the second of them. Every other empty resolution is a failure,
50
+ * whether the reader declared patterns that matched nothing or a `packages` value the resolver could not read
51
+ * as patterns at all.
10
52
  *
11
- * @returns The discovered workspace paths, or `undefined` if no workspace config is found.
53
+ * `packageDirs` come back relative to `monorepoRoot` as POSIX paths, because that is what every consumer
54
+ * needs: `WorkspaceConfig.paths` feeds `git log -- <paths>`, and `packageFiles` and `changelogPaths` are read
55
+ * from disk relative to the working directory. The workspace root itself relativizes to `.`, the path
56
+ * `deriveWorkspaceConfig` reads a root package's manifest through.
12
57
  */
13
- export declare function discoverWorkspaces(): Promise<string[] | undefined>;
58
+ export declare function discoverWorkspaces(monorepoRoot?: string): WorkspaceDiscovery;
@@ -1,42 +1,62 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { describeError } from '@williamthorsen/toolbelt.errors';
4
- import { glob } from 'glob';
5
- import { parse } from 'yaml';
6
- import { isRecord } from "./typeGuards.js";
7
- export async function discoverWorkspaces() {
8
- const workspaceFile = 'pnpm-workspace.yaml';
9
- if (!existsSync(workspaceFile)) {
10
- return undefined;
11
- }
12
- let content;
13
- try {
14
- content = readFileSync(workspaceFile, 'utf8');
15
- }
16
- catch (error) {
17
- console.warn(`Warning: Failed to read ${workspaceFile}: ${describeError(error)}`);
18
- return undefined;
19
- }
20
- const parsed = parse(content);
21
- if (!isRecord(parsed)) {
22
- return undefined;
1
+ import path from 'node:path';
2
+ import { resolveWorkspace } from '@williamthorsen/nmr-core/workspace';
3
+ export function describeEmptyWorkspace(workspace) {
4
+ const declaredClause = describeDeclaredPatterns(workspace.patterns);
5
+ switch (workspace.cause) {
6
+ case 'all-excluded':
7
+ return (`pnpm-workspace.yaml ${declaredClause}, whose \`!\` entries exclude every directory matched by the ` +
8
+ 'positive patterns. Drop or narrow the exclusion.');
9
+ case 'no-package':
10
+ return (`pnpm-workspace.yaml ${declaredClause}, and the matcher found no directory holding a \`package.json\`. ` +
11
+ 'Add one to the directory that should be a package, or declare a pattern reaching a directory that ' +
12
+ 'holds one.');
13
+ case 'no-pattern':
14
+ return (`pnpm-workspace.yaml ${declaredClause}, so no pattern reaches the matcher. Declare a positive pattern ` +
15
+ 'such as `packages/*`, and quote any `!` entry, which YAML reads as a tag rather than a string where ' +
16
+ 'it stands bare.');
17
+ case 'unreadable-manifest':
18
+ return ('pnpm-workspace.yaml holds no valid YAML, so nothing it declares reaches the matcher. Repair the ' +
19
+ 'syntax error and run the command again. An unterminated quoted string and a mis-indented entry are ' +
20
+ 'the usual ones.');
21
+ case 'unreadable-packages':
22
+ return ('pnpm-workspace.yaml declares a `packages` value that is not a list of strings, so nothing it ' +
23
+ 'declares reaches the matcher. Make it a list whose every entry is a quoted pattern; a bare value ' +
24
+ 'and an entry YAML read as a number or a map are the usual ones.');
25
+ default: {
26
+ const unhandledCause = workspace.cause;
27
+ throw new Error(`Unhandled empty-workspace cause: ${String(unhandledCause)}`);
28
+ }
23
29
  }
24
- const packagesField = parsed['packages'];
25
- if (!Array.isArray(packagesField)) {
26
- return undefined;
30
+ }
31
+ export function discoverWorkspaces(monorepoRoot = process.cwd()) {
32
+ const resolution = resolveWorkspace(monorepoRoot);
33
+ if (resolution.kind === 'not-a-workspace') {
34
+ return { kind: 'single-package' };
27
35
  }
28
- const patterns = packagesField.filter((p) => typeof p === 'string');
29
- if (patterns.length === 0) {
30
- return undefined;
36
+ if (resolution.kind === 'empty') {
37
+ const { cause, patterns } = resolution;
38
+ return cause === 'no-packages-list' ? { kind: 'single-package' } : { cause, kind: 'empty', patterns };
31
39
  }
32
- const directories = [];
33
- for (const pattern of patterns) {
34
- const matches = await glob(pattern, { posix: true });
35
- for (const match of matches) {
36
- if (existsSync(join(match, 'package.json'))) {
37
- directories.push(match);
38
- }
39
- }
40
+ return {
41
+ ...resolution,
42
+ packageDirs: resolution.packageDirs.map((packageDir) => relativizeToPosix(monorepoRoot, packageDir)),
43
+ };
44
+ }
45
+ function describeDeclaredPatterns(patterns) {
46
+ const quotablePatterns = patterns.filter((pattern) => pattern.trim() !== '');
47
+ const emptiedCount = patterns.length - quotablePatterns.length;
48
+ if (emptiedCount === 0) {
49
+ return `declares ${renderQuotedList(patterns)}`;
40
50
  }
41
- return directories.length > 0 ? [...directories].sort() : undefined;
51
+ const emptiedClause = `${String(emptiedCount)} ${emptiedCount === 1 ? 'entry' : 'entries'} that YAML left empty`;
52
+ return quotablePatterns.length === 0
53
+ ? `declares ${emptiedClause}`
54
+ : `declares ${renderQuotedList(quotablePatterns)}, beside ${emptiedClause}`;
55
+ }
56
+ function relativizeToPosix(monorepoRoot, packageDir) {
57
+ const relativeDir = path.relative(monorepoRoot, packageDir);
58
+ return relativeDir === '' ? '.' : relativeDir.split(path.sep).join('/');
59
+ }
60
+ function renderQuotedList(patterns) {
61
+ return patterns.map((pattern) => `\`${pattern}\``).join(', ');
42
62
  }
@@ -1,6 +1,6 @@
1
- /** Wrap text in ANSI bold. */
2
- export declare function bold(text: string): string;
3
- /** Wrap text in ANSI dim. */
4
- export declare function dim(text: string): string;
5
- /** Render a section header for separating workspaces in CLI output. */
1
+ /** Styles text bold, returning it unstyled for a stream that cannot render color. */
2
+ export declare function bold(text: string, stream?: NodeJS.WritableStream): string;
3
+ /** Styles text dim, returning it unstyled for a stream that cannot render color. */
4
+ export declare function dim(text: string, stream?: NodeJS.WritableStream): string;
5
+ /** Renders a section header for separating workspaces in CLI output. */
6
6
  export declare function sectionHeader(name: string): string;
@@ -1,8 +1,9 @@
1
- export function bold(text) {
2
- return `\u{1B}[1m${text}\u{1B}[0m`;
1
+ import { styleText } from 'node:util';
2
+ export function bold(text, stream = process.stdout) {
3
+ return styleText('bold', text, { stream });
3
4
  }
4
- export function dim(text) {
5
- return `\u{1B}[2m${text}\u{1B}[0m`;
5
+ export function dim(text, stream = process.stdout) {
6
+ return styleText('dim', text, { stream });
6
7
  }
7
8
  export function sectionHeader(name) {
8
9
  return `━━━ ${name} ━━━`;
@@ -0,0 +1,3 @@
1
+ /** Glyphs that name no status; a status marker comes from nmr-core's `STATUS_GLYPHS`. */
2
+ export declare const RELEASE_GLYPHS: Readonly<Record<"plain" | "rich", Readonly<Record<"bump" | "dryRun" | "tag", import("@williamthorsen/toolbelt.terminal/candidate").Glyph>>>>;
3
+ export type ReleaseGlyphName = keyof (typeof RELEASE_GLYPHS)['rich'];
@@ -0,0 +1,6 @@
1
+ import { defineGlyphSet } from '@williamthorsen/nmr-core';
2
+ export const RELEASE_GLYPHS = defineGlyphSet({
3
+ bump: { plain: 'BUMP', rich: '📦' },
4
+ dryRun: { plain: '', rich: '🔍' },
5
+ tag: { plain: 'TAG', rich: '🔖' },
6
+ });