@williamthorsen/release-kit 2.1.0 → 2.3.1

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 (68) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +1 -11
  3. package/bin/release-kit.js +2 -0
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +148 -0
  6. package/dist/esm/bumpAllVersions.d.ts +2 -2
  7. package/dist/esm/bumpAllVersions.js +1 -4
  8. package/dist/esm/component.d.ts +1 -1
  9. package/dist/esm/component.js +2 -3
  10. package/dist/esm/createTags.d.ts +5 -0
  11. package/dist/esm/createTags.js +73 -0
  12. package/dist/esm/detectPackageManager.d.ts +2 -0
  13. package/dist/esm/detectPackageManager.js +44 -0
  14. package/dist/esm/findPackageRoot.d.ts +1 -0
  15. package/dist/esm/findPackageRoot.js +17 -0
  16. package/dist/esm/format.d.ts +3 -0
  17. package/dist/esm/format.js +14 -0
  18. package/dist/esm/generateChangelogs.d.ts +2 -2
  19. package/dist/esm/generateChangelogs.js +12 -12
  20. package/dist/esm/hasPrettierConfig.d.ts +1 -0
  21. package/dist/esm/hasPrettierConfig.js +42 -0
  22. package/dist/esm/index.d.ts +12 -2
  23. package/dist/esm/index.js +11 -2
  24. package/dist/esm/init/initCommand.js +9 -2
  25. package/dist/esm/init/scaffold.d.ts +3 -2
  26. package/dist/esm/init/scaffold.js +18 -44
  27. package/dist/esm/init/templates.js +14 -4
  28. package/dist/esm/loadConfig.js +0 -6
  29. package/dist/esm/prepareCommand.d.ts +10 -0
  30. package/dist/esm/prepareCommand.js +96 -20
  31. package/dist/esm/publish.d.ts +7 -0
  32. package/dist/esm/publish.js +49 -0
  33. package/dist/esm/publishCommand.d.ts +1 -0
  34. package/dist/esm/publishCommand.js +54 -0
  35. package/dist/esm/releasePrepare.d.ts +2 -2
  36. package/dist/esm/releasePrepare.js +46 -17
  37. package/dist/esm/releasePrepareMono.d.ts +2 -2
  38. package/dist/esm/releasePrepareMono.js +54 -24
  39. package/dist/esm/reportPrepare.d.ts +2 -0
  40. package/dist/esm/reportPrepare.js +120 -0
  41. package/dist/esm/resolveCliffConfigPath.js +4 -4
  42. package/dist/esm/resolveReleaseTags.d.ts +6 -0
  43. package/dist/esm/resolveReleaseTags.js +42 -0
  44. package/dist/esm/runReleasePrepare.js +3 -2
  45. package/dist/esm/sync-labels/generateCommand.d.ts +4 -0
  46. package/dist/esm/sync-labels/generateCommand.js +51 -0
  47. package/dist/esm/sync-labels/initCommand.d.ts +6 -0
  48. package/dist/esm/sync-labels/initCommand.js +62 -0
  49. package/dist/esm/sync-labels/loadSyncLabelsConfig.d.ts +3 -0
  50. package/dist/esm/sync-labels/loadSyncLabelsConfig.js +59 -0
  51. package/dist/esm/sync-labels/presets.d.ts +2 -0
  52. package/dist/esm/sync-labels/presets.js +40 -0
  53. package/dist/esm/sync-labels/resolveLabels.d.ts +2 -0
  54. package/dist/esm/sync-labels/resolveLabels.js +43 -0
  55. package/dist/esm/sync-labels/scaffold.d.ts +6 -0
  56. package/dist/esm/sync-labels/scaffold.js +25 -0
  57. package/dist/esm/sync-labels/syncCommand.d.ts +1 -0
  58. package/dist/esm/sync-labels/syncCommand.js +33 -0
  59. package/dist/esm/sync-labels/templates.d.ts +4 -0
  60. package/dist/esm/sync-labels/templates.js +50 -0
  61. package/dist/esm/sync-labels/types.d.ts +9 -0
  62. package/dist/esm/sync-labels/types.js +0 -0
  63. package/dist/esm/tagCommand.d.ts +1 -0
  64. package/dist/esm/tagCommand.js +20 -0
  65. package/dist/esm/types.d.ts +29 -1
  66. package/dist/esm/validateConfig.js +11 -6
  67. package/package.json +12 -5
  68. package/presets/labels/common.yaml +52 -0
@@ -4,6 +4,7 @@ import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
4
4
  import { determineBumpType } from "./determineBumpType.js";
5
5
  import { generateChangelog } from "./generateChangelogs.js";
6
6
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
7
+ import { hasPrettierConfig } from "./hasPrettierConfig.js";
7
8
  import { parseCommitMessage } from "./parseCommitMessage.js";
8
9
  function releasePrepareMono(config, options) {
9
10
  const { dryRun, force, bumpOverride } = options;
@@ -11,50 +12,76 @@ function releasePrepareMono(config, options) {
11
12
  const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
12
13
  const tags = [];
13
14
  const modifiedFiles = [];
15
+ const components = [];
14
16
  for (const component of config.components) {
15
17
  const name = component.dir;
16
- console.info(`
17
- Processing component: ${name}`);
18
- console.info(" Finding commits since last release...");
19
18
  const { tag, commits } = getCommitsSinceTarget(component.tagPrefix, component.paths);
20
19
  const since = tag === void 0 ? "(no previous release found)" : `since ${tag}`;
21
- console.info(` Found ${commits.length} commits ${since}`);
22
20
  if (commits.length === 0 && !force) {
23
- console.info(` No changes for ${name} ${since}. Skipping.`);
21
+ components.push({
22
+ name,
23
+ status: "skipped",
24
+ previousTag: tag,
25
+ commitCount: 0,
26
+ bumpedFiles: [],
27
+ changelogFiles: [],
28
+ skipReason: `No changes for ${name} ${since}. Skipping.`
29
+ });
24
30
  continue;
25
31
  }
26
32
  let releaseType;
33
+ let parsedCommitCount;
27
34
  if (bumpOverride === void 0) {
28
35
  const parsedCommits = commits.map((c) => parseCommitMessage(c.message, c.hash, workTypes, config.workspaceAliases)).filter((c) => c !== void 0);
29
- console.info(` Parsed ${parsedCommits.length} typed commits`);
36
+ parsedCommitCount = parsedCommits.length;
30
37
  releaseType = determineBumpType(parsedCommits, workTypes, versionPatterns);
31
38
  } else {
32
39
  releaseType = bumpOverride;
33
- console.info(` Using bump override: ${releaseType}`);
34
40
  }
35
41
  if (releaseType === void 0) {
36
- console.info(` No release-worthy changes for ${name} ${since}. Skipping.`);
42
+ components.push({
43
+ name,
44
+ status: "skipped",
45
+ previousTag: tag,
46
+ commitCount: commits.length,
47
+ parsedCommitCount,
48
+ bumpedFiles: [],
49
+ changelogFiles: [],
50
+ skipReason: `No release-worthy changes for ${name} ${since}. Skipping.`
51
+ });
37
52
  continue;
38
53
  }
39
- console.info(` Bumping versions (${releaseType})...`);
40
- const newVersion = bumpAllVersions(component.packageFiles, releaseType, dryRun);
41
- const newTag = `${component.tagPrefix}${newVersion}`;
54
+ const bump = bumpAllVersions(component.packageFiles, releaseType, dryRun);
55
+ const newTag = `${component.tagPrefix}${bump.newVersion}`;
42
56
  tags.push(newTag);
43
57
  modifiedFiles.push(...component.packageFiles, ...component.changelogPaths.map((p) => `${p}/CHANGELOG.md`));
44
- console.info(" Generating changelogs...");
58
+ const changelogFiles = [];
45
59
  for (const changelogPath of component.changelogPaths) {
46
- generateChangelog(config, changelogPath, newTag, dryRun, { includePaths: component.paths });
60
+ changelogFiles.push(
61
+ ...generateChangelog(config, changelogPath, newTag, dryRun, { includePaths: component.paths })
62
+ );
47
63
  }
48
- console.info(` Component release prepared: ${newTag}`);
64
+ components.push({
65
+ name,
66
+ status: "released",
67
+ previousTag: tag,
68
+ commitCount: commits.length,
69
+ parsedCommitCount,
70
+ releaseType,
71
+ currentVersion: bump.currentVersion,
72
+ newVersion: bump.newVersion,
73
+ tag: newTag,
74
+ bumpedFiles: bump.files,
75
+ changelogFiles
76
+ });
49
77
  }
50
- if (tags.length > 0 && config.formatCommand !== void 0) {
51
- const fullCommand = `${config.formatCommand} ${modifiedFiles.join(" ")}`;
78
+ const formatCommandStr = config.formatCommand ?? (hasPrettierConfig() ? "npx prettier --write" : void 0);
79
+ let formatCommand;
80
+ if (tags.length > 0 && formatCommandStr !== void 0) {
81
+ const fullCommand = `${formatCommandStr} ${modifiedFiles.join(" ")}`;
52
82
  if (dryRun) {
53
- console.info(`
54
- [dry-run] Would run format command: ${fullCommand}`);
83
+ formatCommand = { command: fullCommand, executed: false, files: modifiedFiles };
55
84
  } else {
56
- console.info(`
57
- Running format command: ${fullCommand}`);
58
85
  try {
59
86
  execSync(fullCommand, { stdio: "inherit" });
60
87
  } catch (error) {
@@ -62,12 +89,15 @@ Processing component: ${name}`);
62
89
  `Format command failed ('${fullCommand}'): ${error instanceof Error ? error.message : String(error)}`
63
90
  );
64
91
  }
92
+ formatCommand = { command: fullCommand, executed: true, files: modifiedFiles };
65
93
  }
66
94
  }
67
- const summary = tags.length > 0 ? "Monorepo release preparation complete." : "No components had release-worthy changes.";
68
- console.info(`
69
- ${summary}`);
70
- return tags;
95
+ return {
96
+ components,
97
+ tags,
98
+ formatCommand,
99
+ dryRun
100
+ };
71
101
  }
72
102
  export {
73
103
  releasePrepareMono
@@ -0,0 +1,2 @@
1
+ import type { PrepareResult } from './types.ts';
2
+ export declare function reportPrepare(result: PrepareResult): string;
@@ -0,0 +1,120 @@
1
+ import { bold, dim, sectionHeader } from "./format.js";
2
+ function reportPrepare(result) {
3
+ const isMultiComponent = result.components.some((c) => c.name !== void 0);
4
+ if (isMultiComponent) {
5
+ return formatMultiComponent(result);
6
+ }
7
+ return formatSingleComponent(result);
8
+ }
9
+ function formatSingleComponent(result) {
10
+ const lines = [];
11
+ const component = result.components[0];
12
+ if (component === void 0) {
13
+ return "";
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`));
19
+ }
20
+ if (component.status === "skipped") {
21
+ lines.push(`\u23ED\uFE0F ${component.skipReason ?? "Skipped"}`);
22
+ return lines.join("\n");
23
+ }
24
+ if (component.parsedCommitCount === void 0 && component.releaseType !== void 0) {
25
+ lines.push(` Using bump override: ${component.releaseType}`);
26
+ }
27
+ if (component.releaseType !== void 0) {
28
+ lines.push(dim(`Bumping versions (${component.releaseType})...`));
29
+ }
30
+ if (component.currentVersion !== void 0 && component.newVersion !== void 0 && component.releaseType !== void 0) {
31
+ lines.push(`\u{1F4E6} ${component.currentVersion} \u2192 ${bold(component.newVersion)} (${component.releaseType})`);
32
+ }
33
+ formatBumpFiles(lines, component, result.dryRun);
34
+ lines.push(dim("Generating changelogs..."));
35
+ formatChangelogFiles(lines, component, result.dryRun);
36
+ formatFormatCommand(lines, result);
37
+ lines.push(`\u2705 Release preparation complete.`);
38
+ if (component.tag !== void 0) {
39
+ lines.push(` \u{1F3F7}\uFE0F ${bold(component.tag)}`);
40
+ }
41
+ return lines.join("\n");
42
+ }
43
+ function formatMultiComponent(result) {
44
+ const lines = [];
45
+ for (const component of result.components) {
46
+ if (component.name !== void 0) {
47
+ lines.push(`
48
+ ${sectionHeader(component.name)}`);
49
+ }
50
+ const since = component.previousTag === void 0 ? "(no previous release found)" : `since ${component.previousTag}`;
51
+ lines.push(dim(` Found ${component.commitCount} commits ${since}`));
52
+ if (component.status === "skipped") {
53
+ lines.push(` \u23ED\uFE0F ${component.skipReason ?? "Skipped"}`);
54
+ continue;
55
+ }
56
+ if (component.parsedCommitCount !== void 0) {
57
+ lines.push(dim(` Parsed ${component.parsedCommitCount} typed commits`));
58
+ }
59
+ if (component.parsedCommitCount === void 0 && component.releaseType !== void 0) {
60
+ lines.push(` Using bump override: ${component.releaseType}`);
61
+ }
62
+ if (component.releaseType !== void 0) {
63
+ lines.push(dim(` Bumping versions (${component.releaseType})...`));
64
+ }
65
+ if (component.currentVersion !== void 0 && component.newVersion !== void 0 && component.releaseType !== void 0) {
66
+ lines.push(` \u{1F4E6} ${component.currentVersion} \u2192 ${bold(component.newVersion)} (${component.releaseType})`);
67
+ }
68
+ formatBumpFiles(lines, component, result.dryRun, " ");
69
+ lines.push(dim(" Generating changelogs..."));
70
+ formatChangelogFiles(lines, component, result.dryRun, " ");
71
+ if (component.tag !== void 0) {
72
+ lines.push(` \u{1F3F7}\uFE0F ${bold(component.tag)}`);
73
+ }
74
+ }
75
+ formatFormatCommand(lines, result);
76
+ if (result.tags.length > 0) {
77
+ lines.push(`
78
+ \u2705 Release preparation complete.`);
79
+ for (const tag of result.tags) {
80
+ lines.push(` \u{1F3F7}\uFE0F ${bold(tag)}`);
81
+ }
82
+ } else {
83
+ lines.push(`
84
+ \u23ED\uFE0F No components had release-worthy changes.`);
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+ function formatBumpFiles(lines, component, dryRun, indent = "") {
89
+ for (const file of component.bumpedFiles) {
90
+ if (dryRun) {
91
+ lines.push(dim(`${indent} [dry-run] Would bump ${file}`));
92
+ } else {
93
+ lines.push(dim(`${indent} Bumped ${file}`));
94
+ }
95
+ }
96
+ }
97
+ function formatChangelogFiles(lines, component, dryRun, indent = "") {
98
+ for (const file of component.changelogFiles) {
99
+ if (dryRun) {
100
+ lines.push(dim(`${indent} [dry-run] Would run: npx --yes git-cliff ... --output ${file}`));
101
+ } else {
102
+ lines.push(dim(`${indent} Generating changelog: ${file}`));
103
+ }
104
+ }
105
+ }
106
+ function formatFormatCommand(lines, result) {
107
+ if (result.formatCommand === void 0) {
108
+ return;
109
+ }
110
+ if (result.formatCommand.executed) {
111
+ lines.push(dim(`
112
+ Running format command: ${result.formatCommand.command}`));
113
+ } else {
114
+ lines.push(dim(`
115
+ [dry-run] Would run format command: ${result.formatCommand.command}`));
116
+ }
117
+ }
118
+ export {
119
+ reportPrepare
120
+ };
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
- import { dirname, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
2
+ import { resolve } from "node:path";
3
+ import { findPackageRoot } from "./findPackageRoot.js";
4
4
  function resolveCliffConfigPath(cliffConfigPath, metaUrl) {
5
5
  if (cliffConfigPath !== void 0) {
6
6
  return cliffConfigPath;
@@ -11,8 +11,8 @@ function resolveCliffConfigPath(cliffConfigPath, metaUrl) {
11
11
  return candidate;
12
12
  }
13
13
  }
14
- const thisDir = dirname(fileURLToPath(metaUrl));
15
- const bundledPath = resolve(thisDir, "..", "..", "cliff.toml.template");
14
+ const root = findPackageRoot(metaUrl);
15
+ const bundledPath = resolve(root, "cliff.toml.template");
16
16
  if (existsSync(bundledPath)) {
17
17
  return bundledPath;
18
18
  }
@@ -0,0 +1,6 @@
1
+ export interface ResolvedTag {
2
+ tag: string;
3
+ dir: string;
4
+ workspacePath: string;
5
+ }
6
+ export declare function resolveReleaseTags(workspaceMap?: Map<string, string>): ResolvedTag[];
@@ -0,0 +1,42 @@
1
+ import { execFileSync } from "node:child_process";
2
+ const VERSION_PATTERN = /^v\d+\.\d+\.\d+/;
3
+ function resolveReleaseTags(workspaceMap) {
4
+ const output = execFileSync("git", ["tag", "--points-at", "HEAD"], { encoding: "utf8" });
5
+ const tags = output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
6
+ if (workspaceMap === void 0) {
7
+ return resolveSinglePackageTags(tags);
8
+ }
9
+ return resolveMonorepoTags(tags, workspaceMap);
10
+ }
11
+ function resolveSinglePackageTags(tags) {
12
+ const matched = tags.filter((tag) => VERSION_PATTERN.test(tag));
13
+ if (matched.length > 1) {
14
+ console.warn(
15
+ `Warning: Multiple version tags found on HEAD: ${matched.join(", ")}. Publishing the same package multiple times is almost certainly unintended. Using only the first tag.`
16
+ );
17
+ return matched.slice(0, 1).map((tag) => ({ tag, dir: ".", workspacePath: "." }));
18
+ }
19
+ return matched.map((tag) => ({ tag, dir: ".", workspacePath: "." }));
20
+ }
21
+ function resolveMonorepoTags(tags, workspaceMap) {
22
+ const resolved = [];
23
+ for (const tag of tags) {
24
+ const dashV = tag.lastIndexOf("-v");
25
+ if (dashV === -1) {
26
+ continue;
27
+ }
28
+ const dir = tag.slice(0, dashV);
29
+ const versionPart = tag.slice(dashV + 1);
30
+ if (!VERSION_PATTERN.test(versionPart)) {
31
+ continue;
32
+ }
33
+ const workspacePath = workspaceMap.get(dir);
34
+ if (workspacePath !== void 0) {
35
+ resolved.push({ tag, dir, workspacePath });
36
+ }
37
+ }
38
+ return resolved;
39
+ }
40
+ export {
41
+ resolveReleaseTags
42
+ };
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+ import { dim } from "./format.js";
3
4
  import { releasePrepare } from "./releasePrepare.js";
4
5
  import { releasePrepareMono } from "./releasePrepareMono.js";
5
6
  const RELEASE_TAGS_FILE = "tmp/.release-tags";
@@ -109,12 +110,12 @@ function writeReleaseTags(tags, dryRun) {
109
110
  return;
110
111
  }
111
112
  if (dryRun) {
112
- console.info(` [dry-run] Would write ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`);
113
+ console.info(dim(` [dry-run] Would write ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`));
113
114
  return;
114
115
  }
115
116
  mkdirSync(dirname(RELEASE_TAGS_FILE), { recursive: true });
116
117
  writeFileSync(RELEASE_TAGS_FILE, tags.join("\n"), "utf8");
117
- console.info(` Wrote ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`);
118
+ console.info(dim(` Wrote ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`));
118
119
  }
119
120
  export {
120
121
  RELEASE_TAGS_FILE,
@@ -0,0 +1,4 @@
1
+ import type { LabelDefinition } from './types.ts';
2
+ export declare const LABELS_OUTPUT_PATH = ".github/labels.yaml";
3
+ export declare function formatLabelsYaml(labels: LabelDefinition[]): string;
4
+ export declare function generateCommand(): Promise<number>;
@@ -0,0 +1,51 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { dump } from "js-yaml";
4
+ import { loadSyncLabelsConfig, SYNC_LABELS_CONFIG_PATH } from "./loadSyncLabelsConfig.js";
5
+ import { resolveLabels } from "./resolveLabels.js";
6
+ const LABELS_OUTPUT_PATH = ".github/labels.yaml";
7
+ const GENERATED_HEADER = `# Generated by release-kit sync-labels \u2014 do not edit.
8
+ # Source: ${SYNC_LABELS_CONFIG_PATH}
9
+ `;
10
+ function formatLabelsYaml(labels) {
11
+ const yamlBody = dump(labels, { quotingType: '"', forceQuotes: false, lineWidth: -1 });
12
+ return GENERATED_HEADER + yamlBody;
13
+ }
14
+ async function generateCommand() {
15
+ let config;
16
+ try {
17
+ config = await loadSyncLabelsConfig();
18
+ } catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ console.error(`Error loading config: ${message}`);
21
+ return 1;
22
+ }
23
+ if (config === void 0) {
24
+ console.error(`No config file found at ${SYNC_LABELS_CONFIG_PATH}. Run \`release-kit sync-labels init\` first.`);
25
+ return 1;
26
+ }
27
+ let labels;
28
+ try {
29
+ labels = resolveLabels(config);
30
+ } catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ console.error(`Error resolving labels: ${message}`);
33
+ return 1;
34
+ }
35
+ const content = formatLabelsYaml(labels);
36
+ try {
37
+ mkdirSync(dirname(LABELS_OUTPUT_PATH), { recursive: true });
38
+ writeFileSync(LABELS_OUTPUT_PATH, content, "utf8");
39
+ } catch (error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ console.error(`Error writing ${LABELS_OUTPUT_PATH}: ${message}`);
42
+ return 1;
43
+ }
44
+ console.info(`Generated ${LABELS_OUTPUT_PATH} with ${String(labels.length)} labels.`);
45
+ return 0;
46
+ }
47
+ export {
48
+ LABELS_OUTPUT_PATH,
49
+ formatLabelsYaml,
50
+ generateCommand
51
+ };
@@ -0,0 +1,6 @@
1
+ interface InitOptions {
2
+ dryRun: boolean;
3
+ force: boolean;
4
+ }
5
+ export declare function syncLabelsInitCommand({ dryRun, force }: InitOptions): Promise<number>;
6
+ export {};
@@ -0,0 +1,62 @@
1
+ import { reportWriteResult, writeFileWithCheck } from "@williamthorsen/node-monorepo-core";
2
+ import { discoverWorkspaces } from "../discoverWorkspaces.js";
3
+ import { generateCommand, LABELS_OUTPUT_PATH } from "./generateCommand.js";
4
+ import { SYNC_LABELS_CONFIG_PATH } from "./loadSyncLabelsConfig.js";
5
+ import { buildScopeLabels, syncLabelsConfigScript, syncLabelsWorkflow } from "./templates.js";
6
+ const WORKFLOW_PATH = ".github/workflows/sync-labels.yaml";
7
+ async function syncLabelsInitCommand({ dryRun, force }) {
8
+ if (dryRun) {
9
+ console.info("[dry-run mode]");
10
+ }
11
+ console.info("\n> Discovering workspaces");
12
+ let workspacePaths;
13
+ try {
14
+ workspacePaths = await discoverWorkspaces();
15
+ } catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ console.error(` Failed to discover workspaces: ${message}`);
18
+ return 1;
19
+ }
20
+ const scopeLabels = workspacePaths === void 0 ? [] : buildScopeLabels(workspacePaths);
21
+ if (workspacePaths === void 0) {
22
+ console.info(" No pnpm workspaces found (single-package repo)");
23
+ } else {
24
+ console.info(` Found ${String(workspacePaths.length)} workspaces`);
25
+ }
26
+ console.info("\n> Scaffolding files");
27
+ const workflowResult = writeFileWithCheck(WORKFLOW_PATH, syncLabelsWorkflow(), { dryRun, overwrite: force });
28
+ const configResult = writeFileWithCheck(SYNC_LABELS_CONFIG_PATH, syncLabelsConfigScript(scopeLabels), {
29
+ dryRun,
30
+ overwrite: force
31
+ });
32
+ reportWriteResult(workflowResult, dryRun);
33
+ reportWriteResult(configResult, dryRun);
34
+ if (workflowResult.outcome === "failed" || configResult.outcome === "failed") {
35
+ console.error("Failed to scaffold one or more files.");
36
+ return 1;
37
+ }
38
+ if (dryRun) {
39
+ console.info(`
40
+ > [dry-run] Would generate ${LABELS_OUTPUT_PATH}`);
41
+ } else {
42
+ console.info("\n> Generating labels");
43
+ const generateExitCode = await generateCommand();
44
+ if (generateExitCode !== 0) {
45
+ return generateExitCode;
46
+ }
47
+ }
48
+ console.info(`
49
+ > Next steps
50
+ 1. Review the generated files:
51
+ - ${WORKFLOW_PATH}
52
+ - ${SYNC_LABELS_CONFIG_PATH}
53
+ - ${LABELS_OUTPUT_PATH}
54
+ 2. Customize ${SYNC_LABELS_CONFIG_PATH} as needed, then re-run \`release-kit sync-labels generate\`.
55
+ 3. Commit the generated files.
56
+ 4. Run \`release-kit sync-labels sync\` to apply labels to your GitHub repo.
57
+ `);
58
+ return 0;
59
+ }
60
+ export {
61
+ syncLabelsInitCommand
62
+ };
@@ -0,0 +1,3 @@
1
+ import type { SyncLabelsConfig } from './types.ts';
2
+ export declare const SYNC_LABELS_CONFIG_PATH = ".config/sync-labels.config.ts";
3
+ export declare function loadSyncLabelsConfig(): Promise<SyncLabelsConfig | undefined>;
@@ -0,0 +1,59 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isRecord } from "../typeGuards.js";
4
+ const SYNC_LABELS_CONFIG_PATH = ".config/sync-labels.config.ts";
5
+ async function loadSyncLabelsConfig() {
6
+ const absoluteConfigPath = path.resolve(process.cwd(), SYNC_LABELS_CONFIG_PATH);
7
+ if (!existsSync(absoluteConfigPath)) {
8
+ return void 0;
9
+ }
10
+ const { createJiti } = await import("jiti");
11
+ const jiti = createJiti(import.meta.url);
12
+ const imported = await jiti.import(absoluteConfigPath);
13
+ if (!isRecord(imported)) {
14
+ throw new Error(`Config file must export an object, got ${Array.isArray(imported) ? "array" : typeof imported}`);
15
+ }
16
+ const resolved = imported.default ?? imported.config;
17
+ if (resolved === void 0) {
18
+ throw new Error(
19
+ "Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)"
20
+ );
21
+ }
22
+ return validateSyncLabelsConfig(resolved);
23
+ }
24
+ function validateSyncLabelsConfig(value) {
25
+ if (!isRecord(value)) {
26
+ throw new Error(`Config must be an object, got ${typeof value}`);
27
+ }
28
+ let presets;
29
+ if (value.presets !== void 0) {
30
+ if (!Array.isArray(value.presets) || !value.presets.every((p) => typeof p === "string")) {
31
+ throw new Error("Config `presets` must be an array of strings");
32
+ }
33
+ presets = value.presets;
34
+ }
35
+ let labels;
36
+ if (value.labels !== void 0) {
37
+ if (!Array.isArray(value.labels)) {
38
+ throw new TypeError("Config `labels` must be an array");
39
+ }
40
+ labels = [];
41
+ for (const label of value.labels) {
42
+ if (!isRecord(label)) {
43
+ throw new Error("Each label must be an object with `name`, `color`, and `description`");
44
+ }
45
+ if (typeof label.name !== "string" || typeof label.color !== "string" || typeof label.description !== "string") {
46
+ throw new TypeError("Each label must have string `name`, `color`, and `description` fields");
47
+ }
48
+ labels.push({ name: label.name, color: label.color, description: label.description });
49
+ }
50
+ }
51
+ return {
52
+ ...presets !== void 0 && { presets },
53
+ ...labels !== void 0 && { labels }
54
+ };
55
+ }
56
+ export {
57
+ SYNC_LABELS_CONFIG_PATH,
58
+ loadSyncLabelsConfig
59
+ };
@@ -0,0 +1,2 @@
1
+ import type { LabelDefinition } from './types.ts';
2
+ export declare function loadPreset(presetName: string): LabelDefinition[];
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { load } from "js-yaml";
4
+ import { findPackageRoot } from "../findPackageRoot.js";
5
+ import { isRecord } from "../typeGuards.js";
6
+ function resolvePresetPath(presetName) {
7
+ const root = findPackageRoot(import.meta.url);
8
+ return resolve(root, "presets", "labels", `${presetName}.yaml`);
9
+ }
10
+ function loadPreset(presetName) {
11
+ const presetPath = resolvePresetPath(presetName);
12
+ if (!existsSync(presetPath)) {
13
+ throw new Error(`Unknown preset "${presetName}". No file found at ${presetPath}`);
14
+ }
15
+ let content;
16
+ try {
17
+ content = readFileSync(presetPath, "utf8");
18
+ } catch (error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ throw new Error(`Failed to read preset "${presetName}": ${message}`);
21
+ }
22
+ const parsed = load(content);
23
+ if (!Array.isArray(parsed)) {
24
+ throw new TypeError(`Preset "${presetName}" must be a YAML array of label definitions`);
25
+ }
26
+ const labels = [];
27
+ for (const entry of parsed) {
28
+ if (!isRecord(entry)) {
29
+ throw new Error(`Preset "${presetName}" contains an invalid label entry: ${JSON.stringify(entry)}`);
30
+ }
31
+ if (typeof entry.name !== "string" || typeof entry.color !== "string" || typeof entry.description !== "string") {
32
+ throw new TypeError(`Preset "${presetName}" contains a label with invalid fields: ${JSON.stringify(entry)}`);
33
+ }
34
+ labels.push({ name: entry.name, color: entry.color, description: entry.description });
35
+ }
36
+ return labels;
37
+ }
38
+ export {
39
+ loadPreset
40
+ };
@@ -0,0 +1,2 @@
1
+ import type { LabelDefinition, SyncLabelsConfig } from './types.ts';
2
+ export declare function resolveLabels(config: SyncLabelsConfig): LabelDefinition[];
@@ -0,0 +1,43 @@
1
+ import { loadPreset } from "./presets.js";
2
+ function resolveLabels(config) {
3
+ const presetLabels = [];
4
+ for (const presetName of config.presets ?? []) {
5
+ const loaded = loadPreset(presetName);
6
+ presetLabels.push(...loaded);
7
+ }
8
+ const customLabels = config.labels ?? [];
9
+ detectCollisions(presetLabels, customLabels);
10
+ return sortLabels([...presetLabels, ...customLabels]);
11
+ }
12
+ function detectCollisions(presetLabels, customLabels) {
13
+ const seenPresetNames = /* @__PURE__ */ new Set();
14
+ const withinPresetDuplicates = [];
15
+ for (const label of presetLabels) {
16
+ if (seenPresetNames.has(label.name)) {
17
+ withinPresetDuplicates.push(label.name);
18
+ }
19
+ seenPresetNames.add(label.name);
20
+ }
21
+ if (withinPresetDuplicates.length > 0) {
22
+ throw new Error(
23
+ `Label name collision within presets: the following labels are defined by multiple presets: ${withinPresetDuplicates.join(", ")}. Remove the duplicates or use different presets.`
24
+ );
25
+ }
26
+ const conflicts = [];
27
+ for (const label of customLabels) {
28
+ if (seenPresetNames.has(label.name)) {
29
+ conflicts.push(label.name);
30
+ }
31
+ }
32
+ if (conflicts.length > 0) {
33
+ throw new Error(
34
+ `Label name collision: the following labels appear in both presets and custom labels: ${conflicts.join(", ")}. Remove duplicates from your custom labels or use a different name.`
35
+ );
36
+ }
37
+ }
38
+ function sortLabels(labels) {
39
+ return [...labels].sort((a, b) => a.name.localeCompare(b.name));
40
+ }
41
+ export {
42
+ resolveLabels
43
+ };
@@ -0,0 +1,6 @@
1
+ interface WriteResult {
2
+ action: 'created' | 'skipped' | 'dry-run' | 'failed';
3
+ filePath: string;
4
+ }
5
+ export declare function writeIfAbsent(filePath: string, content: string, dryRun: boolean, overwrite: boolean): WriteResult;
6
+ export {};
@@ -0,0 +1,25 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ function writeIfAbsent(filePath, content, dryRun, overwrite) {
4
+ if (existsSync(filePath) && !overwrite) {
5
+ console.info(` Skipped ${filePath} (already exists)`);
6
+ return { action: "skipped", filePath };
7
+ }
8
+ if (dryRun) {
9
+ console.info(` [dry-run] Would create ${filePath}`);
10
+ return { action: "dry-run", filePath };
11
+ }
12
+ try {
13
+ mkdirSync(dirname(filePath), { recursive: true });
14
+ writeFileSync(filePath, content, "utf8");
15
+ } catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ console.error(` Failed to write ${filePath}: ${message}`);
18
+ return { action: "failed", filePath };
19
+ }
20
+ console.info(` Created ${filePath}`);
21
+ return { action: "created", filePath };
22
+ }
23
+ export {
24
+ writeIfAbsent
25
+ };
@@ -0,0 +1 @@
1
+ export declare function syncLabelsCommand(): number;