@varlock/bumpy 0.0.1 → 1.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 (44) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/dist/add-CgCjs4d-.mjs +313 -0
  3. package/dist/{ai-B8ZL2x8z.mjs → ai-sMYUf3lP.mjs} +22 -5
  4. package/dist/{apply-release-plan-DtU3rVyL.mjs → apply-release-plan-CczGWJTk.mjs} +34 -25
  5. package/dist/bump-file-CCLXMLA8.mjs +143 -0
  6. package/dist/changelog-github-Cd8uJHZI.mjs +195 -0
  7. package/dist/{check-CkRubvuk.mjs → check-BOoxpWqk.mjs} +11 -17
  8. package/dist/ci-Bhx--Tj6.mjs +629 -0
  9. package/dist/ci-setup-qz4Y3v7T.mjs +211 -0
  10. package/dist/clack-CDRCHrC-.mjs +1216 -0
  11. package/dist/cli.mjs +37 -31
  12. package/dist/{config-CJ2orhTL.mjs → config-XZWUL3ma.mjs} +28 -23
  13. package/dist/fs-DYR2XuFE.mjs +81 -0
  14. package/dist/{generate-oOFD9ABC.mjs → generate-gYKTpvex.mjs} +31 -12
  15. package/dist/git-CGHVXXKw.mjs +78 -0
  16. package/dist/index.d.mts +63 -37
  17. package/dist/index.mjs +9 -9
  18. package/dist/{init-Blw2GfC_.mjs → init-lA9E5pEc.mjs} +3 -3
  19. package/dist/logger-C2dEe5Su.mjs +135 -0
  20. package/dist/{migrate-DvOrXSw0.mjs → migrate-DmOYgmfD.mjs} +23 -16
  21. package/dist/{names-C-u50ofE.mjs → names-9VubBmL0.mjs} +3 -2
  22. package/dist/package-manager-VCe10bjc.mjs +80 -0
  23. package/dist/{publish-DZ3m7qkX.mjs → publish-Cun-zQ1b.mjs} +90 -35
  24. package/dist/{publish-pipeline-1M5GmbdP.mjs → publish-pipeline-BwBuKCIk.mjs} +56 -65
  25. package/dist/release-plan-Bi5QNSEo.mjs +264 -0
  26. package/dist/{semver-DWO6NFKN.mjs → semver-DfQyVLM_.mjs} +14 -4
  27. package/dist/shell-Dj7JRD_q.mjs +92 -0
  28. package/dist/{status-DRpq_Mha.mjs → status-CfE63ti5.mjs} +27 -23
  29. package/dist/version-19vVt9dv.mjs +124 -0
  30. package/dist/workspace-C5ULTyUN.mjs +107 -0
  31. package/package.json +16 -2
  32. package/skills/add-change/SKILL.md +8 -12
  33. package/dist/add-u5V9V3L7.mjs +0 -131
  34. package/dist/changelog-github-n-3zV1p9.mjs +0 -59
  35. package/dist/changeset-ClCYsChu.mjs +0 -75
  36. package/dist/ci-8KWWhjXl.mjs +0 -224
  37. package/dist/fs-DbNNEyzq.mjs +0 -51
  38. package/dist/logger-ZqggsyGZ.mjs +0 -176
  39. package/dist/prompt-BP8toAOI.mjs +0 -46
  40. package/dist/release-plan-CFnutSHD.mjs +0 -173
  41. package/dist/shell-DPlltpzb.mjs +0 -44
  42. package/dist/version-CJwf8XIA.mjs +0 -81
  43. package/dist/workspace-mVjawG8g.mjs +0 -183
  44. /package/dist/{dep-graph-DiLeAhl9.mjs → dep-graph-E-9-eQ2J.mjs} +0 -0
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "bumpy",
3
3
  "version": "0.0.1",
4
- "description": "AI-assisted changeset creation for bumpy monorepo versioning",
4
+ "description": "AI-assisted bump file creation for bumpy monorepo versioning",
5
5
  "author": {
6
6
  "name": "DMNO",
7
7
  "url": "https://github.com/dmno-dev"
8
8
  },
9
9
  "repository": "https://github.com/dmno-dev/bumpy",
10
10
  "license": "MIT",
11
- "keywords": ["monorepo", "versioning", "changelog", "changesets"],
11
+ "keywords": ["monorepo", "versioning", "changelog", "bump-files"],
12
12
  "skills": "./skills/"
13
13
  }
@@ -0,0 +1,313 @@
1
+ import { n as log, o as __toESM, r as require_picocolors } from "./logger-C2dEe5Su.mjs";
2
+ import { n as exists, t as ensureDir } from "./fs-DYR2XuFE.mjs";
3
+ import { a as loadConfig, r as getBumpyDir, s as matchGlob } from "./config-XZWUL3ma.mjs";
4
+ import { t as discoverPackages } from "./workspace-C5ULTyUN.mjs";
5
+ import { t as DependencyGraph } from "./dep-graph-E-9-eQ2J.mjs";
6
+ import { i as writeBumpFile } from "./bump-file-CCLXMLA8.mjs";
7
+ import { n as getChangedFiles } from "./git-CGHVXXKw.mjs";
8
+ import { c as ot, d as yt, i as _t, l as pt, o as gt, r as Ot, s as mt, t as unwrap, u as wt } from "./clack-CDRCHrC-.mjs";
9
+ import { n as slugify, t as randomName } from "./names-9VubBmL0.mjs";
10
+ import { relative, resolve } from "node:path";
11
+ import * as readline from "node:readline";
12
+ //#region src/prompts/bump-select.ts
13
+ var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
14
+ const LEVELS = [
15
+ "none",
16
+ "patch",
17
+ "minor",
18
+ "major"
19
+ ];
20
+ /**
21
+ * Custom interactive prompt for selecting bump levels for multiple packages.
22
+ * - Up/Down arrows to navigate between packages
23
+ * - Left/Right arrows to change the bump level
24
+ * - Changed packages default to "patch", unchanged to "none"
25
+ * - Enter to confirm
26
+ * - Ctrl+C / Escape to cancel
27
+ */
28
+ async function bumpSelectPrompt(items) {
29
+ const changedEntries = items.map((item, idx) => ({
30
+ item,
31
+ idx
32
+ })).filter(({ item }) => item.changed);
33
+ const unchangedEntries = items.map((item, idx) => ({
34
+ item,
35
+ idx
36
+ })).filter(({ item }) => !item.changed);
37
+ const displayOrder = [...changedEntries, ...unchangedEntries];
38
+ let cursor = 0;
39
+ const levels = items.map((item) => item.changed ? "patch" : "none");
40
+ return new Promise((resolve) => {
41
+ const { stdin, stdout } = process;
42
+ const rl = readline.createInterface({
43
+ input: stdin,
44
+ terminal: true
45
+ });
46
+ stdout.write("\x1B[?25l");
47
+ let renderedLines = 0;
48
+ function render(final = false) {
49
+ if (renderedLines > 0) {
50
+ stdout.write(`\x1B[${renderedLines}A`);
51
+ stdout.write("\x1B[0J");
52
+ }
53
+ const lines = [];
54
+ if (final) {
55
+ lines.push(`${import_picocolors.default.green("◇")} Bump levels selected`);
56
+ const selected = displayOrder.filter(({ idx }) => levels[idx] !== "none");
57
+ if (selected.length === 0) lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("(none)")}`);
58
+ else for (const { item, idx } of selected) lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.cyan(item.name)} ${import_picocolors.default.dim("→")} ${import_picocolors.default.bold(levels[idx])}`);
59
+ lines.push(import_picocolors.default.dim("│"));
60
+ } else {
61
+ lines.push(`${import_picocolors.default.cyan("◆")} Select bump levels`);
62
+ lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("↑/↓ navigate · ←/→ change level · enter to confirm")}`);
63
+ lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim("0 clear current · x clear all · r reset all to defaults")}`);
64
+ lines.push(import_picocolors.default.dim("│"));
65
+ let displayIdx = 0;
66
+ if (changedEntries.length > 0) {
67
+ lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.underline("Changed")}`);
68
+ for (const { item, idx } of changedEntries) {
69
+ lines.push(formatRow(item, levels[idx], cursor === displayIdx));
70
+ displayIdx++;
71
+ }
72
+ if (unchangedEntries.length > 0) lines.push(import_picocolors.default.dim("│"));
73
+ }
74
+ if (unchangedEntries.length > 0) {
75
+ lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.underline("Unchanged")}`);
76
+ for (const { item, idx } of unchangedEntries) {
77
+ lines.push(formatRow(item, levels[idx], cursor === displayIdx));
78
+ displayIdx++;
79
+ }
80
+ }
81
+ lines.push(import_picocolors.default.dim("│"));
82
+ const selectedCount = levels.filter((l) => l !== "none").length;
83
+ lines.push(`${import_picocolors.default.dim("│")} ${import_picocolors.default.dim(`${selectedCount} package${selectedCount !== 1 ? "s" : ""} selected`)}`);
84
+ lines.push(`${import_picocolors.default.dim("└")}`);
85
+ }
86
+ const output = lines.join("\n") + "\n";
87
+ stdout.write(output);
88
+ renderedLines = lines.length;
89
+ }
90
+ function cleanup() {
91
+ rl.close();
92
+ stdout.write("\x1B[?25h");
93
+ if (stdin.isTTY) stdin.setRawMode(false);
94
+ }
95
+ function finish(result) {
96
+ render(true);
97
+ cleanup();
98
+ resolve(result);
99
+ }
100
+ if (stdin.isTTY) stdin.setRawMode(true);
101
+ stdin.resume();
102
+ render();
103
+ stdin.on("keypress", (_str, key) => {
104
+ if (!key) return;
105
+ if (key.name === "escape" || key.ctrl && key.name === "c") {
106
+ cleanup();
107
+ if (renderedLines > 0) {
108
+ stdout.write(`\x1B[${renderedLines}A`);
109
+ stdout.write("\x1B[0J");
110
+ }
111
+ stdout.write(`${import_picocolors.default.red("■")} Cancelled\n`);
112
+ resolve(Symbol("cancel"));
113
+ return;
114
+ }
115
+ if (key.name === "return") {
116
+ const results = [];
117
+ for (let i = 0; i < items.length; i++) if (levels[i] !== "none") results.push({
118
+ name: items[i].name,
119
+ type: levels[i]
120
+ });
121
+ finish(results);
122
+ return;
123
+ }
124
+ if (key.name === "up" || key.name === "k") cursor = (cursor - 1 + displayOrder.length) % displayOrder.length;
125
+ else if (key.name === "down" || key.name === "j") cursor = (cursor + 1) % displayOrder.length;
126
+ else if (key.name === "right" || key.name === "l") {
127
+ const entry = displayOrder[cursor];
128
+ const currentLevel = LEVELS.indexOf(levels[entry.idx]);
129
+ if (currentLevel < LEVELS.length - 1) levels[entry.idx] = LEVELS[currentLevel + 1];
130
+ } else if (key.name === "left" || key.name === "h") {
131
+ const entry = displayOrder[cursor];
132
+ const currentLevel = LEVELS.indexOf(levels[entry.idx]);
133
+ if (currentLevel > 0) levels[entry.idx] = LEVELS[currentLevel - 1];
134
+ } else if (_str === "0" || key.name === "backspace") {
135
+ const entry = displayOrder[cursor];
136
+ levels[entry.idx] = "none";
137
+ } else if (_str === "r") for (let i = 0; i < items.length; i++) levels[i] = items[i].changed ? "patch" : "none";
138
+ else if (_str === "x") for (let i = 0; i < items.length; i++) levels[i] = "none";
139
+ render();
140
+ });
141
+ readline.emitKeypressEvents(stdin, rl);
142
+ });
143
+ }
144
+ function formatRow(item, level, focused) {
145
+ return `${import_picocolors.default.dim("│")} ${focused ? import_picocolors.default.cyan("›") : " "} ${focused ? import_picocolors.default.cyan(item.name) : item.name} ${import_picocolors.default.dim(`(${item.version})`)} ${formatLevel(level, focused)}`;
146
+ }
147
+ function formatLevel(level, focused) {
148
+ if (!focused) {
149
+ if (level === "none") return import_picocolors.default.dim("·");
150
+ if (level === "major") return import_picocolors.default.red(level);
151
+ if (level === "minor") return import_picocolors.default.yellow(level);
152
+ return import_picocolors.default.green(level);
153
+ }
154
+ return `◄ ${LEVELS.map((l) => {
155
+ if (l === level) {
156
+ if (l === "none") return import_picocolors.default.bold(import_picocolors.default.dim("[none]"));
157
+ if (l === "major") return import_picocolors.default.bold(import_picocolors.default.red(`[${l}]`));
158
+ if (l === "minor") return import_picocolors.default.bold(import_picocolors.default.yellow(`[${l}]`));
159
+ return import_picocolors.default.bold(import_picocolors.default.green(`[${l}]`));
160
+ }
161
+ return import_picocolors.default.dim(l);
162
+ }).join(import_picocolors.default.dim(" · "))} ►`;
163
+ }
164
+ //#endregion
165
+ //#region src/commands/add.ts
166
+ const CASCADE_CHOICES = [
167
+ {
168
+ label: "patch",
169
+ value: "patch"
170
+ },
171
+ {
172
+ label: "minor",
173
+ value: "minor"
174
+ },
175
+ {
176
+ label: "major",
177
+ value: "major"
178
+ }
179
+ ];
180
+ async function addCommand(rootDir, opts) {
181
+ const config = await loadConfig(rootDir);
182
+ const bumpyDir = getBumpyDir(rootDir);
183
+ await ensureDir(bumpyDir);
184
+ if (opts.empty) {
185
+ const filename = opts.name ? slugify(opts.name) : randomName();
186
+ const filePath = resolve(bumpyDir, `${filename}.md`);
187
+ const { writeText } = await import("./fs-DYR2XuFE.mjs").then((n) => n.r);
188
+ await writeText(filePath, "---\n---\n");
189
+ log.success(`Created empty bump file: .bumpy/${filename}.md`);
190
+ return;
191
+ }
192
+ let releases;
193
+ let summary;
194
+ let filename;
195
+ if (opts.packages) {
196
+ releases = parsePackagesFlag(opts.packages);
197
+ summary = opts.message || "";
198
+ filename = opts.name ? slugify(opts.name) : randomName();
199
+ } else {
200
+ mt(import_picocolors.default.bgCyan(import_picocolors.default.black(" bumpy add ")));
201
+ const pkgs = await discoverPackages(rootDir, config);
202
+ const depGraph = new DependencyGraph(pkgs);
203
+ if (pkgs.size === 0) {
204
+ pt("No managed packages found in this workspace.");
205
+ process.exit(1);
206
+ }
207
+ const baseBranch = config.baseBranch;
208
+ const changedFiles = getChangedFiles(rootDir, baseBranch);
209
+ const changedPackageNames = /* @__PURE__ */ new Set();
210
+ for (const file of changedFiles) for (const [name, pkg] of pkgs) {
211
+ const pkgRelDir = relative(rootDir, pkg.dir);
212
+ if (file.startsWith(pkgRelDir + "/")) changedPackageNames.add(name);
213
+ }
214
+ const bumpSelectResult = await bumpSelectPrompt([...pkgs.values()].map((pkg) => ({
215
+ name: pkg.name,
216
+ version: pkg.version,
217
+ changed: changedPackageNames.has(pkg.name)
218
+ })));
219
+ if (typeof bumpSelectResult === "symbol") {
220
+ pt("Aborted");
221
+ process.exit(0);
222
+ }
223
+ const bumpSelections = bumpSelectResult;
224
+ if (bumpSelections.length === 0) {
225
+ pt("No packages selected.");
226
+ process.exit(0);
227
+ }
228
+ releases = [];
229
+ for (const { name, type: bumpType } of bumpSelections) {
230
+ const release = {
231
+ name,
232
+ type: bumpType
233
+ };
234
+ {
235
+ const dependents = depGraph.getDependents(name);
236
+ const cascadeTargets = pkgs.get(name).bumpy?.cascadeTo;
237
+ if (dependents.length > 0 || cascadeTargets) {
238
+ if (unwrap(await ot({
239
+ message: `${import_picocolors.default.cyan(name)} has ${import_picocolors.default.bold(String(dependents.length))} dependents. Specify explicit cascades?`,
240
+ initialValue: false
241
+ }))) {
242
+ const allTargets = /* @__PURE__ */ new Set();
243
+ for (const d of dependents) allTargets.add(d.name);
244
+ if (cascadeTargets) {
245
+ for (const pattern of Object.keys(cascadeTargets)) for (const [pName] of pkgs) if (matchGlob(pName, pattern)) allTargets.add(pName);
246
+ }
247
+ const cascadeSelected = unwrap(await yt({
248
+ message: "Which packages should cascade?",
249
+ options: [...allTargets].map((n) => ({
250
+ label: n,
251
+ value: n
252
+ })),
253
+ required: false
254
+ }));
255
+ if (cascadeSelected.length > 0) {
256
+ const cascadeBump = unwrap(await _t({
257
+ message: "Cascade bump type",
258
+ options: CASCADE_CHOICES
259
+ }));
260
+ const cascade = {};
261
+ for (const target of cascadeSelected) cascade[target] = cascadeBump;
262
+ release.cascade = cascade;
263
+ }
264
+ }
265
+ }
266
+ }
267
+ releases.push(release);
268
+ }
269
+ summary = unwrap(await Ot({
270
+ message: "Summary (what changed and why)",
271
+ placeholder: "A short description of the change",
272
+ validate: (value) => {
273
+ if (!value || !value.trim()) return "Summary is required";
274
+ }
275
+ }));
276
+ const defaultName = randomName();
277
+ filename = slugify(unwrap(await Ot({
278
+ message: "Bump file name",
279
+ placeholder: defaultName,
280
+ defaultValue: defaultName,
281
+ validate: (value) => {
282
+ if (!value) return void 0;
283
+ if (!slugify(value)) return "Name must contain at least one alphanumeric character";
284
+ }
285
+ }))) || defaultName;
286
+ }
287
+ if (await exists(resolve(bumpyDir, `${filename}.md`))) filename = `${filename}-${Date.now()}`;
288
+ await writeBumpFile(rootDir, filename, releases, summary);
289
+ if (opts.packages) {
290
+ log.success(`Created bump file: .bumpy/${filename}.md`);
291
+ for (const r of releases) log.dim(` ${r.name}: ${r.type}${formatCascade(r)}`);
292
+ } else {
293
+ wt(releases.map((r) => `${import_picocolors.default.cyan(r.name)} ${import_picocolors.default.dim("→")} ${import_picocolors.default.bold(r.type)}${formatCascade(r)}`).join("\n"), "Bump file");
294
+ gt(import_picocolors.default.green(`Created .bumpy/${filename}.md`));
295
+ }
296
+ }
297
+ function formatCascade(r) {
298
+ if (!("cascade" in r) || Object.keys(r.cascade).length === 0) return "";
299
+ const parts = Object.entries(r.cascade).map(([k, v]) => `${k}:${v}`);
300
+ return import_picocolors.default.dim(` (cascade: ${parts.join(", ")})`);
301
+ }
302
+ function parsePackagesFlag(input) {
303
+ return input.split(",").map((entry) => {
304
+ const [name, type] = entry.trim().split(":");
305
+ if (!name || !type) throw new Error(`Invalid package format: "${entry}". Expected "name:bumpType"`);
306
+ return {
307
+ name: name.trim(),
308
+ type: type.trim()
309
+ };
310
+ });
311
+ }
312
+ //#endregion
313
+ export { addCommand };
@@ -1,10 +1,12 @@
1
- import { n as log } from "./logger-ZqggsyGZ.mjs";
2
- import { l as writeText, n as exists, t as ensureDir } from "./fs-DbNNEyzq.mjs";
1
+ import { n as log } from "./logger-C2dEe5Su.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
- import { n as log } from "./logger-ZqggsyGZ.mjs";
2
- import { a as readJson, c as writeJson, l as writeText, n as exists, o as readText } from "./fs-DbNNEyzq.mjs";
3
- import { t as deleteChangesets } from "./changeset-ClCYsChu.mjs";
4
- import { resolve } from "node:path";
1
+ import { n as log } from "./logger-C2dEe5Su.mjs";
2
+ import { a as readJson, c as updateJsonFields, d as writeText, l as updateJsonNestedField, n as exists, o as readText } from "./fs-DYR2XuFE.mjs";
3
+ import { t as deleteBumpFiles } from "./bump-file-CCLXMLA8.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-n-3zV1p9.mjs");
31
+ const { createGithubFormatter } = await import("./changelog-github-Cd8uJHZI.mjs");
31
32
  return createGithubFormatter();
32
33
  }
33
34
  };
@@ -37,17 +38,26 @@ 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-Cd8uJHZI.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-n-3zV1p9.mjs");
47
- return createGithubFormatter(options);
48
- }
49
50
  if (typeof name === "string") try {
50
- const mod = await (name.startsWith(".") ? import(resolve(rootDir, name)) : import(name));
51
+ let modulePath;
52
+ if (name.startsWith(".")) {
53
+ modulePath = resolve(rootDir, name);
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`);
59
+ } else modulePath = name;
60
+ const mod = await import(modulePath);
51
61
  const exported = mod.default || mod.changelogFormatter;
52
62
  if (typeof exported === "function") {
53
63
  const result = exported(options);
@@ -63,10 +73,10 @@ async function loadFormatter(changelog, rootDir) {
63
73
  return defaultFormatter;
64
74
  }
65
75
  /** Generate a changelog entry using the configured formatter */
66
- 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]) {
67
77
  return formatter({
68
78
  release,
69
- changesets,
79
+ bumpFiles,
70
80
  date
71
81
  });
72
82
  }
@@ -82,14 +92,14 @@ function prependToChangelog(existingContent, newEntry) {
82
92
  }
83
93
  //#endregion
84
94
  //#region src/core/apply-release-plan.ts
85
- /** Apply the release plan: bump versions, update changelogs, delete changesets */
95
+ /** Apply the release plan: bump versions, update changelogs, delete bump files */
86
96
  async function applyReleasePlan(releasePlan, packages, rootDir, config) {
87
97
  const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
88
98
  const formatter = await loadFormatter(config.changelog, rootDir);
89
99
  for (const release of releasePlan.releases) {
90
100
  const pkgJsonPath = resolve(packages.get(release.name).dir, "package.json");
91
101
  const pkgJson = await readJson(pkgJsonPath);
92
- pkgJson.version = release.newVersion;
102
+ await updateJsonFields(pkgJsonPath, { version: release.newVersion });
93
103
  for (const depField of [
94
104
  "dependencies",
95
105
  "devDependencies",
@@ -101,19 +111,18 @@ async function applyReleasePlan(releasePlan, packages, rootDir, config) {
101
111
  for (const [depName, range] of Object.entries(deps)) {
102
112
  const depRelease = releaseMap.get(depName);
103
113
  if (!depRelease) continue;
104
- deps[depName] = updateRange(range, depRelease.newVersion);
114
+ await updateJsonNestedField(pkgJsonPath, depField, depName, updateRange(range, depRelease.newVersion));
105
115
  }
106
116
  }
107
- await writeJson(pkgJsonPath, pkgJson);
108
117
  }
109
118
  for (const release of releasePlan.releases) {
110
119
  const changelogPath = resolve(packages.get(release.name).dir, "CHANGELOG.md");
111
- const entry = await generateChangelogEntry(release, releasePlan.changesets, formatter);
120
+ const entry = await generateChangelogEntry(release, releasePlan.bumpFiles, formatter);
112
121
  let existingContent = "";
113
122
  if (await exists(changelogPath)) existingContent = await readText(changelogPath);
114
123
  await writeText(changelogPath, prependToChangelog(existingContent, entry));
115
124
  }
116
- await deleteChangesets(rootDir, releasePlan.changesets.map((cs) => cs.id));
125
+ await deleteBumpFiles(rootDir, releasePlan.bumpFiles.map((bf) => bf.id));
117
126
  }
118
127
  /** Update a version range to include a new version, preserving the range prefix */
119
128
  function updateRange(range, newVersion) {
@@ -0,0 +1,143 @@
1
+ import { n as log } from "./logger-C2dEe5Su.mjs";
2
+ import { d as writeText, i as listFiles, o as readText, s as removeFile } from "./fs-DYR2XuFE.mjs";
3
+ import { r as getBumpyDir } from "./config-XZWUL3ma.mjs";
4
+ import { t as jsYaml } from "./js-yaml-DpZfOoD4.mjs";
5
+ import { o as tryRunArgs } from "./shell-Dj7JRD_q.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
+ /** Delete consumed bump files */
135
+ async function deleteBumpFiles(rootDir, ids) {
136
+ const dir = getBumpyDir(rootDir);
137
+ for (const id of ids) await removeFile(resolve(dir, `${id}.md`));
138
+ }
139
+ function fileToId(filePath) {
140
+ return filePath.split("/").pop().replace(/\.md$/, "");
141
+ }
142
+ //#endregion
143
+ export { writeBumpFile as i, parseBumpFile as n, readBumpFiles as r, deleteBumpFiles as t };