@williamthorsen/release-kit 4.8.0 → 5.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 (82) hide show
  1. package/CHANGELOG.md +74 -4
  2. package/README.md +310 -40
  3. package/cliff.toml.template +2 -1
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +44 -13
  6. package/dist/esm/buildDependencyGraph.d.ts +3 -3
  7. package/dist/esm/buildDependencyGraph.js +10 -10
  8. package/dist/esm/buildReleaseSummary.js +4 -4
  9. package/dist/esm/bumpAllVersions.d.ts +1 -0
  10. package/dist/esm/bumpAllVersions.js +16 -2
  11. package/dist/esm/bumpVersion.js +3 -0
  12. package/dist/esm/commitCommand.js +1 -1
  13. package/dist/esm/compareVersions.d.ts +1 -0
  14. package/dist/esm/compareVersions.js +27 -0
  15. package/dist/esm/createGithubRelease.d.ts +6 -2
  16. package/dist/esm/createGithubRelease.js +17 -17
  17. package/dist/esm/createGithubReleaseCommand.d.ts +1 -0
  18. package/dist/esm/createGithubReleaseCommand.js +41 -0
  19. package/dist/esm/defaults.js +5 -3
  20. package/dist/esm/deriveWorkspaceConfig.d.ts +2 -0
  21. package/dist/esm/deriveWorkspaceConfig.js +37 -0
  22. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +7 -0
  23. package/dist/esm/detectUndeclaredTagPrefixes.js +46 -0
  24. package/dist/esm/generateChangelogJson.js +37 -1
  25. package/dist/esm/generateChangelogs.d.ts +1 -1
  26. package/dist/esm/generateChangelogs.js +14 -3
  27. package/dist/esm/getCommitsSinceTarget.d.ts +1 -1
  28. package/dist/esm/getCommitsSinceTarget.js +8 -4
  29. package/dist/esm/index.d.ts +7 -3
  30. package/dist/esm/index.js +10 -3
  31. package/dist/esm/init/initCommand.js +1 -1
  32. package/dist/esm/init/scaffold.d.ts +1 -1
  33. package/dist/esm/init/scaffold.js +8 -5
  34. package/dist/esm/init/templates.d.ts +1 -0
  35. package/dist/esm/init/templates.js +33 -3
  36. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  37. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  38. package/dist/esm/loadConfig.d.ts +2 -1
  39. package/dist/esm/loadConfig.js +65 -12
  40. package/dist/esm/parseRequestedTags.d.ts +1 -0
  41. package/dist/esm/parseRequestedTags.js +10 -0
  42. package/dist/esm/prepareCommand.d.ts +3 -1
  43. package/dist/esm/prepareCommand.js +74 -26
  44. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  45. package/dist/esm/previewTagPrefixes.js +120 -0
  46. package/dist/esm/propagateBumps.d.ts +1 -0
  47. package/dist/esm/propagateBumps.js +1 -1
  48. package/dist/esm/publishCommand.js +8 -13
  49. package/dist/esm/pushCommand.js +5 -4
  50. package/dist/esm/readCurrentVersion.d.ts +1 -0
  51. package/dist/esm/readCurrentVersion.js +21 -0
  52. package/dist/esm/releasePrepare.d.ts +2 -0
  53. package/dist/esm/releasePrepare.js +72 -30
  54. package/dist/esm/releasePrepareMono.js +235 -112
  55. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  56. package/dist/esm/renderReleaseNotes.js +29 -2
  57. package/dist/esm/reportPrepare.js +100 -73
  58. package/dist/esm/resolveCliffConfigPath.js +1 -1
  59. package/dist/esm/resolveCommandTags.d.ts +1 -1
  60. package/dist/esm/resolveCommandTags.js +17 -13
  61. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  62. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  63. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  64. package/dist/esm/resolveReleaseTags.js +19 -14
  65. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  66. package/dist/esm/showTagPrefixesCommand.js +84 -0
  67. package/dist/esm/sync-labels/initCommand.js +1 -1
  68. package/dist/esm/sync-labels/presets.js +1 -1
  69. package/dist/esm/tagCommand.js +1 -1
  70. package/dist/esm/types.d.ts +22 -7
  71. package/dist/esm/validateConfig.js +179 -36
  72. package/dist/esm/version.d.ts +1 -1
  73. package/dist/esm/version.js +1 -1
  74. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  75. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  76. package/package.json +2 -2
  77. package/dist/esm/component.d.ts +0 -2
  78. package/dist/esm/component.js +0 -14
  79. package/dist/esm/findPackageRoot.d.ts +0 -1
  80. package/dist/esm/findPackageRoot.js +0 -17
  81. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  82. package/dist/esm/githubReleaseCommand.js +0 -35
@@ -1,86 +1,58 @@
1
1
  import { bold, dim, sectionHeader } from "./format.js";
2
2
  function reportPrepare(result) {
3
- const isMultiComponent = result.components.some((c) => c.name !== void 0);
4
- if (isMultiComponent) {
5
- return formatMultiComponent(result);
3
+ const isMultiWorkspace = result.workspaces.some((w) => w.name !== void 0);
4
+ if (isMultiWorkspace) {
5
+ return formatMultiWorkspace(result);
6
6
  }
7
- return formatSingleComponent(result);
7
+ return formatSingleWorkspace(result);
8
8
  }
9
- function formatSingleComponent(result) {
9
+ function formatSingleWorkspace(result) {
10
10
  const lines = [];
11
- const component = result.components[0];
12
- if (component === void 0) {
11
+ const workspace = result.workspaces[0];
12
+ if (workspace === void 0) {
13
13
  return "";
14
14
  }
15
- const since = component.previousTag === void 0 ? "the beginning" : component.previousTag;
16
- lines.push(dim(`Found ${component.commitCount} commits since ${since}`));
17
- if (component.parsedCommitCount !== void 0) {
18
- lines.push(dim(` Parsed ${component.parsedCommitCount} typed commits`));
15
+ const since = workspace.previousTag === void 0 ? "the beginning" : workspace.previousTag;
16
+ lines.push(dim(`Found ${workspace.commitCount} commits since ${since}`));
17
+ if (workspace.parsedCommitCount !== void 0) {
18
+ lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
19
19
  }
20
- formatUnparseableWarning(lines, component);
21
- if (component.status === "skipped") {
22
- lines.push(`\u23ED\uFE0F ${component.skipReason ?? "Skipped"}`);
20
+ formatUnparseableWarning(lines, workspace);
21
+ if (workspace.status === "skipped") {
22
+ lines.push(`\u23ED\uFE0F ${workspace.skipReason ?? "Skipped"}`);
23
23
  return lines.join("\n");
24
24
  }
25
- if (component.parsedCommitCount === void 0 && component.releaseType !== void 0) {
26
- lines.push(` Using bump override: ${component.releaseType}`);
27
- }
28
- if (component.releaseType !== void 0) {
29
- lines.push(dim(`Bumping versions (${component.releaseType})...`));
30
- }
31
- if (component.currentVersion !== void 0 && component.newVersion !== void 0 && component.releaseType !== void 0) {
32
- lines.push(`\u{1F4E6} ${component.currentVersion} \u2192 ${bold(component.newVersion)} (${component.releaseType})`);
25
+ if (workspace.setVersion !== void 0) {
26
+ lines.push(` Using version override: ${workspace.setVersion}`);
27
+ } else if (workspace.parsedCommitCount === void 0 && workspace.releaseType !== void 0) {
28
+ lines.push(` Using bump override: ${workspace.releaseType}`);
29
+ }
30
+ if (workspace.releaseType !== void 0) {
31
+ lines.push(dim(`Bumping versions (${workspace.releaseType})...`));
32
+ } else if (workspace.setVersion !== void 0) {
33
+ lines.push(dim(`Bumping versions (version override)...`));
34
+ }
35
+ if (workspace.currentVersion !== void 0 && workspace.newVersion !== void 0) {
36
+ if (workspace.setVersion !== void 0) {
37
+ lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (version override)`);
38
+ } else if (workspace.releaseType !== void 0) {
39
+ lines.push(`\u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (${workspace.releaseType})`);
40
+ }
33
41
  }
34
- formatBumpFiles(lines, component, result.dryRun);
42
+ formatBumpFiles(lines, workspace, result.dryRun);
35
43
  lines.push(dim("Generating changelogs..."));
36
- formatChangelogFiles(lines, component, result.dryRun);
44
+ formatChangelogFiles(lines, workspace, result.dryRun);
37
45
  formatFormatCommand(lines, result);
38
46
  lines.push(`\u2705 Release preparation complete.`);
39
- if (component.tag !== void 0) {
40
- lines.push(` \u{1F3F7}\uFE0F ${bold(component.tag)}`);
47
+ if (workspace.tag !== void 0) {
48
+ lines.push(` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
41
49
  }
42
50
  return lines.join("\n");
43
51
  }
44
- function formatMultiComponent(result) {
52
+ function formatMultiWorkspace(result) {
45
53
  const lines = [];
46
- for (const component of result.components) {
47
- if (component.name !== void 0) {
48
- lines.push(`
49
- ${sectionHeader(component.name)}`);
50
- }
51
- const since = component.previousTag === void 0 ? "(no previous release found)" : `since ${component.previousTag}`;
52
- lines.push(dim(` Found ${component.commitCount} commits ${since}`));
53
- if (component.status === "skipped") {
54
- lines.push(` \u23ED\uFE0F ${component.skipReason ?? "Skipped"}`);
55
- continue;
56
- }
57
- const { propagatedFrom } = component;
58
- const isPropagatedOnly = propagatedFrom !== void 0 && component.commitCount === 0;
59
- if (isPropagatedOnly) {
60
- const depNames = propagatedFrom.map((p) => p.packageName).join(", ");
61
- lines.push(dim(` 0 commits (bumped via dependency: ${depNames})`));
62
- } else if (component.parsedCommitCount !== void 0) {
63
- lines.push(dim(` Parsed ${component.parsedCommitCount} typed commits`));
64
- }
65
- formatUnparseableWarning(lines, component, " ");
66
- if (component.parsedCommitCount === void 0 && component.releaseType !== void 0 && !isPropagatedOnly) {
67
- lines.push(` Using bump override: ${component.releaseType}`);
68
- }
69
- if (component.releaseType !== void 0) {
70
- lines.push(dim(` Bumping versions (${component.releaseType})...`));
71
- }
72
- if (component.currentVersion !== void 0 && component.newVersion !== void 0 && component.releaseType !== void 0) {
73
- const suffix = isPropagatedOnly ? formatPropagationSuffix(propagatedFrom) : "";
74
- lines.push(
75
- ` \u{1F4E6} ${component.currentVersion} \u2192 ${bold(component.newVersion)} (${component.releaseType}${suffix})`
76
- );
77
- }
78
- formatBumpFiles(lines, component, result.dryRun, " ");
79
- lines.push(dim(" Generating changelogs..."));
80
- formatChangelogFiles(lines, component, result.dryRun, " ");
81
- if (component.tag !== void 0) {
82
- lines.push(` \u{1F3F7}\uFE0F ${bold(component.tag)}`);
83
- }
54
+ for (const workspace of result.workspaces) {
55
+ formatWorkspaceSection(lines, workspace, result.dryRun);
84
56
  }
85
57
  formatFormatCommand(lines, result);
86
58
  formatWarnings(lines, result);
@@ -92,12 +64,67 @@ ${sectionHeader(component.name)}`);
92
64
  }
93
65
  } else {
94
66
  lines.push(`
95
- \u23ED\uFE0F No components had release-worthy changes.`);
67
+ \u23ED\uFE0F No workspaces had release-worthy changes.`);
96
68
  }
97
69
  return lines.join("\n");
98
70
  }
99
- function formatBumpFiles(lines, component, dryRun, indent = "") {
100
- for (const file of component.bumpedFiles) {
71
+ function formatWorkspaceSection(lines, workspace, dryRun) {
72
+ if (workspace.name !== void 0) {
73
+ lines.push(`
74
+ ${sectionHeader(workspace.name)}`);
75
+ }
76
+ const since = workspace.previousTag === void 0 ? "(no previous release found)" : `since ${workspace.previousTag}`;
77
+ lines.push(dim(` Found ${workspace.commitCount} commits ${since}`));
78
+ if (workspace.status === "skipped") {
79
+ lines.push(` \u23ED\uFE0F ${workspace.skipReason ?? "Skipped"}`);
80
+ return;
81
+ }
82
+ const { propagatedFrom } = workspace;
83
+ const isPropagatedOnly = propagatedFrom !== void 0 && workspace.commitCount === 0;
84
+ formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly);
85
+ formatUnparseableWarning(lines, workspace, " ");
86
+ formatBumpLabels(lines, workspace, isPropagatedOnly);
87
+ formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly);
88
+ formatBumpFiles(lines, workspace, dryRun, " ");
89
+ lines.push(dim(" Generating changelogs..."));
90
+ formatChangelogFiles(lines, workspace, dryRun, " ");
91
+ if (workspace.tag !== void 0) {
92
+ lines.push(` \u{1F3F7}\uFE0F ${bold(workspace.tag)}`);
93
+ }
94
+ }
95
+ function formatCommitSummary(lines, workspace, propagatedFrom, isPropagatedOnly) {
96
+ if (isPropagatedOnly && propagatedFrom !== void 0) {
97
+ const depNames = propagatedFrom.map((p) => p.packageName).join(", ");
98
+ lines.push(dim(` 0 commits (bumped via dependency: ${depNames})`));
99
+ } else if (workspace.parsedCommitCount !== void 0) {
100
+ lines.push(dim(` Parsed ${workspace.parsedCommitCount} typed commits`));
101
+ }
102
+ }
103
+ function formatBumpLabels(lines, workspace, isPropagatedOnly) {
104
+ if (workspace.setVersion !== void 0) {
105
+ lines.push(` Using version override: ${workspace.setVersion}`);
106
+ } else if (workspace.parsedCommitCount === void 0 && workspace.releaseType !== void 0 && !isPropagatedOnly) {
107
+ lines.push(` Using bump override: ${workspace.releaseType}`);
108
+ }
109
+ if (workspace.releaseType !== void 0) {
110
+ lines.push(dim(` Bumping versions (${workspace.releaseType})...`));
111
+ } else if (workspace.setVersion !== void 0) {
112
+ lines.push(dim(` Bumping versions (version override)...`));
113
+ }
114
+ }
115
+ function formatVersionLine(lines, workspace, propagatedFrom, isPropagatedOnly) {
116
+ if (workspace.currentVersion === void 0 || workspace.newVersion === void 0) {
117
+ return;
118
+ }
119
+ if (workspace.setVersion !== void 0) {
120
+ lines.push(` \u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (version override)`);
121
+ } else if (workspace.releaseType !== void 0) {
122
+ const suffix = isPropagatedOnly ? formatPropagationSuffix(propagatedFrom) : "";
123
+ lines.push(` \u{1F4E6} ${workspace.currentVersion} \u2192 ${bold(workspace.newVersion)} (${workspace.releaseType}${suffix})`);
124
+ }
125
+ }
126
+ function formatBumpFiles(lines, workspace, dryRun, indent = "") {
127
+ for (const file of workspace.bumpedFiles) {
101
128
  if (dryRun) {
102
129
  lines.push(dim(`${indent} [dry-run] Would bump ${file}`));
103
130
  } else {
@@ -105,8 +132,8 @@ function formatBumpFiles(lines, component, dryRun, indent = "") {
105
132
  }
106
133
  }
107
134
  }
108
- function formatChangelogFiles(lines, component, dryRun, indent = "") {
109
- for (const file of component.changelogFiles) {
135
+ function formatChangelogFiles(lines, workspace, dryRun, indent = "") {
136
+ for (const file of workspace.changelogFiles) {
110
137
  if (dryRun) {
111
138
  lines.push(dim(`${indent} [dry-run] Would run: npx --yes git-cliff ... --output ${file}`));
112
139
  } else {
@@ -114,13 +141,13 @@ function formatChangelogFiles(lines, component, dryRun, indent = "") {
114
141
  }
115
142
  }
116
143
  }
117
- function formatUnparseableWarning(lines, component, indent = "") {
118
- const unparseable = component.unparseableCommits;
144
+ function formatUnparseableWarning(lines, workspace, indent = "") {
145
+ const unparseable = workspace.unparseableCommits;
119
146
  if (unparseable === void 0 || unparseable.length === 0) {
120
147
  return;
121
148
  }
122
149
  const count = unparseable.length;
123
- const isPatchFloor = component.parsedCommitCount === 0;
150
+ const isPatchFloor = workspace.parsedCommitCount === 0;
124
151
  const suffix = isPatchFloor ? " (defaulting to patch bump)" : "";
125
152
  lines.push(`${indent} \u26A0\uFE0F ${count} commit${count === 1 ? "" : "s"} could not be parsed${suffix}`);
126
153
  for (const commit of unparseable) {
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { findPackageRoot } from "./findPackageRoot.js";
3
+ import { findPackageRoot } from "@williamthorsen/nmr-core";
4
4
  function resolveCliffConfigPath(cliffConfigPath, metaUrl) {
5
5
  if (cliffConfigPath !== void 0) {
6
6
  return cliffConfigPath;
@@ -1,2 +1,2 @@
1
1
  import type { ResolvedTag } from './resolveReleaseTags.ts';
2
- export declare function resolveCommandTags(only: string[] | undefined): Promise<ResolvedTag[]>;
2
+ export declare function resolveCommandTags(tags: string[] | undefined): Promise<ResolvedTag[]>;
@@ -1,7 +1,7 @@
1
- import { basename } from "node:path";
1
+ import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
2
2
  import { discoverWorkspaces } from "./discoverWorkspaces.js";
3
3
  import { resolveReleaseTags } from "./resolveReleaseTags.js";
4
- async function resolveCommandTags(only) {
4
+ async function resolveCommandTags(tags) {
5
5
  let discoveredPaths;
6
6
  try {
7
7
  discoveredPaths = await discoverWorkspaces();
@@ -9,25 +9,29 @@ async function resolveCommandTags(only) {
9
9
  console.error(`Error discovering workspaces: ${error instanceof Error ? error.message : String(error)}`);
10
10
  process.exit(1);
11
11
  }
12
- if (only !== void 0 && discoveredPaths === void 0) {
13
- console.error("Error: --only is only supported for monorepo configurations");
14
- process.exit(1);
12
+ let workspaces;
13
+ if (discoveredPaths !== void 0) {
14
+ try {
15
+ workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
16
+ } catch (error) {
17
+ console.error(`Error resolving workspaces: ${error instanceof Error ? error.message : String(error)}`);
18
+ process.exit(1);
19
+ }
15
20
  }
16
- const workspaceMap = discoveredPaths === void 0 ? void 0 : new Map(discoveredPaths.map((p) => [basename(p), p]));
17
- let resolvedTags = resolveReleaseTags(workspaceMap);
21
+ let resolvedTags = resolveReleaseTags(workspaces);
18
22
  if (resolvedTags.length === 0) {
19
23
  console.error("Error: No release tags found on HEAD. Create tags with `release-kit tag` first.");
20
24
  process.exit(1);
21
25
  }
22
- if (only !== void 0) {
23
- const availableNames = resolvedTags.map((t) => t.dir);
24
- for (const name of only) {
25
- if (!availableNames.includes(name)) {
26
- console.error(`Error: Unknown package "${name}" in --only. Available: ${availableNames.join(", ")}`);
26
+ if (tags !== void 0) {
27
+ const availableTagNames = resolvedTags.map((t) => t.tag);
28
+ for (const name of tags) {
29
+ if (!availableTagNames.includes(name)) {
30
+ console.error(`Error: Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(", ")}`);
27
31
  process.exit(1);
28
32
  }
29
33
  }
30
- resolvedTags = resolvedTags.filter((t) => only.includes(t.dir));
34
+ resolvedTags = resolvedTags.filter((t) => tags.includes(t.tag));
31
35
  }
32
36
  return resolvedTags;
33
37
  }
@@ -2,5 +2,12 @@ import type { ReleaseNotesConfig } from './types.ts';
2
2
  export interface ResolvedReleaseNotesConfig {
3
3
  releaseNotes: ReleaseNotesConfig;
4
4
  changelogJsonOutputPath: string;
5
+ sectionOrder: string[];
5
6
  }
6
- export declare function resolveReleaseNotesConfig(): Promise<ResolvedReleaseNotesConfig>;
7
+ export interface ResolveReleaseNotesConfigOptions {
8
+ strictLoad?: boolean;
9
+ }
10
+ export declare function resolveReleaseNotesConfig(options?: ResolveReleaseNotesConfigOptions): Promise<ResolvedReleaseNotesConfig>;
11
+ export declare function deriveSectionOrder(workTypes: Record<string, {
12
+ header: string;
13
+ }>): string[];
@@ -1,19 +1,24 @@
1
1
  import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_RELEASE_NOTES_CONFIG } from "./defaults.js";
2
- import { loadConfig } from "./loadConfig.js";
2
+ import { loadConfig, resolveWorkTypes } from "./loadConfig.js";
3
3
  import { validateConfig } from "./validateConfig.js";
4
- async function resolveReleaseNotesConfig() {
4
+ async function resolveReleaseNotesConfig(options = {}) {
5
+ const { strictLoad = false } = options;
5
6
  let rawConfig;
6
7
  try {
7
8
  rawConfig = await loadConfig();
8
9
  } catch (error) {
9
- console.warn(
10
- `Warning: failed to load config; using defaults: ${error instanceof Error ? error.message : String(error)}`
11
- );
10
+ const message = error instanceof Error ? error.message : String(error);
11
+ if (strictLoad) {
12
+ console.error(`Error: failed to load config: ${message}`);
13
+ process.exit(1);
14
+ }
15
+ console.warn(`Warning: failed to load config; using defaults: ${message}`);
12
16
  }
13
17
  if (rawConfig === void 0) {
14
18
  return {
15
19
  releaseNotes: { ...DEFAULT_RELEASE_NOTES_CONFIG },
16
- changelogJsonOutputPath: DEFAULT_CHANGELOG_JSON_CONFIG.outputPath
20
+ changelogJsonOutputPath: DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
21
+ sectionOrder: deriveSectionOrder(resolveWorkTypes())
17
22
  };
18
23
  }
19
24
  const { config, errors, warnings } = validateConfig(rawConfig);
@@ -29,9 +34,14 @@ async function resolveReleaseNotesConfig() {
29
34
  }
30
35
  return {
31
36
  releaseNotes: { ...DEFAULT_RELEASE_NOTES_CONFIG, ...config.releaseNotes },
32
- changelogJsonOutputPath: config.changelogJson?.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath
37
+ changelogJsonOutputPath: config.changelogJson?.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
38
+ sectionOrder: deriveSectionOrder(resolveWorkTypes(config.workTypes))
33
39
  };
34
40
  }
41
+ function deriveSectionOrder(workTypes) {
42
+ return Object.values(workTypes).map((entry) => entry.header);
43
+ }
35
44
  export {
45
+ deriveSectionOrder,
36
46
  resolveReleaseNotesConfig
37
47
  };
@@ -1,6 +1,7 @@
1
+ import type { WorkspaceConfig } from './types.ts';
1
2
  export interface ResolvedTag {
2
3
  tag: string;
3
4
  dir: string;
4
5
  workspacePath: string;
5
6
  }
6
- export declare function resolveReleaseTags(workspaceMap?: Map<string, string>): ResolvedTag[];
7
+ export declare function resolveReleaseTags(workspaces?: readonly WorkspaceConfig[]): ResolvedTag[];
@@ -1,12 +1,13 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  const VERSION_PATTERN = /^v\d+\.\d+\.\d+/;
3
- function resolveReleaseTags(workspaceMap) {
3
+ const SEMVER_SUFFIX_PATTERN = /^\d+\.\d+\.\d+/;
4
+ function resolveReleaseTags(workspaces) {
4
5
  const output = execFileSync("git", ["tag", "--points-at", "HEAD"], { encoding: "utf8" });
5
6
  const tags = output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
6
- if (workspaceMap === void 0) {
7
+ if (workspaces === void 0) {
7
8
  return resolveSinglePackageTags(tags);
8
9
  }
9
- return resolveMonorepoTags(tags, workspaceMap);
10
+ return resolveMonorepoTags(tags, workspaces);
10
11
  }
11
12
  function resolveSinglePackageTags(tags) {
12
13
  const matched = tags.filter((tag) => VERSION_PATTERN.test(tag));
@@ -18,24 +19,28 @@ function resolveSinglePackageTags(tags) {
18
19
  }
19
20
  return matched.map((tag) => ({ tag, dir: ".", workspacePath: "." }));
20
21
  }
21
- function resolveMonorepoTags(tags, workspaceMap) {
22
+ function resolveMonorepoTags(tags, workspaces) {
23
+ const sortedWorkspaces = [...workspaces].sort((a, b) => b.tagPrefix.length - a.tagPrefix.length);
22
24
  const resolved = [];
23
25
  for (const tag of tags) {
24
- const dashV = tag.lastIndexOf("-v");
25
- if (dashV === -1) {
26
- continue;
26
+ const match = findMatchingWorkspace(tag, sortedWorkspaces);
27
+ if (match !== void 0) {
28
+ resolved.push({ tag, dir: match.dir, workspacePath: match.workspacePath });
27
29
  }
28
- const dir = tag.slice(0, dashV);
29
- const versionPart = tag.slice(dashV + 1);
30
- if (!VERSION_PATTERN.test(versionPart)) {
30
+ }
31
+ return resolved;
32
+ }
33
+ function findMatchingWorkspace(tag, sortedWorkspaces) {
34
+ for (const workspace of sortedWorkspaces) {
35
+ if (!tag.startsWith(workspace.tagPrefix)) {
31
36
  continue;
32
37
  }
33
- const workspacePath = workspaceMap.get(dir);
34
- if (workspacePath !== void 0) {
35
- resolved.push({ tag, dir, workspacePath });
38
+ const versionSuffix = tag.slice(workspace.tagPrefix.length);
39
+ if (SEMVER_SUFFIX_PATTERN.test(versionSuffix)) {
40
+ return workspace;
36
41
  }
37
42
  }
38
- return resolved;
43
+ return void 0;
39
44
  }
40
45
  export {
41
46
  resolveReleaseTags
@@ -0,0 +1 @@
1
+ export declare function showTagPrefixesCommand(): Promise<number>;
@@ -0,0 +1,84 @@
1
+ import { detectRepoType } from "./init/detectRepoType.js";
2
+ import { previewTagPrefixes } from "./previewTagPrefixes.js";
3
+ async function showTagPrefixesCommand() {
4
+ if (detectRepoType() === "single-package") {
5
+ process.stdout.write(renderSinglePackage());
6
+ return 0;
7
+ }
8
+ const preview = await previewTagPrefixes();
9
+ process.stdout.write(renderMonorepo(preview));
10
+ return computeExitCode(preview);
11
+ }
12
+ function renderSinglePackage() {
13
+ const lines = [
14
+ "Workspace Derived prefix Status",
15
+ ". v (single-package mode)",
16
+ ""
17
+ ];
18
+ return lines.join("\n");
19
+ }
20
+ function renderMonorepo(preview) {
21
+ const lines = ["Workspace tag prefixes:", ""];
22
+ for (const row of preview.workspaces) {
23
+ lines.push(...renderWorkspaceRow(row));
24
+ }
25
+ if (preview.collisions.length > 0) {
26
+ lines.push(
27
+ "",
28
+ ...preview.collisions.map(
29
+ (collision) => `\u26D4 tag prefix collision: '${collision.tagPrefix}' used by ${collision.workspacePaths.join(", ")}`
30
+ )
31
+ );
32
+ }
33
+ if (preview.undeclaredCandidates.length > 0) {
34
+ lines.push(
35
+ "",
36
+ "Undeclared tag prefixes:",
37
+ "",
38
+ ...preview.undeclaredCandidates.map(
39
+ (candidate) => ` '${candidate.prefix}' \u2014 ${candidate.tagCount} tags (e.g., ${candidate.exampleTags.join(", ")})`
40
+ ),
41
+ "",
42
+ "Suggested config snippet (adjust `dir` to match your workspace if the guess is wrong, and replace the `name` placeholder with the legacy npm name):",
43
+ "",
44
+ renderSuggestedSnippet(preview.undeclaredCandidates),
45
+ "",
46
+ "If the suggested `dir` does not match your workspace, adjust before pasting. Each legacy identity requires a `name` \u2014 replace the `TODO-fill-in-legacy-npm-name` placeholder with the package's prior npm name."
47
+ );
48
+ }
49
+ lines.push("");
50
+ return lines.join("\n");
51
+ }
52
+ function renderWorkspaceRow(row) {
53
+ const lines = [];
54
+ if (row.derivedPrefix === null) {
55
+ lines.push(` ${row.workspacePath} \u2014 \u26D4 derivation failed: ${row.derivationError ?? "unknown error"}`);
56
+ return lines;
57
+ }
58
+ const statusMarker = row.derivedTagCount > 0 ? `\u2705 ${row.derivedTagCount} tags` : "\u26A0\uFE0F no existing tags";
59
+ lines.push(` ${row.workspacePath} \u2014 derived prefix '${row.derivedPrefix}', ${statusMarker}`);
60
+ for (const entry of row.legacyEntries) {
61
+ if (entry.tagCount > 0) {
62
+ lines.push(` \u2705 ${entry.tagCount} legacy tags with '${entry.prefix}' prefix (recognized)`);
63
+ } else {
64
+ lines.push(` \u26A0\uFE0F recorded legacy prefix '${entry.prefix}' has no tags`);
65
+ }
66
+ }
67
+ return lines;
68
+ }
69
+ function renderSuggestedSnippet(candidates) {
70
+ const entries = candidates.map(
71
+ (candidate) => ` { dir: '${candidate.suggestedDir}', legacyIdentities: [{ name: 'TODO-fill-in-legacy-npm-name', tagPrefix: '${candidate.prefix}' }] },`
72
+ ).join("\n");
73
+ return ` workspaces: [
74
+ ${entries}
75
+ ],`;
76
+ }
77
+ function computeExitCode(preview) {
78
+ const hasDerivationFailure = preview.workspaces.some((row) => row.derivedPrefix === null);
79
+ const hasCollision = preview.collisions.length > 0;
80
+ return hasDerivationFailure || hasCollision ? 1 : 0;
81
+ }
82
+ export {
83
+ showTagPrefixesCommand
84
+ };
@@ -1,4 +1,4 @@
1
- import { reportWriteResult, writeFileWithCheck } from "@williamthorsen/node-monorepo-core";
1
+ import { reportWriteResult, writeFileWithCheck } from "@williamthorsen/nmr-core";
2
2
  import { discoverWorkspaces } from "../discoverWorkspaces.js";
3
3
  import { generateCommand, LABELS_OUTPUT_PATH } from "./generateCommand.js";
4
4
  import { SYNC_LABELS_CONFIG_PATH } from "./loadSyncLabelsConfig.js";
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
+ import { findPackageRoot } from "@williamthorsen/nmr-core";
4
5
  import { load } from "js-yaml";
5
- import { findPackageRoot } from "../findPackageRoot.js";
6
6
  import { isRecord } from "../typeGuards.js";
7
7
  function resolvePresetPath(presetName) {
8
8
  const root = findPackageRoot(import.meta.url);
@@ -1,4 +1,4 @@
1
- import { parseArgs, translateParseError } from "@williamthorsen/node-monorepo-core";
1
+ import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
2
2
  import { createTags } from "./createTags.js";
3
3
  const tagFlagSchema = {
4
4
  dryRun: { long: "--dry-run", type: "boolean" },
@@ -2,6 +2,7 @@ export type ReleaseType = 'major' | 'minor' | 'patch';
2
2
  export type ChangelogAudience = 'all' | 'dev';
3
3
  export interface ChangelogItem {
4
4
  description: string;
5
+ body?: string;
5
6
  }
6
7
  export interface ChangelogSection {
7
8
  title: string;
@@ -20,7 +21,6 @@ export interface ChangelogJsonConfig {
20
21
  }
21
22
  export interface ReleaseNotesConfig {
22
23
  shouldInjectIntoReadme: boolean;
23
- shouldCreateGithubRelease: boolean;
24
24
  }
25
25
  export interface PropagationSource {
26
26
  packageName: string;
@@ -31,7 +31,7 @@ export interface BumpResult {
31
31
  newVersion: string;
32
32
  files: string[];
33
33
  }
34
- export interface ComponentPrepareResult {
34
+ export interface WorkspacePrepareResult {
35
35
  name?: string | undefined;
36
36
  status: 'released' | 'skipped';
37
37
  previousTag?: string | undefined;
@@ -47,9 +47,10 @@ export interface ComponentPrepareResult {
47
47
  unparseableCommits?: Commit[] | undefined;
48
48
  propagatedFrom?: PropagationSource[] | undefined;
49
49
  skipReason?: string | undefined;
50
+ setVersion?: string | undefined;
50
51
  }
51
52
  export interface PrepareResult {
52
- components: ComponentPrepareResult[];
53
+ workspaces: WorkspacePrepareResult[];
53
54
  tags: string[];
54
55
  formatCommand?: {
55
56
  command: string;
@@ -68,7 +69,7 @@ export interface VersionPatterns {
68
69
  minor: string[];
69
70
  }
70
71
  export interface ReleaseKitConfig {
71
- components?: ComponentOverride[];
72
+ workspaces?: WorkspaceOverride[];
72
73
  versionPatterns?: VersionPatterns;
73
74
  workTypes?: Record<string, WorkTypeConfig>;
74
75
  formatCommand?: string;
@@ -76,10 +77,21 @@ export interface ReleaseKitConfig {
76
77
  scopeAliases?: Record<string, string>;
77
78
  changelogJson?: Partial<ChangelogJsonConfig>;
78
79
  releaseNotes?: Partial<ReleaseNotesConfig>;
80
+ retiredPackages?: RetiredPackage[];
79
81
  }
80
- export interface ComponentOverride {
82
+ export interface LegacyIdentity {
83
+ name: string;
84
+ tagPrefix: string;
85
+ }
86
+ export interface RetiredPackage {
87
+ name: string;
88
+ tagPrefix: string;
89
+ successor?: string;
90
+ }
91
+ export interface WorkspaceOverride {
81
92
  dir: string;
82
93
  shouldExclude?: boolean;
94
+ legacyIdentities?: LegacyIdentity[];
83
95
  }
84
96
  export interface Commit {
85
97
  message: string;
@@ -93,15 +105,18 @@ export interface ParsedCommit {
93
105
  scope?: string;
94
106
  breaking: boolean;
95
107
  }
96
- export interface ComponentConfig {
108
+ export interface WorkspaceConfig {
97
109
  dir: string;
110
+ name: string;
98
111
  tagPrefix: string;
112
+ workspacePath: string;
99
113
  packageFiles: string[];
100
114
  changelogPaths: string[];
101
115
  paths: string[];
116
+ legacyIdentities?: LegacyIdentity[];
102
117
  }
103
118
  export interface MonorepoReleaseConfig {
104
- components: ComponentConfig[];
119
+ workspaces: WorkspaceConfig[];
105
120
  workTypes?: Record<string, WorkTypeConfig>;
106
121
  versionPatterns?: VersionPatterns;
107
122
  formatCommand?: string;