@varlock/bumpy 0.0.2 → 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.
- package/.claude-plugin/plugin.json +2 -2
- package/dist/add-CgCjs4d-.mjs +313 -0
- package/dist/{ai-CQhUyHAG.mjs → ai-sMYUf3lP.mjs} +21 -4
- package/dist/{apply-release-plan-D6TSrcwX.mjs → apply-release-plan-CczGWJTk.mjs} +28 -24
- package/dist/bump-file-CCLXMLA8.mjs +143 -0
- package/dist/{changelog-github-Du62krXi.mjs → changelog-github-Cd8uJHZI.mjs} +22 -20
- package/dist/{check-jIwike9F.mjs → check-BOoxpWqk.mjs} +9 -9
- package/dist/{ci-D6LQbR38.mjs → ci-Bhx--Tj6.mjs} +116 -72
- package/dist/{ci-setup-C6FlOfW5.mjs → ci-setup-qz4Y3v7T.mjs} +1 -1
- package/dist/cli.mjs +32 -30
- package/dist/{config-BkwIEaQg.mjs → config-XZWUL3ma.mjs} +27 -22
- package/dist/fs-DYR2XuFE.mjs +81 -0
- package/dist/{generate-Btrsn1qi.mjs → generate-gYKTpvex.mjs} +8 -8
- package/dist/index.d.mts +55 -37
- package/dist/index.mjs +8 -8
- package/dist/{init-B0q3wEQW.mjs → init-lA9E5pEc.mjs} +2 -2
- package/dist/{migrate-CfQNwD0T.mjs → migrate-DmOYgmfD.mjs} +10 -10
- package/dist/{names-Ck8cun7B.mjs → names-9VubBmL0.mjs} +1 -1
- package/dist/{package-manager-DcI5TdDE.mjs → package-manager-VCe10bjc.mjs} +1 -1
- package/dist/{publish-D_7RqEYL.mjs → publish-Cun-zQ1b.mjs} +21 -20
- package/dist/{publish-pipeline-ChnqW8nR.mjs → publish-pipeline-BwBuKCIk.mjs} +22 -17
- package/dist/release-plan-Bi5QNSEo.mjs +264 -0
- package/dist/{semver-BTzYh8vc.mjs → semver-DfQyVLM_.mjs} +13 -3
- package/dist/{status--Q8yAxQ4.mjs → status-CfE63ti5.mjs} +25 -21
- package/dist/{version-cAUkfYPx.mjs → version-19vVt9dv.mjs} +16 -12
- package/dist/{workspace-CxEKakDm.mjs → workspace-C5ULTyUN.mjs} +3 -3
- package/package.json +13 -1
- package/skills/add-change/SKILL.md +8 -12
- package/dist/add-BjyVIUlr.mjs +0 -175
- package/dist/changeset-UCZdSRDv.mjs +0 -108
- package/dist/fs-0AtnPUUe.mjs +0 -51
- package/dist/release-plan-BEzwApuK.mjs +0 -173
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bumpy",
|
|
3
3
|
"version": "0.0.1",
|
|
4
|
-
"description": "AI-assisted
|
|
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", "
|
|
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
1
|
import { n as log } from "./logger-C2dEe5Su.mjs";
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
3
|
-
import { t as
|
|
4
|
-
import { resolve } from "node:path";
|
|
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,
|
|
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
|
|
15
|
-
if (
|
|
16
|
-
for (const
|
|
17
|
-
const summaryLines =
|
|
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 &&
|
|
23
|
-
if (release.isCascadeBump && !release.isDependencyBump &&
|
|
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-
|
|
31
|
+
const { createGithubFormatter } = await import("./changelog-github-Cd8uJHZI.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-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-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
|
-
|
|
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,
|
|
76
|
+
async function generateChangelogEntry(release, bumpFiles, formatter = defaultFormatter, date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]) {
|
|
72
77
|
return formatter({
|
|
73
78
|
release,
|
|
74
|
-
|
|
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
|
|
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
98
|
const formatter = await loadFormatter(config.changelog, rootDir);
|
|
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
|
-
|
|
102
|
+
await updateJsonFields(pkgJsonPath, { version: release.newVersion });
|
|
98
103
|
for (const depField of [
|
|
99
104
|
"dependencies",
|
|
100
105
|
"devDependencies",
|
|
@@ -106,19 +111,18 @@ 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
|
-
|
|
114
|
+
await updateJsonNestedField(pkgJsonPath, depField, depName, updateRange(range, depRelease.newVersion));
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
|
-
await writeJson(pkgJsonPath, pkgJson);
|
|
113
117
|
}
|
|
114
118
|
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.
|
|
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
|
|
125
|
+
await deleteBumpFiles(rootDir, releasePlan.bumpFiles.map((bf) => bf.id));
|
|
122
126
|
}
|
|
123
127
|
/** Update a version range to include a new version, preserving the range prefix */
|
|
124
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 };
|