@varlock/bumpy 0.0.2 → 1.1.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 (43) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/config-schema.json +327 -0
  3. package/dist/add-BmNL5VwL.mjs +323 -0
  4. package/dist/{ai-CQhUyHAG.mjs → ai-sMYUf3lP.mjs} +21 -4
  5. package/dist/{apply-release-plan-D6TSrcwX.mjs → apply-release-plan-0kH62jhu.mjs} +35 -26
  6. package/dist/bump-file-DVqR3k67.mjs +157 -0
  7. package/dist/{changelog-github-Du62krXi.mjs → changelog-github-DkACMj0j.mjs} +23 -21
  8. package/dist/check-BjWF6SJm.mjs +65 -0
  9. package/dist/{ci-D6LQbR38.mjs → ci-DY58ugIi.mjs} +138 -91
  10. package/dist/{ci-setup-C6FlOfW5.mjs → ci-setup-BQwktQEe.mjs} +3 -3
  11. package/dist/cli.mjs +36 -41
  12. package/dist/commit-message-BwsowSds.mjs +23 -0
  13. package/dist/{config-BkwIEaQg.mjs → config-B-Qg3DZH.mjs} +30 -24
  14. package/dist/fs-DYR2XuFE.mjs +81 -0
  15. package/dist/generate-DX46X-rW.mjs +186 -0
  16. package/dist/{git-CGHVXXKw.mjs → git-YDedMddc.mjs} +54 -2
  17. package/dist/index.d.mts +68 -39
  18. package/dist/index.mjs +9 -9
  19. package/dist/init-DkTPs_WQ.mjs +196 -0
  20. package/dist/{names-Ck8cun7B.mjs → names-C-TuOPbd.mjs} +1 -1
  21. package/dist/{js-yaml-DpZfOoD4.mjs → package-manager-Clsmr-9r.mjs} +79 -1
  22. package/dist/picomatch-DMmqYjgq.mjs +1870 -0
  23. package/dist/{publish-D_7RqEYL.mjs → publish-CGB4TIKD.mjs} +26 -25
  24. package/dist/{publish-pipeline-ChnqW8nR.mjs → publish-pipeline-CXuqce1N.mjs} +24 -19
  25. package/dist/release-plan-JNir7bSM.mjs +264 -0
  26. package/dist/{semver-BTzYh8vc.mjs → semver-BJzWIuRz.mjs} +13 -3
  27. package/dist/{shell-Dj7JRD_q.mjs → shell-CY7OD48z.mjs} +20 -2
  28. package/dist/{status--Q8yAxQ4.mjs → status-EGYqULJg.mjs} +26 -22
  29. package/dist/{version-cAUkfYPx.mjs → version-BcfidiVX.mjs} +23 -22
  30. package/dist/{workspace-CxEKakDm.mjs → workspace-DWXlwcH4.mjs} +3 -3
  31. package/package.json +16 -1
  32. package/skills/add-change/SKILL.md +18 -14
  33. package/dist/add-BjyVIUlr.mjs +0 -175
  34. package/dist/changeset-UCZdSRDv.mjs +0 -108
  35. package/dist/check-jIwike9F.mjs +0 -51
  36. package/dist/fs-0AtnPUUe.mjs +0 -51
  37. package/dist/generate-Btrsn1qi.mjs +0 -177
  38. package/dist/init-B0q3wEQW.mjs +0 -22
  39. package/dist/migrate-CfQNwD0T.mjs +0 -121
  40. package/dist/package-manager-DcI5TdDE.mjs +0 -80
  41. package/dist/release-plan-BEzwApuK.mjs +0 -173
  42. /package/dist/{clack-CDRCHrC-.mjs → clack-C6bVkGxf.mjs} +0 -0
  43. /package/dist/{dep-graph-E-9-eQ2J.mjs → dep-graph-DiLeAhl9.mjs} +0 -0
@@ -1,10 +1,12 @@
1
1
  import { n as log } from "./logger-C2dEe5Su.mjs";
2
- import { l as writeText, n as exists, t as ensureDir } from "./fs-0AtnPUUe.mjs";
2
+ import { d as writeText, n as exists, t as ensureDir } from "./fs-DYR2XuFE.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { readFile } from "node:fs/promises";
5
+ import { execSync } from "node:child_process";
5
6
  import { fileURLToPath } from "node:url";
6
7
  //#region src/commands/ai.ts
7
8
  const SUPPORTED_TARGETS = [
9
+ "claude",
8
10
  "opencode",
9
11
  "cursor",
10
12
  "codex"
@@ -13,13 +15,16 @@ async function aiSetupCommand(rootDir, opts) {
13
15
  const target = opts.target;
14
16
  if (!target) {
15
17
  log.error(`Please specify a target: bumpy ai setup --target <${SUPPORTED_TARGETS.join("|")}>`);
16
- log.dim(" Claude Code users: install the plugin instead — claude plugin install @varlock/bumpy");
17
18
  process.exit(1);
18
19
  }
19
20
  if (!SUPPORTED_TARGETS.includes(target)) {
20
21
  log.error(`Unknown target: "${target}". Supported: ${SUPPORTED_TARGETS.join(", ")}`);
21
22
  process.exit(1);
22
23
  }
24
+ if (target === "claude") {
25
+ setupClaude();
26
+ return;
27
+ }
23
28
  const promptContent = await loadPromptTemplate();
24
29
  switch (target) {
25
30
  case "opencode":
@@ -36,6 +41,18 @@ async function aiSetupCommand(rootDir, opts) {
36
41
  async function loadPromptTemplate() {
37
42
  return (await readFile(resolve(dirname(fileURLToPath(import.meta.url)), "../../skills/add-change/SKILL.md"), "utf-8")).replace(/^---\n[\s\S]*?\n---\n\n?/, "");
38
43
  }
44
+ /** Install as a Claude Code plugin */
45
+ function setupClaude() {
46
+ log.step("Installing Claude Code plugin...");
47
+ try {
48
+ execSync("claude plugin install @varlock/bumpy", { stdio: "inherit" });
49
+ log.success("Installed Claude Code plugin");
50
+ log.dim(" Usage: /bumpy:add-change in Claude Code");
51
+ } catch {
52
+ log.error("Failed to install Claude Code plugin. Make sure `claude` is installed and available in your PATH.");
53
+ process.exit(1);
54
+ }
55
+ }
39
56
  /** Install as an OpenCode custom command */
40
57
  async function setupOpenCode(rootDir, promptContent) {
41
58
  const commandsDir = resolve(rootDir, ".opencode", "commands");
@@ -43,7 +60,7 @@ async function setupOpenCode(rootDir, promptContent) {
43
60
  await ensureDir(commandsDir);
44
61
  if (await exists(targetPath)) log.warn(".opencode/commands/add-bumpy-change.md already exists — overwriting");
45
62
  await writeText(targetPath, `---
46
- description: Create a bumpy changeset to track package version bumps
63
+ description: Create a bumpy bump file to track package version bumps
47
64
  ---
48
65
 
49
66
  ${promptContent}`);
@@ -58,7 +75,7 @@ async function setupCursor(rootDir, promptContent) {
58
75
  await ensureDir(rulesDir);
59
76
  if (await exists(targetPath)) log.warn(".cursor/rules/add-bumpy-change.mdc already exists — overwriting");
60
77
  await writeText(targetPath, `---
61
- description: Create a bumpy changeset to track package version bumps
78
+ description: Create a bumpy bump file to track package version bumps
62
79
  globs:
63
80
  alwaysApply: false
64
81
  ---
@@ -1,33 +1,34 @@
1
1
  import { n as log } from "./logger-C2dEe5Su.mjs";
2
- import { a as readJson, c as writeJson, l as writeText, n as exists, o as readText } from "./fs-0AtnPUUe.mjs";
3
- import { t as deleteChangesets } from "./changeset-UCZdSRDv.mjs";
4
- import { resolve } from "node:path";
2
+ import { a as readJson, c as updateJsonFields, d as writeText, i as listFiles, l as updateJsonNestedField, n as exists, o as readText, s as removeFile } from "./fs-DYR2XuFE.mjs";
3
+ import { r as getBumpyDir } from "./config-B-Qg3DZH.mjs";
4
+ import { relative, resolve } from "node:path";
5
+ import { realpathSync } from "node:fs";
5
6
  //#region src/core/changelog.ts
6
7
  /** Default formatter — version heading, date, bullet points */
7
8
  const defaultFormatter = (ctx) => {
8
- const { release, changesets, date } = ctx;
9
+ const { release, bumpFiles, date } = ctx;
9
10
  const lines = [];
10
11
  lines.push(`## ${release.newVersion}`);
11
12
  lines.push("");
12
13
  lines.push(`_${date}_`);
13
14
  lines.push("");
14
- const relevantChangesets = changesets.filter((cs) => release.changesets.includes(cs.id));
15
- if (relevantChangesets.length > 0) {
16
- for (const cs of relevantChangesets) if (cs.summary) {
17
- const summaryLines = cs.summary.split("\n");
15
+ const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id));
16
+ if (relevantBumpFiles.length > 0) {
17
+ for (const bf of relevantBumpFiles) if (bf.summary) {
18
+ const summaryLines = bf.summary.split("\n");
18
19
  lines.push(`- ${summaryLines[0]}`);
19
20
  for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${summaryLines[i]}`);
20
21
  }
21
22
  }
22
- if (release.isDependencyBump && relevantChangesets.length === 0) lines.push("- Updated dependencies");
23
- if (release.isCascadeBump && !release.isDependencyBump && relevantChangesets.length === 0) lines.push("- Version bump via cascade rule");
23
+ if (release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Updated dependencies");
24
+ if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Version bump via cascade rule");
24
25
  lines.push("");
25
26
  return lines.join("\n");
26
27
  };
27
28
  const BUILTIN_FORMATTERS = {
28
29
  default: defaultFormatter,
29
30
  github: async () => {
30
- const { createGithubFormatter } = await import("./changelog-github-Du62krXi.mjs");
31
+ const { createGithubFormatter } = await import("./changelog-github-DkACMj0j.mjs");
31
32
  return createGithubFormatter();
32
33
  }
33
34
  };
@@ -37,20 +38,24 @@ const BUILTIN_FORMATTERS = {
37
38
  */
38
39
  async function loadFormatter(changelog, rootDir) {
39
40
  const [name, options] = Array.isArray(changelog) ? changelog : [changelog, {}];
41
+ if (name === "github") {
42
+ const { createGithubFormatter } = await import("./changelog-github-DkACMj0j.mjs");
43
+ return createGithubFormatter(options);
44
+ }
40
45
  if (typeof name === "string" && BUILTIN_FORMATTERS[name]) {
41
46
  const builtin = BUILTIN_FORMATTERS[name];
42
47
  if (typeof builtin === "function" && builtin.length === 0) return builtin();
43
48
  return builtin;
44
49
  }
45
- if (name === "github") {
46
- const { createGithubFormatter } = await import("./changelog-github-Du62krXi.mjs");
47
- return createGithubFormatter(options);
48
- }
49
50
  if (typeof name === "string") try {
50
51
  let modulePath;
51
52
  if (name.startsWith(".")) {
52
53
  modulePath = resolve(rootDir, name);
53
- if (!modulePath.startsWith(rootDir + "/")) throw new Error(`Changelog formatter path "${name}" resolves outside the project root`);
54
+ try {
55
+ modulePath = realpathSync(modulePath);
56
+ } catch {}
57
+ const rel = relative(realpathSync(rootDir), modulePath);
58
+ if (rel.startsWith("..") || resolve("/", rel) === resolve("/")) throw new Error(`Changelog formatter path "${name}" resolves outside the project root`);
54
59
  } else modulePath = name;
55
60
  const mod = await import(modulePath);
56
61
  const exported = mod.default || mod.changelogFormatter;
@@ -68,10 +73,10 @@ async function loadFormatter(changelog, rootDir) {
68
73
  return defaultFormatter;
69
74
  }
70
75
  /** Generate a changelog entry using the configured formatter */
71
- async function generateChangelogEntry(release, changesets, formatter = defaultFormatter, date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]) {
76
+ async function generateChangelogEntry(release, bumpFiles, formatter = defaultFormatter, date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]) {
72
77
  return formatter({
73
78
  release,
74
- changesets,
79
+ bumpFiles,
75
80
  date
76
81
  });
77
82
  }
@@ -87,14 +92,14 @@ function prependToChangelog(existingContent, newEntry) {
87
92
  }
88
93
  //#endregion
89
94
  //#region src/core/apply-release-plan.ts
90
- /** Apply the release plan: bump versions, update changelogs, delete changesets */
95
+ /** Apply the release plan: bump versions, update changelogs, delete bump files */
91
96
  async function applyReleasePlan(releasePlan, packages, rootDir, config) {
92
97
  const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
93
- const formatter = await loadFormatter(config.changelog, rootDir);
98
+ const formatter = config.changelog !== false ? await loadFormatter(config.changelog, rootDir) : null;
94
99
  for (const release of releasePlan.releases) {
95
100
  const pkgJsonPath = resolve(packages.get(release.name).dir, "package.json");
96
101
  const pkgJson = await readJson(pkgJsonPath);
97
- pkgJson.version = release.newVersion;
102
+ await updateJsonFields(pkgJsonPath, { version: release.newVersion });
98
103
  for (const depField of [
99
104
  "dependencies",
100
105
  "devDependencies",
@@ -106,19 +111,23 @@ async function applyReleasePlan(releasePlan, packages, rootDir, config) {
106
111
  for (const [depName, range] of Object.entries(deps)) {
107
112
  const depRelease = releaseMap.get(depName);
108
113
  if (!depRelease) continue;
109
- deps[depName] = updateRange(range, depRelease.newVersion);
114
+ await updateJsonNestedField(pkgJsonPath, depField, depName, updateRange(range, depRelease.newVersion));
110
115
  }
111
116
  }
112
- await writeJson(pkgJsonPath, pkgJson);
113
117
  }
114
- for (const release of releasePlan.releases) {
118
+ if (formatter) for (const release of releasePlan.releases) {
115
119
  const changelogPath = resolve(packages.get(release.name).dir, "CHANGELOG.md");
116
- const entry = await generateChangelogEntry(release, releasePlan.changesets, formatter);
120
+ const entry = await generateChangelogEntry(release, releasePlan.bumpFiles, formatter);
117
121
  let existingContent = "";
118
122
  if (await exists(changelogPath)) existingContent = await readText(changelogPath);
119
123
  await writeText(changelogPath, prependToChangelog(existingContent, entry));
120
124
  }
121
- await deleteChangesets(rootDir, releasePlan.changesets.map((cs) => cs.id));
125
+ const bumpyDir = getBumpyDir(rootDir);
126
+ const allBumpFiles = await listFiles(bumpyDir, ".md");
127
+ for (const file of allBumpFiles) {
128
+ if (file === "README.md") continue;
129
+ await removeFile(resolve(bumpyDir, file));
130
+ }
122
131
  }
123
132
  /** Update a version range to include a new version, preserving the range prefix */
124
133
  function updateRange(range, newVersion) {
@@ -0,0 +1,157 @@
1
+ import { n as log } from "./logger-C2dEe5Su.mjs";
2
+ import { d as writeText, i as listFiles, o as readText } from "./fs-DYR2XuFE.mjs";
3
+ import { r as getBumpyDir } from "./config-B-Qg3DZH.mjs";
4
+ import { i as jsYaml } from "./package-manager-Clsmr-9r.mjs";
5
+ import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
6
+ import { resolve } from "node:path";
7
+ //#region src/core/bump-file.ts
8
+ const VALID_BUMP_TYPES = new Set([
9
+ "major",
10
+ "minor",
11
+ "patch",
12
+ "none"
13
+ ]);
14
+ /**
15
+ * Reject package names that contain characters which could cause injection
16
+ * when used in git tags, markdown, URLs, or shell-quoted strings.
17
+ * Intentionally permissive — we don't enforce npm naming rules because
18
+ * bumpy may be used with other registries or non-JS packages.
19
+ */
20
+ function validatePackageName(name) {
21
+ if (!name || name.length > 214) return false;
22
+ if (/[\u0000-\u001f\u007f]/.test(name)) return false;
23
+ if (/[<>"'`&;|$(){}[\]\\!#%\s]/.test(name)) return false;
24
+ if (name.startsWith("-")) return false;
25
+ return true;
26
+ }
27
+ /** Read all bump files from .bumpy/ directory, sorted by git creation order */
28
+ async function readBumpFiles(rootDir) {
29
+ const dir = getBumpyDir(rootDir);
30
+ const files = await listFiles(dir, ".md");
31
+ const bumpFiles = [];
32
+ for (const file of files) {
33
+ if (file === "README.md") continue;
34
+ const bf = await parseBumpFileFromPath(resolve(dir, file));
35
+ if (bf) bumpFiles.push(bf);
36
+ }
37
+ const creationOrder = getBumpFileCreationOrder(rootDir);
38
+ if (creationOrder.size > 0) bumpFiles.sort((a, b) => {
39
+ return (creationOrder.get(a.id) ?? Infinity) - (creationOrder.get(b.id) ?? Infinity) || a.id.localeCompare(b.id);
40
+ });
41
+ return bumpFiles;
42
+ }
43
+ /**
44
+ * Use `git log` to get the commit timestamp when each bump file was first added.
45
+ * Returns a map of bump file ID → unix timestamp (seconds).
46
+ */
47
+ function getBumpFileCreationOrder(rootDir) {
48
+ const order = /* @__PURE__ */ new Map();
49
+ const result = tryRunArgs([
50
+ "git",
51
+ "log",
52
+ "--diff-filter=A",
53
+ "--format=%at",
54
+ "--name-only",
55
+ "--",
56
+ ".bumpy/*.md"
57
+ ], { cwd: rootDir });
58
+ if (!result) return order;
59
+ let currentTimestamp = 0;
60
+ for (const line of result.split("\n")) {
61
+ const trimmed = line.trim();
62
+ if (!trimmed) continue;
63
+ if (/^\d+$/.test(trimmed)) currentTimestamp = parseInt(trimmed, 10);
64
+ else if (trimmed.startsWith(".bumpy/") && trimmed.endsWith(".md")) {
65
+ const id = trimmed.replace(/^\.bumpy\//, "").replace(/\.md$/, "");
66
+ order.set(id, currentTimestamp);
67
+ }
68
+ }
69
+ return order;
70
+ }
71
+ /** Parse a single bump file from disk */
72
+ async function parseBumpFileFromPath(filePath) {
73
+ return parseBumpFile(await readText(filePath), fileToId(filePath));
74
+ }
75
+ /** Parse bump file content (for testing) */
76
+ function parseBumpFile(content, id) {
77
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
78
+ if (!match) return null;
79
+ const frontmatter = match[1];
80
+ const summary = match[2].trim();
81
+ const parsed = jsYaml.load(frontmatter);
82
+ if (!parsed || typeof parsed !== "object") return null;
83
+ const releases = [];
84
+ for (const [name, value] of Object.entries(parsed)) {
85
+ if (!validatePackageName(name)) {
86
+ log.warn(`Skipping invalid package name in bump file "${id}": ${name}`);
87
+ continue;
88
+ }
89
+ if (typeof value === "string") {
90
+ if (!VALID_BUMP_TYPES.has(value)) {
91
+ log.warn(`Skipping unknown bump type "${value}" for ${name} in bump file "${id}"`);
92
+ continue;
93
+ }
94
+ releases.push({
95
+ name,
96
+ type: value
97
+ });
98
+ } else if (value && typeof value === "object") {
99
+ const obj = value;
100
+ if (!VALID_BUMP_TYPES.has(obj.bump)) {
101
+ log.warn(`Skipping unknown bump type "${obj.bump}" for ${name} in bump file "${id}"`);
102
+ continue;
103
+ }
104
+ const release = {
105
+ name,
106
+ type: obj.bump,
107
+ cascade: obj.cascade || {}
108
+ };
109
+ releases.push(release);
110
+ }
111
+ }
112
+ if (releases.length === 0) return null;
113
+ return {
114
+ id,
115
+ releases,
116
+ summary
117
+ };
118
+ }
119
+ /** Write a bump file */
120
+ async function writeBumpFile(rootDir, filename, releases, summary) {
121
+ const filePath = resolve(getBumpyDir(rootDir), `${filename}.md`);
122
+ const frontmatter = {};
123
+ for (const release of releases) if ("cascade" in release && Object.keys(release.cascade).length > 0) frontmatter[release.name] = {
124
+ bump: release.type,
125
+ cascade: release.cascade
126
+ };
127
+ else frontmatter[release.name] = release.type;
128
+ await writeText(filePath, `---\n${jsYaml.dump(frontmatter, {
129
+ lineWidth: -1,
130
+ quotingType: "\""
131
+ }).trim()}\n---\n\n${summary}\n`);
132
+ return filePath;
133
+ }
134
+ function fileToId(filePath) {
135
+ return filePath.split("/").pop().replace(/\.md$/, "");
136
+ }
137
+ /**
138
+ * Given a list of changed file paths (relative to root), extract the IDs
139
+ * of bump files that were added/modified. Shared by `check` and `ci check`.
140
+ */
141
+ function extractBumpFileIdsFromChangedFiles(changedFiles) {
142
+ return new Set(changedFiles.filter((f) => /^\.bumpy\/.*\.md$/.test(f) && !f.endsWith("README.md")).map((f) => f.replace(/^\.bumpy\//, "").replace(/\.md$/, "")));
143
+ }
144
+ /**
145
+ * Filter bump files to only those added/modified on the current branch.
146
+ * Returns the filtered bump files and whether any changed bump file was
147
+ * empty (has no releases — signals intentionally no releases needed).
148
+ */
149
+ function filterBranchBumpFiles(allBumpFiles, changedFiles) {
150
+ const branchBumpFileIds = extractBumpFileIdsFromChangedFiles(changedFiles);
151
+ return {
152
+ branchBumpFiles: allBumpFiles.filter((bf) => branchBumpFileIds.has(bf.id)),
153
+ branchBumpFileIds
154
+ };
155
+ }
156
+ //#endregion
157
+ export { writeBumpFile as i, parseBumpFile as n, readBumpFiles as r, filterBranchBumpFiles as t };
@@ -1,4 +1,4 @@
1
- import { o as tryRunArgs } from "./shell-Dj7JRD_q.mjs";
1
+ import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
2
2
  //#region src/core/changelog-github.ts
3
3
  /**
4
4
  * GitHub-enhanced changelog formatter.
@@ -7,12 +7,14 @@ import { o as tryRunArgs } from "./shell-Dj7JRD_q.mjs";
7
7
  * Usage in config:
8
8
  * "changelog": "github"
9
9
  * "changelog": ["github", { "repo": "dmno-dev/bumpy" }]
10
- * "changelog": ["github", { "repo": "dmno-dev/bumpy", "internalAuthors": ["theoephraim"] }]
10
+ * "changelog": ["github", { "thankContributors": false }]
11
+ * "changelog": ["github", { "internalAuthors": ["theoephraim"] }]
11
12
  */
12
13
  function createGithubFormatter(options = {}) {
14
+ const thankContributors = options.thankContributors ?? true;
13
15
  const internalAuthorsSet = new Set((options.internalAuthors ?? []).map((a) => a.toLowerCase()));
14
16
  return async (ctx) => {
15
- const { release, changesets, date } = ctx;
17
+ const { release, bumpFiles, date } = ctx;
16
18
  const repoSlug = options.repo ?? detectRepo();
17
19
  const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
18
20
  const lines = [];
@@ -20,25 +22,25 @@ function createGithubFormatter(options = {}) {
20
22
  lines.push("");
21
23
  lines.push(`_${date}_`);
22
24
  lines.push("");
23
- const relevantChangesets = changesets.filter((cs) => release.changesets.includes(cs.id));
24
- if (relevantChangesets.length > 0) for (const cs of relevantChangesets) {
25
- if (!cs.summary) continue;
26
- const { cleanSummary, overrides } = extractSummaryMeta(cs.summary);
27
- const gitInfo = resolveChangesetInfo(cs.id, repoSlug, serverUrl, overrides);
25
+ const relevantBumpFiles = bumpFiles.filter((bf) => release.bumpFiles.includes(bf.id));
26
+ if (relevantBumpFiles.length > 0) for (const bf of relevantBumpFiles) {
27
+ if (!bf.summary) continue;
28
+ const { cleanSummary, overrides } = extractSummaryMeta(bf.summary);
29
+ const gitInfo = resolveBumpFileInfo(bf.id, repoSlug, serverUrl, overrides);
28
30
  const summaryLines = cleanSummary.split("\n");
29
31
  const firstLine = linkifyIssueRefs(summaryLines[0], serverUrl, repoSlug);
30
- const prefix = formatPrefix(gitInfo, serverUrl, repoSlug, internalAuthorsSet);
32
+ const prefix = formatPrefix(gitInfo, serverUrl, repoSlug, thankContributors, internalAuthorsSet);
31
33
  lines.push(`-${prefix ? ` ${prefix} -` : ""} ${firstLine}`);
32
34
  for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${linkifyIssueRefs(summaryLines[i], serverUrl, repoSlug)}`);
33
35
  }
34
- if (release.isDependencyBump && relevantChangesets.length === 0) lines.push("- Updated dependencies");
35
- if (release.isCascadeBump && !release.isDependencyBump && relevantChangesets.length === 0) lines.push("- Version bump via cascade rule");
36
+ if (release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Updated dependencies");
37
+ if (release.isCascadeBump && !release.isDependencyBump && relevantBumpFiles.length === 0) lines.push("- Version bump via cascade rule");
36
38
  lines.push("");
37
39
  return lines.join("\n");
38
40
  };
39
41
  }
40
42
  /**
41
- * Extract metadata lines (pr, commit, author) from a changeset summary.
43
+ * Extract metadata lines (pr, commit, author) from a bump file summary.
42
44
  * These override git-derived info, matching the behavior of @changesets/changelog-github.
43
45
  */
44
46
  function extractSummaryMeta(summary) {
@@ -60,10 +62,10 @@ function extractSummaryMeta(summary) {
60
62
  };
61
63
  }
62
64
  /**
63
- * Resolve PR, commit, and author info for a changeset.
65
+ * Resolve PR, commit, and author info for a bump file.
64
66
  * Summary overrides take precedence over git-derived info.
65
67
  */
66
- function resolveChangesetInfo(changesetId, repo, serverUrl, overrides) {
68
+ function resolveBumpFileInfo(bumpFileId, repo, serverUrl, overrides) {
67
69
  if (overrides.pr !== void 0) {
68
70
  const prInfo = lookupPr(overrides.pr, repo);
69
71
  return {
@@ -73,7 +75,7 @@ function resolveChangesetInfo(changesetId, repo, serverUrl, overrides) {
73
75
  author: overrides.authors?.[0] ?? prInfo?.author
74
76
  };
75
77
  }
76
- const gitInfo = findChangesetCommitInfo(changesetId, repo);
78
+ const gitInfo = findBumpFileCommitInfo(bumpFileId, repo);
77
79
  return {
78
80
  prNumber: gitInfo?.prNumber,
79
81
  prUrl: gitInfo?.prUrl,
@@ -106,10 +108,10 @@ function lookupPr(prNumber, repo) {
106
108
  }
107
109
  }
108
110
  /**
109
- * Find the PR that introduced a changeset file by checking git log
111
+ * Find the PR that introduced a bump file by checking git log
110
112
  * for the commit that added the file, then looking up the PR.
111
113
  */
112
- function findChangesetCommitInfo(changesetId, repo) {
114
+ function findBumpFileCommitInfo(bumpFileId, repo) {
113
115
  try {
114
116
  const commitOutput = tryRunArgs([
115
117
  "git",
@@ -117,8 +119,8 @@ function findChangesetCommitInfo(changesetId, repo) {
117
119
  "--diff-filter=A",
118
120
  "--format=%H",
119
121
  "--",
120
- `.bumpy/${changesetId}.md`,
121
- `.changeset/${changesetId}.md`
122
+ `.bumpy/${bumpFileId}.md`,
123
+ `.changeset/${bumpFileId}.md`
122
124
  ]);
123
125
  if (!commitOutput) return null;
124
126
  const commitHash = commitOutput.split("\n")[0].trim();
@@ -155,14 +157,14 @@ function findChangesetCommitInfo(changesetId, repo) {
155
157
  * Build the prefix portion of a changelog line: PR link, commit link, thanks.
156
158
  * Matches the format used by @changesets/changelog-github.
157
159
  */
158
- function formatPrefix(info, serverUrl, repo, internalAuthors) {
160
+ function formatPrefix(info, serverUrl, repo, thankContributors, internalAuthors) {
159
161
  const parts = [];
160
162
  if (info.prNumber && info.prUrl) parts.push(`[#${info.prNumber}](${info.prUrl})`);
161
163
  if (info.commitHash && repo) {
162
164
  const short = info.commitHash.slice(0, 7);
163
165
  parts.push(`[\`${short}\`](${serverUrl}/${repo}/commit/${info.commitHash})`);
164
166
  }
165
- if (info.author && !internalAuthors.has(info.author.toLowerCase())) parts.push(`Thanks [@${info.author}](${serverUrl}/${info.author})!`);
167
+ if (thankContributors && info.author && !internalAuthors.has(info.author.toLowerCase())) parts.push(`Thanks [@${info.author}](${serverUrl}/${info.author})!`);
166
168
  return parts.join(" ");
167
169
  }
168
170
  /**
@@ -0,0 +1,65 @@
1
+ import { n as log, o as __toESM, t as colorize } from "./logger-C2dEe5Su.mjs";
2
+ import { a as loadConfig, o as loadPackageConfig } from "./config-B-Qg3DZH.mjs";
3
+ import { n as discoverWorkspace } from "./workspace-DWXlwcH4.mjs";
4
+ import { r as readBumpFiles, t as filterBranchBumpFiles } from "./bump-file-DVqR3k67.mjs";
5
+ import { r as getChangedFiles } from "./git-YDedMddc.mjs";
6
+ import { t as require_picomatch } from "./picomatch-DMmqYjgq.mjs";
7
+ import { relative } from "node:path";
8
+ //#region src/commands/check.ts
9
+ var import_picomatch = /* @__PURE__ */ __toESM(require_picomatch(), 1);
10
+ /**
11
+ * Local check: detect which packages have changed on this branch
12
+ * and verify they have corresponding bump files.
13
+ * Designed for pre-push hooks — no GitHub API needed.
14
+ */
15
+ async function checkCommand(rootDir) {
16
+ const config = await loadConfig(rootDir);
17
+ const { packages } = await discoverWorkspace(rootDir, config);
18
+ const baseBranch = config.baseBranch;
19
+ const changedFiles = getChangedFiles(rootDir, baseBranch);
20
+ if (changedFiles.length === 0) {
21
+ log.info("No changed files detected.");
22
+ return;
23
+ }
24
+ const { branchBumpFiles, branchBumpFileIds } = filterBranchBumpFiles(await readBumpFiles(rootDir), changedFiles);
25
+ if (branchBumpFileIds.size > branchBumpFiles.length) {
26
+ log.success("Empty bump file found — no releases needed.");
27
+ return;
28
+ }
29
+ const coveredPackages = /* @__PURE__ */ new Set();
30
+ for (const bf of branchBumpFiles) for (const release of bf.releases) coveredPackages.add(release.name);
31
+ const changedPackages = await findChangedPackages(changedFiles, packages, rootDir, config);
32
+ if (changedPackages.length === 0) {
33
+ log.info("No managed packages have changed.");
34
+ return;
35
+ }
36
+ const missing = changedPackages.filter((name) => !coveredPackages.has(name));
37
+ if (missing.length === 0) {
38
+ log.success(`🐸 All ${changedPackages.length} changed package(s) have bump files.`);
39
+ return;
40
+ }
41
+ log.warn(`${missing.length} changed package(s) missing bump files:\n`);
42
+ for (const name of missing) console.log(` ${colorize(name, "yellow")}`);
43
+ console.log();
44
+ log.dim("Run `bumpy add` to create a bump file, or `bumpy add --empty` if no release is needed.");
45
+ process.exit(1);
46
+ }
47
+ /** Map changed files to the packages they belong to */
48
+ async function findChangedPackages(changedFiles, packages, rootDir, config) {
49
+ const changed = /* @__PURE__ */ new Set();
50
+ const matchers = /* @__PURE__ */ new Map();
51
+ for (const [name, pkg] of packages) {
52
+ const patterns = (await loadPackageConfig(pkg.dir, config, name)).changedFilePatterns ?? config.changedFilePatterns;
53
+ matchers.set(name, (0, import_picomatch.default)(patterns));
54
+ }
55
+ for (const file of changedFiles) for (const [name, pkg] of packages) {
56
+ const pkgRelDir = relative(rootDir, pkg.dir);
57
+ if (file.startsWith(pkgRelDir + "/")) {
58
+ const relToPackage = file.slice(pkgRelDir.length + 1);
59
+ if (matchers.get(name)(relToPackage)) changed.add(name);
60
+ }
61
+ }
62
+ return [...changed];
63
+ }
64
+ //#endregion
65
+ export { checkCommand };