@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
@@ -0,0 +1,107 @@
1
+ import { a as readJson, n as exists } from "./fs-DYR2XuFE.mjs";
2
+ import { i as isPackageManaged, o as loadPackageConfig } from "./config-XZWUL3ma.mjs";
3
+ import { n as detectWorkspaces } from "./package-manager-VCe10bjc.mjs";
4
+ import { relative, resolve } from "node:path";
5
+ import { readdir, stat } from "node:fs/promises";
6
+ //#region src/core/workspace.ts
7
+ /** Discover all workspace packages and catalogs in a monorepo */
8
+ async function discoverWorkspace(rootDir, config) {
9
+ const { globs, catalogs } = await detectWorkspaces(rootDir);
10
+ if (globs.length === 0) throw new Error("No workspace globs found. Is this a monorepo?");
11
+ const packages = /* @__PURE__ */ new Map();
12
+ for (const glob of globs) {
13
+ const dirs = await resolveGlob(rootDir, glob);
14
+ for (const dir of dirs) {
15
+ const pkg = await loadWorkspacePackage(dir, rootDir, config);
16
+ if (pkg) {
17
+ if (!isPackageManaged(pkg.name, pkg.private, config, pkg.bumpy)) continue;
18
+ packages.set(pkg.name, pkg);
19
+ }
20
+ }
21
+ }
22
+ return {
23
+ packages,
24
+ catalogs
25
+ };
26
+ }
27
+ /** Convenience wrapper that returns just packages (backwards compat) */
28
+ async function discoverPackages(rootDir, config) {
29
+ const { packages } = await discoverWorkspace(rootDir, config);
30
+ return packages;
31
+ }
32
+ /** Resolve a workspace glob pattern to directories containing package.json */
33
+ async function resolveGlob(rootDir, pattern) {
34
+ return expandGlob(rootDir, pattern.split("/"));
35
+ }
36
+ async function expandGlob(baseDir, parts) {
37
+ if (parts.length === 0) {
38
+ if (await exists(resolve(baseDir, "package.json"))) return [baseDir];
39
+ return [];
40
+ }
41
+ const [current, ...rest] = parts;
42
+ if (current === "*") {
43
+ const entries = await safeReaddir(baseDir);
44
+ const results = [];
45
+ for (const entry of entries) {
46
+ const entryPath = resolve(baseDir, entry);
47
+ if (await isDirectory(entryPath)) results.push(...await expandGlob(entryPath, rest));
48
+ }
49
+ return results;
50
+ } else if (current === "**") {
51
+ const results = [];
52
+ results.push(...await expandGlob(baseDir, rest));
53
+ const entries = await safeReaddir(baseDir);
54
+ for (const entry of entries) {
55
+ if (entry.startsWith(".") || entry === "node_modules") continue;
56
+ const entryPath = resolve(baseDir, entry);
57
+ if (await isDirectory(entryPath)) results.push(...await expandGlob(entryPath, parts));
58
+ }
59
+ return results;
60
+ } else {
61
+ const next = resolve(baseDir, current);
62
+ if (await isDirectory(next)) return expandGlob(next, rest);
63
+ return [];
64
+ }
65
+ }
66
+ async function safeReaddir(dir) {
67
+ try {
68
+ return await readdir(dir);
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+ async function isDirectory(path) {
74
+ try {
75
+ return (await stat(path)).isDirectory();
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+ async function loadWorkspacePackage(dir, rootDir, config) {
81
+ const pkgPath = resolve(dir, "package.json");
82
+ if (!await exists(pkgPath)) return null;
83
+ let pkg;
84
+ try {
85
+ pkg = await readJson(pkgPath);
86
+ } catch {
87
+ return null;
88
+ }
89
+ const name = pkg.name;
90
+ if (!name) return null;
91
+ const bumpy = await loadPackageConfig(dir, config, name);
92
+ return {
93
+ name,
94
+ version: pkg.version || "0.0.0",
95
+ dir: resolve(dir),
96
+ relativeDir: relative(rootDir, dir),
97
+ packageJson: pkg,
98
+ private: !!pkg.private,
99
+ dependencies: pkg.dependencies || {},
100
+ devDependencies: pkg.devDependencies || {},
101
+ peerDependencies: pkg.peerDependencies || {},
102
+ optionalDependencies: pkg.optionalDependencies || {},
103
+ bumpy
104
+ };
105
+ }
106
+ //#endregion
107
+ export { discoverWorkspace as n, discoverPackages as t };
package/package.json CHANGED
@@ -1,7 +1,19 @@
1
1
  {
2
2
  "name": "@varlock/bumpy",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "Modern monorepo versioning and changelog tool",
5
+ "keywords": [
6
+ "bump",
7
+ "changelog",
8
+ "changesets",
9
+ "monorepo",
10
+ "version",
11
+ "version management"
12
+ ],
13
+ "homepage": "https://bumpy.varlock.dev",
14
+ "bugs": "https://github.com/dmno-dev/bumpy/issues",
15
+ "license": "MIT",
16
+ "author": "dmno-dev",
5
17
  "repository": {
6
18
  "type": "git",
7
19
  "url": "https://github.com/dmno-dev/bumpy",
@@ -24,14 +36,16 @@
24
36
  },
25
37
  "scripts": {
26
38
  "build": "tsdown",
39
+ "check": "bun run tsc --noEmit",
27
40
  "test": "bun test"
28
41
  },
29
42
  "devDependencies": {
43
+ "@clack/prompts": "^1.2.0",
30
44
  "@types/bun": "latest",
31
45
  "@types/js-yaml": "^4.0.9",
32
46
  "@types/semver": "^7.7.0",
33
- "ansis": "^4.2.0",
34
47
  "js-yaml": "^4.1.0",
48
+ "picocolors": "^1.1.1",
35
49
  "semver": "^7.7.2",
36
50
  "tsdown": "^0.21.8"
37
51
  }
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: add-change
3
- description: Create a bumpy changeset describing which packages changed and how, for version bumping and changelog generation. Use when the user wants to record a change, add a changeset, or prepare packages for release.
3
+ description: Create a bumpy bump file describing which packages changed and how, for version bumping and changelog generation. Use when the user wants to record a change, add a bump file, or prepare packages for release.
4
4
  argument-hint: '[description of changes]'
5
5
  allowed-tools: Read Grep Glob Bash Edit Write
6
6
  ---
7
7
 
8
- # Create a bumpy changeset
8
+ # Create a bumpy bump file
9
9
 
10
- You are helping the user create a **bumpy changeset** — a markdown file in `.bumpy/` that describes which packages changed and how. Bumpy uses these to bump versions and generate changelogs.
10
+ You are helping the user create a **bumpy bump file** — a markdown file in `.bumpy/` that describes which packages changed and how. Bumpy uses these to bump versions and generate changelogs.
11
11
 
12
12
  ## Steps
13
13
 
@@ -17,7 +17,7 @@ First, understand what changed. Run these in parallel:
17
17
 
18
18
  - `git diff --stat` — see which files changed
19
19
  - `git diff --cached --stat` — see staged changes
20
- - `bumpy status --json` — see if there are already pending changesets
20
+ - `bumpy status --json` — see if there are already pending bump files
21
21
 
22
22
  If the user provided a description via `$ARGUMENTS`, use that as additional context for understanding the change.
23
23
 
@@ -43,11 +43,7 @@ For each affected package, choose the appropriate bump level:
43
43
  | **minor** | New features: added exports, new options, new functionality |
44
44
  | **patch** | Bug fixes, internal refactors, documentation, dependency updates |
45
45
 
46
- Append `-isolated` (e.g., `patch-isolated`) if the change is purely internal and dependents should NOT be bumped. Use this for:
47
-
48
- - Internal refactors with no API changes
49
- - Dev tooling / test changes
50
- - Documentation-only changes
46
+ Use `none` in a bump file to suppress a bump on a package that would otherwise be included via propagation. If skipping would leave a broken range, bumpy throws an error.
51
47
 
52
48
  ### 4. Write a clear summary
53
49
 
@@ -57,7 +53,7 @@ Write a concise summary (1-3 sentences) describing **what** changed and **why**.
57
53
  - Focus on user-facing impact, not implementation details
58
54
  - Are specific enough to be useful months later
59
55
 
60
- ### 5. Create the changeset
56
+ ### 5. Create the bump file
61
57
 
62
58
  Use the non-interactive CLI:
63
59
 
@@ -83,7 +79,7 @@ bumpy add \
83
79
 
84
80
  ## Advanced: cascading bumps
85
81
 
86
- If a change in a core package should explicitly cascade to dependents with specific bump levels, write the changeset file directly instead of using the CLI:
82
+ If a change in a core package should explicitly cascade to dependents with specific bump levels, write the bump file directly instead of using the CLI:
87
83
 
88
84
  ```bash
89
85
  cat > .bumpy/<name>.md << 'EOF'
@@ -105,4 +101,4 @@ EOF
105
101
  - Only include packages that have **actual code changes** — bumpy handles dependency propagation automatically
106
102
  - If the user hasn't made any changes yet, ask what they're planning to change
107
103
  - If the change doesn't affect any publishable packages (e.g., only root config files), suggest using `bumpy add --empty` to satisfy CI checks
108
- - One changeset per logical change — don't combine unrelated changes
104
+ - One bump file per logical change — don't combine unrelated changes
@@ -1,131 +0,0 @@
1
- import { n as log, t as colorize } from "./logger-ZqggsyGZ.mjs";
2
- import { n as exists, t as ensureDir } from "./fs-DbNNEyzq.mjs";
3
- import { a as loadConfig, r as getBumpyDir, s as matchGlob } from "./config-CJ2orhTL.mjs";
4
- import { t as discoverPackages } from "./workspace-mVjawG8g.mjs";
5
- import { t as DependencyGraph } from "./dep-graph-DiLeAhl9.mjs";
6
- import { i as writeChangeset } from "./changeset-ClCYsChu.mjs";
7
- import { i as select, n as confirm, r as multiSelect, t as ask } from "./prompt-BP8toAOI.mjs";
8
- import { n as slugify, t as randomName } from "./names-C-u50ofE.mjs";
9
- import { resolve } from "node:path";
10
- //#region src/commands/add.ts
11
- const BUMP_CHOICES = [
12
- {
13
- label: "patch",
14
- value: "patch"
15
- },
16
- {
17
- label: "minor",
18
- value: "minor"
19
- },
20
- {
21
- label: "major",
22
- value: "major"
23
- },
24
- {
25
- label: "patch (isolated - no cascade)",
26
- value: "patch-isolated"
27
- },
28
- {
29
- label: "minor (isolated - no cascade)",
30
- value: "minor-isolated"
31
- }
32
- ];
33
- async function addCommand(rootDir, opts) {
34
- const config = await loadConfig(rootDir);
35
- const bumpyDir = getBumpyDir(rootDir);
36
- await ensureDir(bumpyDir);
37
- if (opts.empty) {
38
- const filename = opts.name ? slugify(opts.name) : randomName();
39
- const filePath = resolve(bumpyDir, `${filename}.md`);
40
- const { writeText } = await import("./fs-DbNNEyzq.mjs").then((n) => n.r);
41
- await writeText(filePath, "---\n---\n");
42
- log.success(`Created empty changeset: .bumpy/${filename}.md`);
43
- return;
44
- }
45
- let releases;
46
- let summary;
47
- if (opts.packages) {
48
- releases = parsePackagesFlag(opts.packages);
49
- summary = opts.message || "";
50
- } else {
51
- const pkgs = await discoverPackages(rootDir, config);
52
- const depGraph = new DependencyGraph(pkgs);
53
- const selected = await multiSelect("Which packages should be included in this changeset?", [...pkgs.values()].map((p) => ({
54
- label: `${p.name} (${p.version})`,
55
- value: p.name
56
- })));
57
- if (selected.length === 0) {
58
- log.warn("No packages selected. Aborting.");
59
- return;
60
- }
61
- releases = [];
62
- for (const name of selected) {
63
- const bumpType = await select(`Bump type for ${colorize(name, "cyan")}:`, BUMP_CHOICES);
64
- const release = {
65
- name,
66
- type: bumpType
67
- };
68
- if (!bumpType.endsWith("-isolated")) {
69
- const dependents = depGraph.getDependents(name);
70
- const cascadeTargets = pkgs.get(name).bumpy?.cascadeTo;
71
- if (dependents.length > 0 || cascadeTargets) {
72
- if (await confirm(`${name} has ${dependents.length} dependents. Specify explicit cascades?`, false)) {
73
- const allTargets = /* @__PURE__ */ new Set();
74
- for (const d of dependents) allTargets.add(d.name);
75
- if (cascadeTargets) {
76
- for (const pattern of Object.keys(cascadeTargets)) for (const [pName] of pkgs) if (matchGlob(pName, pattern)) allTargets.add(pName);
77
- }
78
- const cascadeSelected = await multiSelect("Which packages should cascade?", [...allTargets].map((n) => ({
79
- label: n,
80
- value: n
81
- })));
82
- if (cascadeSelected.length > 0) {
83
- const cascadeBump = await select("Cascade bump type:", [
84
- {
85
- label: "patch",
86
- value: "patch"
87
- },
88
- {
89
- label: "minor",
90
- value: "minor"
91
- },
92
- {
93
- label: "major",
94
- value: "major"
95
- }
96
- ]);
97
- const cascade = {};
98
- for (const target of cascadeSelected) cascade[target] = cascadeBump;
99
- release.cascade = cascade;
100
- }
101
- }
102
- }
103
- }
104
- releases.push(release);
105
- }
106
- summary = await ask("Summary (what changed and why)");
107
- }
108
- let filename;
109
- if (opts.name) filename = slugify(opts.name);
110
- else if (opts.packages) filename = randomName();
111
- else filename = slugify(await ask("Changeset name", randomName())) || randomName();
112
- if (await exists(resolve(bumpyDir, `${filename}.md`))) filename = `${filename}-${Date.now()}`;
113
- await writeChangeset(rootDir, filename, releases, summary);
114
- log.success(`Created changeset: .bumpy/${filename}.md`);
115
- for (const r of releases) {
116
- const cascade = "cascade" in r && Object.keys(r.cascade).length > 0 ? ` (cascade: ${Object.entries(r.cascade).map(([k, v]) => `${k}:${v}`).join(", ")})` : "";
117
- log.dim(` ${r.name}: ${r.type}${cascade}`);
118
- }
119
- }
120
- function parsePackagesFlag(input) {
121
- return input.split(",").map((entry) => {
122
- const [name, type] = entry.trim().split(":");
123
- if (!name || !type) throw new Error(`Invalid package format: "${entry}". Expected "name:bumpType"`);
124
- return {
125
- name: name.trim(),
126
- type: type.trim()
127
- };
128
- });
129
- }
130
- //#endregion
131
- export { addCommand };
@@ -1,59 +0,0 @@
1
- import { i as tryRun } from "./shell-DPlltpzb.mjs";
2
- //#region src/core/changelog-github.ts
3
- /**
4
- * GitHub-enhanced changelog formatter.
5
- * Adds PR links and author attribution when git/gh info is available.
6
- *
7
- * Usage in config:
8
- * "changelog": "github"
9
- * "changelog": ["github", { "repo": "dmno-dev/bumpy" }]
10
- */
11
- function createGithubFormatter(options = {}) {
12
- return async (ctx) => {
13
- const { release, changesets, date } = ctx;
14
- const lines = [];
15
- lines.push(`## ${release.newVersion}`);
16
- lines.push("");
17
- lines.push(`_${date}_`);
18
- lines.push("");
19
- const relevantChangesets = changesets.filter((cs) => release.changesets.includes(cs.id));
20
- if (relevantChangesets.length > 0) for (const cs of relevantChangesets) {
21
- if (!cs.summary) continue;
22
- const firstLine = cs.summary.split("\n")[0];
23
- const prInfo = await findPrForChangeset(cs.id, options.repo);
24
- if (prInfo) lines.push(`- ${firstLine} ([#${prInfo.number}](${prInfo.url})) by @${prInfo.author}`);
25
- else lines.push(`- ${firstLine}`);
26
- const summaryLines = cs.summary.split("\n");
27
- for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${summaryLines[i]}`);
28
- }
29
- if (release.isDependencyBump && relevantChangesets.length === 0) lines.push("- Updated dependencies");
30
- if (release.isCascadeBump && !release.isDependencyBump && relevantChangesets.length === 0) lines.push("- Version bump via cascade rule");
31
- lines.push("");
32
- return lines.join("\n");
33
- };
34
- }
35
- /**
36
- * Find the PR that introduced a changeset file by checking git log
37
- * for the commit that added the file, then looking up the PR.
38
- */
39
- async function findPrForChangeset(changesetId, repo) {
40
- try {
41
- const commitHash = tryRun(`git log --diff-filter=A --format="%H" -- ".bumpy/${changesetId}.md" ".changeset/${changesetId}.md"`);
42
- if (!commitHash) return null;
43
- const hash = commitHash.split("\n")[0].trim();
44
- if (!hash) return null;
45
- const prJson = tryRun(`gh pr list --search "${hash}" --state merged --json number,url,author --jq ".[0]" ${repo ? `--repo ${repo}` : ""}`);
46
- if (!prJson) return null;
47
- const pr = JSON.parse(prJson);
48
- if (!pr.number) return null;
49
- return {
50
- number: pr.number,
51
- url: pr.url,
52
- author: pr.author?.login || "unknown"
53
- };
54
- } catch {
55
- return null;
56
- }
57
- }
58
- //#endregion
59
- export { createGithubFormatter };
@@ -1,75 +0,0 @@
1
- import { i as listFiles, l as writeText, o as readText, s as removeFile } from "./fs-DbNNEyzq.mjs";
2
- import { r as getBumpyDir } from "./config-CJ2orhTL.mjs";
3
- import { t as jsYaml } from "./js-yaml-DpZfOoD4.mjs";
4
- import { resolve } from "node:path";
5
- //#region src/core/changeset.ts
6
- /** Read all changeset files from .bumpy/ directory */
7
- async function readChangesets(rootDir) {
8
- const dir = getBumpyDir(rootDir);
9
- const files = await listFiles(dir, ".md");
10
- const changesets = [];
11
- for (const file of files) {
12
- if (file === "README.md") continue;
13
- const cs = await parseChangesetFile(resolve(dir, file));
14
- if (cs) changesets.push(cs);
15
- }
16
- return changesets;
17
- }
18
- /** Parse a single changeset markdown file */
19
- async function parseChangesetFile(filePath) {
20
- return parseChangeset(await readText(filePath), fileToId(filePath));
21
- }
22
- /** Parse changeset content (for testing) */
23
- function parseChangeset(content, id) {
24
- const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
25
- if (!match) return null;
26
- const frontmatter = match[1];
27
- const summary = match[2].trim();
28
- const parsed = jsYaml.load(frontmatter);
29
- if (!parsed || typeof parsed !== "object") return null;
30
- const releases = [];
31
- for (const [name, value] of Object.entries(parsed)) if (typeof value === "string") releases.push({
32
- name,
33
- type: value
34
- });
35
- else if (value && typeof value === "object") {
36
- const obj = value;
37
- const release = {
38
- name,
39
- type: obj.bump,
40
- cascade: obj.cascade || {}
41
- };
42
- releases.push(release);
43
- }
44
- if (releases.length === 0) return null;
45
- return {
46
- id,
47
- releases,
48
- summary
49
- };
50
- }
51
- /** Write a changeset file */
52
- async function writeChangeset(rootDir, filename, releases, summary) {
53
- const filePath = resolve(getBumpyDir(rootDir), `${filename}.md`);
54
- const frontmatter = {};
55
- for (const release of releases) if ("cascade" in release && Object.keys(release.cascade).length > 0) frontmatter[release.name] = {
56
- bump: release.type,
57
- cascade: release.cascade
58
- };
59
- else frontmatter[release.name] = release.type;
60
- await writeText(filePath, `---\n${jsYaml.dump(frontmatter, {
61
- lineWidth: -1,
62
- quotingType: "\""
63
- }).trim()}\n---\n\n${summary}\n`);
64
- return filePath;
65
- }
66
- /** Delete consumed changeset files */
67
- async function deleteChangesets(rootDir, ids) {
68
- const dir = getBumpyDir(rootDir);
69
- for (const id of ids) await removeFile(resolve(dir, `${id}.md`));
70
- }
71
- function fileToId(filePath) {
72
- return filePath.split("/").pop().replace(/\.md$/, "");
73
- }
74
- //#endregion
75
- export { writeChangeset as i, parseChangeset as n, readChangesets as r, deleteChangesets as t };