@varlock/bumpy 1.0.0 → 1.2.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 (38) hide show
  1. package/README.md +256 -0
  2. package/config-schema.json +327 -0
  3. package/dist/{add-CgCjs4d-.mjs → add-yP81c9_q.mjs} +23 -13
  4. package/dist/{ai-sMYUf3lP.mjs → ai-STKnq09z.mjs} +1 -1
  5. package/dist/{apply-release-plan-CczGWJTk.mjs → apply-release-plan-CPzu6JcF.mjs} +12 -7
  6. package/dist/{bump-file-CCLXMLA8.mjs → bump-file-Br2bTaWp.mjs} +36 -10
  7. package/dist/{changelog-github-Cd8uJHZI.mjs → changelog-github-DkACMj0j.mjs} +1 -1
  8. package/dist/check-D_0exKi6.mjs +87 -0
  9. package/dist/{ci-Bhx--Tj6.mjs → ci-CvaikKX1.mjs} +48 -33
  10. package/dist/{ci-setup-qz4Y3v7T.mjs → ci-setup-CARJFhcE.mjs} +3 -3
  11. package/dist/cli.mjs +31 -31
  12. package/dist/commit-message-BwsowSds.mjs +23 -0
  13. package/dist/{config-XZWUL3ma.mjs → config-D7Umr-fT.mjs} +6 -5
  14. package/dist/{fs-DYR2XuFE.mjs → fs-DnDogVn-.mjs} +16 -1
  15. package/dist/{generate-gYKTpvex.mjs → generate-BOLrTYWR.mjs} +74 -65
  16. package/dist/{git-CGHVXXKw.mjs → git-YDedMddc.mjs} +54 -2
  17. package/dist/index.d.mts +13 -2
  18. package/dist/index.mjs +8 -8
  19. package/dist/init-DJhMaceS.mjs +196 -0
  20. package/dist/{js-yaml-DpZfOoD4.mjs → package-manager-ByJ0wKYh.mjs} +79 -1
  21. package/dist/picomatch-DMmqYjgq.mjs +1870 -0
  22. package/dist/{publish-Cun-zQ1b.mjs → publish-CPZwqyWh.mjs} +10 -10
  23. package/dist/{publish-pipeline-BwBuKCIk.mjs → publish-pipeline-BFt96o_h.mjs} +5 -5
  24. package/dist/{release-plan-Bi5QNSEo.mjs → release-plan-CNOuSI-d.mjs} +2 -2
  25. package/dist/{shell-Dj7JRD_q.mjs → shell-CY7OD48z.mjs} +20 -2
  26. package/dist/{status-CfE63ti5.mjs → status-skGX8uU7.mjs} +6 -6
  27. package/dist/{version-19vVt9dv.mjs → version-CnXcbqi1.mjs} +13 -16
  28. package/dist/{workspace-C5ULTyUN.mjs → workspace-BHsAPUmC.mjs} +3 -3
  29. package/package.json +5 -1
  30. package/skills/add-change/SKILL.md +11 -3
  31. package/dist/check-BOoxpWqk.mjs +0 -51
  32. package/dist/init-lA9E5pEc.mjs +0 -22
  33. package/dist/migrate-DmOYgmfD.mjs +0 -121
  34. package/dist/package-manager-VCe10bjc.mjs +0 -80
  35. /package/dist/{clack-CDRCHrC-.mjs → clack-C6bVkGxf.mjs} +0 -0
  36. /package/dist/{dep-graph-E-9-eQ2J.mjs → dep-graph-DiLeAhl9.mjs} +0 -0
  37. /package/dist/{names-9VubBmL0.mjs → names-C-TuOPbd.mjs} +0 -0
  38. /package/dist/{semver-DfQyVLM_.mjs → semver-BJzWIuRz.mjs} +0 -0
@@ -1,11 +1,23 @@
1
1
  import { a as __exportAll } from "./logger-C2dEe5Su.mjs";
2
2
  import { access, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
3
+ //#region src/utils/jsonc.ts
4
+ const stringOrCommentRe = /("(?:\\?[^])*?")|(\/\/.*)|(\/\*[^]*?\*\/)/g;
5
+ const stringOrTrailingCommaRe = /("(?:\\?[^])*?")|(,\s*)(?=]|})/g;
6
+ function parseJsonc(text) {
7
+ try {
8
+ return JSON.parse(text);
9
+ } catch {
10
+ return JSON.parse(text.replace(stringOrCommentRe, "$1").replace(stringOrTrailingCommaRe, "$1"));
11
+ }
12
+ }
13
+ //#endregion
3
14
  //#region src/utils/fs.ts
4
15
  var fs_exports = /* @__PURE__ */ __exportAll({
5
16
  ensureDir: () => ensureDir,
6
17
  exists: () => exists,
7
18
  listFiles: () => listFiles,
8
19
  readJson: () => readJson,
20
+ readJsonc: () => readJsonc,
9
21
  readText: () => readText,
10
22
  removeFile: () => removeFile,
11
23
  updateJsonFields: () => updateJsonFields,
@@ -17,6 +29,9 @@ async function readJson(filePath) {
17
29
  const content = await readFile(filePath, "utf-8");
18
30
  return JSON.parse(content);
19
31
  }
32
+ async function readJsonc(filePath) {
33
+ return parseJsonc(await readFile(filePath, "utf-8"));
34
+ }
20
35
  async function writeJson(filePath, data, indent = 2) {
21
36
  await writeFile(filePath, JSON.stringify(data, null, indent) + "\n", "utf-8");
22
37
  }
@@ -78,4 +93,4 @@ async function ensureDir(dir) {
78
93
  await mkdir(dir, { recursive: true });
79
94
  }
80
95
  //#endregion
81
- export { readJson as a, updateJsonFields as c, writeText as d, listFiles as i, updateJsonNestedField as l, exists as n, readText as o, fs_exports as r, removeFile as s, ensureDir as t, writeJson as u };
96
+ export { readJson as a, removeFile as c, writeJson as d, writeText as f, listFiles as i, updateJsonFields as l, exists as n, readJsonc as o, fs_exports as r, readText as s, ensureDir as t, updateJsonNestedField as u };
@@ -1,10 +1,12 @@
1
1
  import { n as log, t as colorize } from "./logger-C2dEe5Su.mjs";
2
- import { t as ensureDir } from "./fs-DYR2XuFE.mjs";
3
- import { a as loadConfig, r as getBumpyDir } from "./config-XZWUL3ma.mjs";
4
- import { t as discoverPackages } from "./workspace-C5ULTyUN.mjs";
5
- import { o as tryRunArgs } from "./shell-Dj7JRD_q.mjs";
6
- import { i as writeBumpFile } from "./bump-file-CCLXMLA8.mjs";
7
- import { n as slugify, t as randomName } from "./names-9VubBmL0.mjs";
2
+ import { t as ensureDir } from "./fs-DnDogVn-.mjs";
3
+ import { a as loadConfig, r as getBumpyDir } from "./config-D7Umr-fT.mjs";
4
+ import { t as discoverPackages } from "./workspace-BHsAPUmC.mjs";
5
+ import { s as tryRunArgs } from "./shell-CY7OD48z.mjs";
6
+ import { i as writeBumpFile } from "./bump-file-Br2bTaWp.mjs";
7
+ import { i as getFilesChangedInCommit, n as getBranchCommits } from "./git-YDedMddc.mjs";
8
+ import { n as slugify, t as randomName } from "./names-C-TuOPbd.mjs";
9
+ import { relative } from "node:path";
8
10
  //#region src/commands/generate.ts
9
11
  const BUMP_MAP = {
10
12
  feat: "minor",
@@ -21,57 +23,62 @@ const BUMP_MAP = {
21
23
  async function generateCommand(rootDir, opts) {
22
24
  const config = await loadConfig(rootDir);
23
25
  const packages = await discoverPackages(rootDir, config);
24
- const from = opts.from || findLastVersionTag(rootDir);
25
- if (!from) {
26
- log.error("Could not detect last version tag. Use --from <ref> to specify.");
27
- process.exit(1);
28
- }
29
- log.step(`Scanning commits from ${colorize(from, "cyan")}...`);
30
- const rawLog = tryRunArgs([
31
- "git",
32
- "log",
33
- `${from}..HEAD`,
34
- "--format=%H%n%s%n%b%n---END---"
35
- ], { cwd: rootDir });
36
- if (!rawLog) {
37
- log.info("No commits found since " + from);
38
- return;
26
+ let commits;
27
+ if (opts.from) {
28
+ log.step(`Scanning commits from ${colorize(opts.from, "cyan")}...`);
29
+ const rawLog = tryRunArgs([
30
+ "git",
31
+ "log",
32
+ `${opts.from}..HEAD`,
33
+ "--format=%H%n%s%n%b%n---END---"
34
+ ], { cwd: rootDir });
35
+ if (!rawLog) {
36
+ log.info("No commits found since " + opts.from);
37
+ return;
38
+ }
39
+ commits = parseGitLog(rawLog);
40
+ } else {
41
+ log.step(`Scanning commits on this branch (vs ${colorize(config.baseBranch, "cyan")})...`);
42
+ commits = getBranchCommits(rootDir, config.baseBranch);
39
43
  }
40
- const conventional = parseGitLog(rawLog).map(parseConventionalCommit).filter((c) => c !== null);
41
- if (conventional.length === 0) {
42
- log.info("No conventional commits found. Commits must follow the format: type(scope): description");
44
+ if (commits.length === 0) {
45
+ log.info("No commits found on this branch.");
43
46
  return;
44
47
  }
45
- log.dim(` Found ${conventional.length} conventional commit(s)`);
48
+ log.dim(` Found ${commits.length} commit(s)`);
46
49
  const scopeMap = buildScopeMap(packages, config);
47
50
  const releaseMap = /* @__PURE__ */ new Map();
48
- for (const commit of conventional) {
49
- const bump = commit.breaking ? "major" : BUMP_MAP[commit.type] || "patch";
50
- let pkgNames = [];
51
- if (commit.scope) {
52
- const resolved = resolveScope(commit.scope, scopeMap, packages);
53
- if (resolved.length > 0) pkgNames = resolved;
54
- else {
55
- log.dim(` Skipping: unknown scope "${commit.scope}" in: ${commit.description}`);
51
+ let ccCount = 0;
52
+ let fileBasedCount = 0;
53
+ for (const commit of commits) {
54
+ const cc = parseConventionalCommit(commit);
55
+ if (cc) {
56
+ ccCount++;
57
+ const bump = cc.breaking ? "major" : BUMP_MAP[cc.type] || "patch";
58
+ let pkgNames = [];
59
+ if (cc.scope) {
60
+ const resolved = resolveScope(cc.scope, scopeMap, packages);
61
+ if (resolved.length > 0) pkgNames = resolved;
62
+ }
63
+ if (pkgNames.length > 0) {
64
+ for (const name of pkgNames) mergeRelease(releaseMap, name, bump, cc.description);
56
65
  continue;
57
66
  }
67
+ const touchedPkgs = mapFilesToPackages(getFilesChangedInCommit(commit.hash, { cwd: rootDir }), packages, rootDir);
68
+ if (touchedPkgs.length > 0) for (const name of touchedPkgs) mergeRelease(releaseMap, name, bump, cc.description);
69
+ else log.dim(` Skipping CC (no matching packages): ${cc.type}: ${cc.description}`);
58
70
  } else {
59
- log.dim(` Skipping (no scope): ${commit.type}: ${commit.description}`);
60
- continue;
61
- }
62
- for (const name of pkgNames) {
63
- const existing = releaseMap.get(name);
64
- if (existing) {
65
- if (bumpPriority(bump) > bumpPriority(existing.type)) existing.type = bump;
66
- existing.messages.push(commit.description);
67
- } else releaseMap.set(name, {
68
- type: bump,
69
- messages: [commit.description]
70
- });
71
+ const touchedPkgs = mapFilesToPackages(getFilesChangedInCommit(commit.hash, { cwd: rootDir }), packages, rootDir);
72
+ if (touchedPkgs.length > 0) {
73
+ fileBasedCount++;
74
+ for (const name of touchedPkgs) mergeRelease(releaseMap, name, "patch", commit.subject);
75
+ } else log.dim(` Skipping (no matching packages): ${commit.subject}`);
71
76
  }
72
77
  }
78
+ if (ccCount > 0) log.dim(` ${ccCount} conventional commit(s)`);
79
+ if (fileBasedCount > 0) log.dim(` ${fileBasedCount} commit(s) detected via changed files`);
73
80
  if (releaseMap.size === 0) {
74
- log.info("No package bumps detected from conventional commits.");
81
+ log.info("No package bumps detected from commits.");
75
82
  return;
76
83
  }
77
84
  const releases = [];
@@ -94,9 +101,29 @@ async function generateCommand(rootDir, opts) {
94
101
  await ensureDir(getBumpyDir(rootDir));
95
102
  const filename = opts.name ? slugify(opts.name) : randomName();
96
103
  await writeBumpFile(rootDir, filename, releases, summaryLines.join("\n"));
97
- log.success(`Created bump file: .bumpy/${filename}.md`);
104
+ log.success(`🐸 Created bump file: .bumpy/${filename}.md`);
98
105
  for (const r of releases) log.dim(` ${r.name}: ${r.type}`);
99
106
  }
107
+ /** Merge a bump into the release map, keeping the highest bump level */
108
+ function mergeRelease(releaseMap, name, bump, message) {
109
+ const existing = releaseMap.get(name);
110
+ if (existing) {
111
+ if (bumpPriority(bump) > bumpPriority(existing.type)) existing.type = bump;
112
+ existing.messages.push(message);
113
+ } else releaseMap.set(name, {
114
+ type: bump,
115
+ messages: [message]
116
+ });
117
+ }
118
+ /** Map file paths to package names based on directory containment */
119
+ function mapFilesToPackages(files, packages, rootDir) {
120
+ const matched = /* @__PURE__ */ new Set();
121
+ for (const file of files) for (const [name, pkg] of packages) {
122
+ const pkgRelDir = relative(rootDir, pkg.dir);
123
+ if (file.startsWith(pkgRelDir + "/")) matched.add(name);
124
+ }
125
+ return [...matched];
126
+ }
100
127
  /** Parse raw git log output into individual commits */
101
128
  function parseGitLog(raw) {
102
129
  const commits = [];
@@ -155,23 +182,5 @@ function resolveScope(scope, scopeMap, packages) {
155
182
  function bumpPriority(type) {
156
183
  return type === "major" ? 2 : type === "minor" ? 1 : 0;
157
184
  }
158
- /** Find the most recent version tag in the repo */
159
- function findLastVersionTag(rootDir) {
160
- return tryRunArgs([
161
- "git",
162
- "describe",
163
- "--tags",
164
- "--abbrev=0",
165
- "--match",
166
- "v*"
167
- ], { cwd: rootDir }) || tryRunArgs([
168
- "git",
169
- "describe",
170
- "--tags",
171
- "--abbrev=0",
172
- "--match",
173
- "*@*"
174
- ], { cwd: rootDir }) || null;
175
- }
176
185
  //#endregion
177
186
  export { generateCommand };
@@ -1,4 +1,4 @@
1
- import { o as tryRunArgs, t as runArgs } from "./shell-Dj7JRD_q.mjs";
1
+ import { n as runArgs, s as tryRunArgs } from "./shell-CY7OD48z.mjs";
2
2
  //#region src/core/git.ts
3
3
  /** Create a git tag */
4
4
  function createTag(tag, opts) {
@@ -63,6 +63,58 @@ function getChangedFiles(rootDir, baseBranch) {
63
63
  if (!diff) return [];
64
64
  return diff.split("\n").filter(Boolean);
65
65
  }
66
+ /** Get commits on the current branch since it diverged from baseBranch */
67
+ function getBranchCommits(rootDir, baseBranch) {
68
+ if (!tryRunArgs([
69
+ "git",
70
+ "rev-parse",
71
+ "--verify",
72
+ `origin/${baseBranch}`
73
+ ], { cwd: rootDir })) tryRunArgs([
74
+ "git",
75
+ "fetch",
76
+ "origin",
77
+ baseBranch,
78
+ "--depth=1"
79
+ ], { cwd: rootDir });
80
+ const rawLog = tryRunArgs([
81
+ "git",
82
+ "log",
83
+ `${tryRunArgs([
84
+ "git",
85
+ "merge-base",
86
+ "HEAD",
87
+ `origin/${baseBranch}`
88
+ ], { cwd: rootDir }) || `origin/${baseBranch}`}..HEAD`,
89
+ "--format=%H%n%s%n%b%n---END---"
90
+ ], { cwd: rootDir });
91
+ if (!rawLog) return [];
92
+ const commits = [];
93
+ const entries = rawLog.split("---END---").filter((e) => e.trim());
94
+ for (const entry of entries) {
95
+ const lines = entry.trim().split("\n");
96
+ if (lines.length < 2) continue;
97
+ commits.push({
98
+ hash: lines[0].trim(),
99
+ subject: lines[1].trim(),
100
+ body: lines.slice(2).join("\n").trim()
101
+ });
102
+ }
103
+ return commits;
104
+ }
105
+ /** Get files changed in a specific commit */
106
+ function getFilesChangedInCommit(hash, opts) {
107
+ const result = tryRunArgs([
108
+ "git",
109
+ "diff-tree",
110
+ "--no-commit-id",
111
+ "--name-only",
112
+ "-r",
113
+ hash
114
+ ], opts);
115
+ if (!result) return [];
116
+ return result.split("\n").filter(Boolean);
117
+ }
66
118
  /** Get all tags matching a pattern */
67
119
  function listTags(pattern, opts) {
68
120
  const result = tryRunArgs([
@@ -75,4 +127,4 @@ function listTags(pattern, opts) {
75
127
  return result.split("\n").filter(Boolean);
76
128
  }
77
129
  //#endregion
78
- export { pushWithTags as a, listTags as i, getChangedFiles as n, tagExists as o, hasUncommittedChanges as r, createTag as t };
130
+ export { hasUncommittedChanges as a, tagExists as c, getFilesChangedInCommit as i, getBranchCommits as n, listTags as o, getChangedFiles as r, pushWithTags as s, createTag as t };
package/dist/index.d.mts CHANGED
@@ -32,10 +32,19 @@ interface PublishConfig {
32
32
  interface BumpyConfig {
33
33
  baseBranch: string;
34
34
  access: 'public' | 'restricted';
35
- commit: boolean;
36
- changelog: string | [string, Record<string, unknown>];
35
+ /**
36
+ * Customize the commit message used when versioning.
37
+ * A string starting with "./" is treated as a path to a module that exports
38
+ * a function receiving the release plan and returning a message string.
39
+ * Any other string is used as a static commit message.
40
+ * Omit to use the default: "Version packages\n\npkg@version..."
41
+ */
42
+ versionCommitMessage?: string;
43
+ changelog: false | string | [string, Record<string, unknown>];
37
44
  fixed: string[][];
38
45
  linked: string[][];
46
+ /** Glob patterns to filter which changed files count toward marking a package as changed */
47
+ changedFilePatterns: string[];
39
48
  /** Package names/globs to exclude from version management */
40
49
  ignore: string[];
41
50
  /** Package names/globs to explicitly include (overrides private + ignore) */
@@ -90,6 +99,8 @@ interface PackageConfig {
90
99
  skipNpmPublish?: boolean;
91
100
  /** Command to check if a version is already published. Should output the published version string. */
92
101
  checkPublished?: string;
102
+ /** Glob patterns to filter which changed files count toward marking this package as changed */
103
+ changedFilePatterns?: string[];
93
104
  dependencyBumpRules?: Partial<Record<DepType, DependencyBumpRule | false>>;
94
105
  cascadeTo?: Record<string, DependencyBumpRule>;
95
106
  }
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- import { a as loadConfig, c as BUMP_LEVELS, d as DEFAULT_PUBLISH_CONFIG, f as DEP_TYPES, h as maxBump, l as DEFAULT_BUMP_RULES, m as hasCascade, n as findRoot, p as bumpLevel, r as getBumpyDir, s as matchGlob, u as DEFAULT_CONFIG } from "./config-XZWUL3ma.mjs";
2
- import { t as discoverPackages } from "./workspace-C5ULTyUN.mjs";
3
- import { t as DependencyGraph } from "./dep-graph-E-9-eQ2J.mjs";
4
- import { i as writeBumpFile, n as parseBumpFile, r as readBumpFiles } from "./bump-file-CCLXMLA8.mjs";
5
- import { n as satisfies, r as stripProtocol, t as bumpVersion } from "./semver-DfQyVLM_.mjs";
6
- import { t as assembleReleasePlan } from "./release-plan-Bi5QNSEo.mjs";
7
- import { a as prependToChangelog, i as loadFormatter, n as defaultFormatter, r as generateChangelogEntry, t as applyReleasePlan } from "./apply-release-plan-CczGWJTk.mjs";
8
- import { t as publishPackages } from "./publish-pipeline-BwBuKCIk.mjs";
1
+ import { a as loadConfig, c as BUMP_LEVELS, d as DEFAULT_PUBLISH_CONFIG, f as DEP_TYPES, h as maxBump, l as DEFAULT_BUMP_RULES, m as hasCascade, n as findRoot, p as bumpLevel, r as getBumpyDir, s as matchGlob, u as DEFAULT_CONFIG } from "./config-D7Umr-fT.mjs";
2
+ import { t as discoverPackages } from "./workspace-BHsAPUmC.mjs";
3
+ import { t as DependencyGraph } from "./dep-graph-DiLeAhl9.mjs";
4
+ import { i as writeBumpFile, n as parseBumpFile, r as readBumpFiles } from "./bump-file-Br2bTaWp.mjs";
5
+ import { n as satisfies, r as stripProtocol, t as bumpVersion } from "./semver-BJzWIuRz.mjs";
6
+ import { t as assembleReleasePlan } from "./release-plan-CNOuSI-d.mjs";
7
+ import { a as prependToChangelog, i as loadFormatter, n as defaultFormatter, r as generateChangelogEntry, t as applyReleasePlan } from "./apply-release-plan-CPzu6JcF.mjs";
8
+ import { t as publishPackages } from "./publish-pipeline-BFt96o_h.mjs";
9
9
  export { BUMP_LEVELS, DEFAULT_BUMP_RULES, DEFAULT_CONFIG, DEFAULT_PUBLISH_CONFIG, DEP_TYPES, DependencyGraph, applyReleasePlan, assembleReleasePlan, bumpLevel, bumpVersion, defaultFormatter, discoverPackages, findRoot, generateChangelogEntry, getBumpyDir, hasCascade, loadConfig, loadFormatter, matchGlob, maxBump, parseBumpFile, prependToChangelog, publishPackages, readBumpFiles, satisfies, stripProtocol, writeBumpFile };
@@ -0,0 +1,196 @@
1
+ import { n as log, o as __toESM, r as require_picocolors } from "./logger-C2dEe5Su.mjs";
2
+ import { a as readJson, d as writeJson, f as writeText, i as listFiles, n as exists, s as readText, t as ensureDir } from "./fs-DnDogVn-.mjs";
3
+ import { t as detectPackageManager } from "./package-manager-ByJ0wKYh.mjs";
4
+ import { t as run } from "./shell-CY7OD48z.mjs";
5
+ import { c as ot, o as gt, s as mt, t as unwrap } from "./clack-C6bVkGxf.mjs";
6
+ import { resolve } from "node:path";
7
+ import { readdir, rename, rm } from "node:fs/promises";
8
+ //#region ../../.bumpy/README.md
9
+ var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
10
+ var README_default = "# 🐸 Bumpy\n\nThis directory is used by [bumpy](https://bumpy.varlock.dev) to manage versioning and changelogs.\n\nBumpy is a modern versioning tool for JavaScript/TypeScript projects (monorepos and single packages). It uses **bump files** — small markdown files in this directory — to declare pending version changes. These files are consumed during the release process to compute version bumps, update changelogs, and publish packages.\n\n## How it works\n\n1. When you make a change that should trigger a release, create a bump file (typically one per PR)\n2. Bump files accumulate on your main branch until you're ready to release\n3. At release time, bumpy merges all pending bumps into a release plan, updates versions and changelogs, and publishes packages\n\n## Creating bump files\n\n### Interactive\n\n```bash\nbunx bumpy add\n```\n\n### Non-interactive (useful for AI-assisted development)\n\n```bash\nbunx bumpy add --packages \"package-name:minor,other-package:patch\" --message \"Description of changes\" --name \"my-change\"\n```\n\n### By hand\n\nCreate a `.md` file in this directory with YAML frontmatter mapping package names to bump levels (`major`, `minor`, `patch`, or `none`), and a markdown body for the changelog entry:\n\n```markdown\n---\n'package-name': minor\n---\n\nAdded a new feature.\n```\n\n### From conventional commits\n\n```bash\nbunx bumpy generate\n```\n\n### Empty bump files\n\nFor PRs that intentionally don't need a release (docs, CI, etc.):\n\n```bash\nbunx bumpy add --empty --name \"docs-update\"\n```\n\n## Keeping bump files up to date\n\nAs a PR evolves, make sure its bump file stays in sync. If the scope of changes grows (e.g., a patch becomes a new feature), update the bump level and description to match. Reviewers and AI assistants should treat the bump file as part of the PR — just like tests and docs.\n\n## Files in this directory\n\n- `_config.json` — bumpy configuration\n- `README.md` — this file\n- `*.md` (other than README.md) — pending bump files\n\n📖 Full documentation: https://bumpy.varlock.dev\n";
11
+ //#endregion
12
+ //#region src/commands/init.ts
13
+ const PM_RUNNER = {
14
+ bun: "bunx bumpy",
15
+ pnpm: "pnpm bumpy",
16
+ yarn: "yarn bumpy",
17
+ npm: "npx bumpy"
18
+ };
19
+ const PM_ADD_DEV = {
20
+ bun: "bun add -d",
21
+ pnpm: "pnpm add -Dw",
22
+ yarn: "yarn add -D -W",
23
+ npm: "npm install -D"
24
+ };
25
+ const PM_REMOVE = {
26
+ bun: "bun remove",
27
+ pnpm: "pnpm remove -w",
28
+ yarn: "yarn remove -W",
29
+ npm: "npm uninstall"
30
+ };
31
+ async function initCommand(rootDir, opts = {}) {
32
+ const bumpyDir = resolve(rootDir, ".bumpy");
33
+ const changesetDir = resolve(rootDir, ".changeset");
34
+ const hasChangeset = await exists(changesetDir);
35
+ if (await exists(resolve(bumpyDir, "_config.json"))) {
36
+ log.info("🐸 Detected .bumpy/ directory - looks like we're ready to go!");
37
+ return;
38
+ }
39
+ const pm = await detectPackageManager(rootDir);
40
+ if (!opts.force) mt(import_picocolors.default.bgCyan(import_picocolors.default.black(" bumpy init ")));
41
+ if (hasChangeset) {
42
+ log.step("🦋 Detected .changeset/ directory — migrating to .bumpy/ 🐸");
43
+ await rename(changesetDir, bumpyDir);
44
+ log.dim(" Renamed .changeset/ → .bumpy/");
45
+ const oldConfigPath = resolve(bumpyDir, "config.json");
46
+ if (await exists(oldConfigPath)) {
47
+ const bumpyConfig = migrateChangesetConfig(await readJson(oldConfigPath));
48
+ await writeJson(resolve(bumpyDir, "_config.json"), bumpyConfig);
49
+ await rm(oldConfigPath);
50
+ log.dim(" Migrated config.json → _config.json");
51
+ const migratedFields = Object.keys(bumpyConfig).filter((k) => k !== "$schema");
52
+ if (migratedFields.length > 0) log.dim(" Migrated fields: " + migratedFields.join(", "));
53
+ } else await writeJson(resolve(bumpyDir, "_config.json"), makeDefaultConfig());
54
+ const readmeContent = README_default.replaceAll("bunx bumpy", PM_RUNNER[pm] || "npx bumpy");
55
+ await writeText(resolve(bumpyDir, "README.md"), readmeContent);
56
+ log.dim(" Replaced README.md");
57
+ const pendingFiles = (await readdir(bumpyDir)).filter((f) => f.endsWith(".md") && f !== "README.md");
58
+ if (pendingFiles.length > 0) log.dim(` Kept ${pendingFiles.length} pending bump file(s)`);
59
+ if (await isPackageInstalled(rootDir, "@changesets/cli")) {
60
+ if (opts.force) await uninstallPackage(pm, "@changesets/cli", rootDir);
61
+ else if (unwrap(await ot({
62
+ message: "Uninstall @changesets/cli?",
63
+ initialValue: true
64
+ }))) await uninstallPackage(pm, "@changesets/cli", rootDir);
65
+ }
66
+ await warnChangesetWorkflows(rootDir, pm);
67
+ } else {
68
+ await ensureDir(bumpyDir);
69
+ await writeJson(resolve(bumpyDir, "_config.json"), makeDefaultConfig());
70
+ const readmeContent = README_default.replaceAll("bunx bumpy", PM_RUNNER[pm] || "npx bumpy");
71
+ await writeText(resolve(bumpyDir, "README.md"), readmeContent);
72
+ log.success("Initialized .bumpy/ directory");
73
+ log.dim(" Created .bumpy/_config.json");
74
+ log.dim(" Created .bumpy/README.md");
75
+ }
76
+ if (!await isPackageInstalled(rootDir, "@varlock/bumpy")) {
77
+ if (opts.force) await installPackage(pm, "@varlock/bumpy", rootDir);
78
+ else if (unwrap(await ot({
79
+ message: "Install @varlock/bumpy as a dev dependency?",
80
+ initialValue: true
81
+ }))) await installPackage(pm, "@varlock/bumpy", rootDir);
82
+ }
83
+ if (!opts.force) gt(import_picocolors.default.green("bumpy is ready!"));
84
+ else if (hasChangeset) log.success("Migration complete!");
85
+ }
86
+ function makeDefaultConfig() {
87
+ return {
88
+ $schema: "../node_modules/@varlock/bumpy/config-schema.json",
89
+ baseBranch: "main",
90
+ changelog: "default"
91
+ };
92
+ }
93
+ function migrateChangesetConfig(csConfig) {
94
+ const bumpyConfig = makeDefaultConfig();
95
+ for (const field of [
96
+ "baseBranch",
97
+ "access",
98
+ "fixed",
99
+ "linked",
100
+ "ignore",
101
+ "updateInternalDependencies",
102
+ "privatePackages"
103
+ ]) if (csConfig[field] !== void 0) bumpyConfig[field] = csConfig[field];
104
+ return bumpyConfig;
105
+ }
106
+ async function isPackageInstalled(rootDir, pkgName) {
107
+ try {
108
+ const pkg = await readJson(resolve(rootDir, "package.json"));
109
+ return !!(pkg.devDependencies?.[pkgName] || pkg.dependencies?.[pkgName]);
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ async function installPackage(pm, pkgName, rootDir) {
115
+ const cmd = `${PM_ADD_DEV[pm] || "npm install -D"} ${pkgName}`;
116
+ log.step(`Installing ${pkgName}...`);
117
+ try {
118
+ run(cmd, { cwd: rootDir });
119
+ log.dim(` ${cmd}`);
120
+ } catch (err) {
121
+ log.warn(`Failed to install ${pkgName}: ${err instanceof Error ? err.message : err}`);
122
+ log.dim(` Run manually: ${cmd}`);
123
+ }
124
+ }
125
+ async function uninstallPackage(pm, pkgName, rootDir) {
126
+ const cmd = `${PM_REMOVE[pm] || "npm uninstall"} ${pkgName}`;
127
+ log.step(`Uninstalling ${pkgName}...`);
128
+ try {
129
+ run(cmd, { cwd: rootDir });
130
+ log.dim(` ${cmd}`);
131
+ } catch (err) {
132
+ log.warn(`Failed to uninstall ${pkgName}: ${err instanceof Error ? err.message : err}`);
133
+ log.dim(` Run manually: ${cmd}`);
134
+ }
135
+ }
136
+ /** Patterns to detect in workflow files, with suggested replacements */
137
+ const CHANGESET_PATTERNS = [
138
+ {
139
+ pattern: /changesets\/action/,
140
+ replacement: "see https://bumpy.varlock.dev/ci for bumpy CI setup"
141
+ },
142
+ {
143
+ pattern: /changeset publish/,
144
+ replacement: "bumpy publish"
145
+ },
146
+ {
147
+ pattern: /changeset version/,
148
+ replacement: "bumpy version"
149
+ },
150
+ {
151
+ pattern: /changeset status/,
152
+ replacement: "bumpy status"
153
+ },
154
+ {
155
+ pattern: /@changesets\//,
156
+ replacement: "replace with @varlock/bumpy"
157
+ }
158
+ ];
159
+ async function warnChangesetWorkflows(rootDir, pm) {
160
+ const workflowDir = resolve(rootDir, ".github", "workflows");
161
+ if (!await exists(workflowDir)) return;
162
+ const yamlFiles = (await listFiles(workflowDir)).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
163
+ if (yamlFiles.length === 0) return;
164
+ const runner = PM_RUNNER[pm] || "npx bumpy";
165
+ const hits = [];
166
+ for (const file of yamlFiles) {
167
+ const lines = (await readText(resolve(workflowDir, file))).split("\n");
168
+ const fileMatches = [];
169
+ for (let i = 0; i < lines.length; i++) {
170
+ const line = lines[i];
171
+ for (const { pattern, replacement } of CHANGESET_PATTERNS) {
172
+ const match = line.match(pattern);
173
+ if (match) fileMatches.push({
174
+ line: i + 1,
175
+ found: match[0],
176
+ suggestion: replacement
177
+ });
178
+ }
179
+ }
180
+ if (fileMatches.length > 0) hits.push({
181
+ file,
182
+ matches: fileMatches
183
+ });
184
+ }
185
+ if (hits.length === 0) return;
186
+ console.log();
187
+ log.warn("Found changeset references in GitHub workflows:");
188
+ for (const { file, matches } of hits) {
189
+ log.dim(` .github/workflows/${file}`);
190
+ for (const { line, found, suggestion } of matches) log.dim(` L${line}: ${import_picocolors.default.red(found)} → ${import_picocolors.default.green(suggestion)}`);
191
+ }
192
+ console.log();
193
+ log.dim(` Run ${import_picocolors.default.cyan(`${runner} ci setup`)} for help configuring CI workflows.`);
194
+ }
195
+ //#endregion
196
+ export { initCommand };
@@ -1,3 +1,5 @@
1
+ import { a as readJson, n as exists, s as readText } from "./fs-DnDogVn-.mjs";
2
+ import { resolve } from "node:path";
1
3
  //#region ../../node_modules/.bun/js-yaml@4.1.1/node_modules/js-yaml/dist/js-yaml.mjs
2
4
  /*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
3
5
  function isNothing(subject) {
@@ -2028,4 +2030,80 @@ var jsYaml = {
2028
2030
  safeDump: renamed("safeDump", "dump")
2029
2031
  };
2030
2032
  //#endregion
2031
- export { jsYaml as t };
2033
+ //#region src/utils/package-manager.ts
2034
+ /** Detect the package manager, extract workspace globs, and load catalogs */
2035
+ async function detectWorkspaces(rootDir) {
2036
+ const pm = await detectPackageManager(rootDir);
2037
+ return {
2038
+ packageManager: pm,
2039
+ globs: await getWorkspaceGlobs(rootDir, pm),
2040
+ catalogs: await loadCatalogs(rootDir, pm)
2041
+ };
2042
+ }
2043
+ async function detectPackageManager(rootDir) {
2044
+ if (await exists(resolve(rootDir, "bun.lock")) || await exists(resolve(rootDir, "bun.lockb"))) return "bun";
2045
+ if (await exists(resolve(rootDir, "pnpm-lock.yaml"))) return "pnpm";
2046
+ if (await exists(resolve(rootDir, "yarn.lock"))) return "yarn";
2047
+ try {
2048
+ const pkg = await readJson(resolve(rootDir, "package.json"));
2049
+ if (typeof pkg.packageManager === "string") {
2050
+ const name = pkg.packageManager.split("@")[0];
2051
+ if (name === "pnpm" || name === "yarn" || name === "bun") return name;
2052
+ }
2053
+ } catch {}
2054
+ return "npm";
2055
+ }
2056
+ async function getWorkspaceGlobs(rootDir, pm) {
2057
+ if (pm === "pnpm") {
2058
+ const wsFile = resolve(rootDir, "pnpm-workspace.yaml");
2059
+ if (await exists(wsFile)) {
2060
+ const content = await readText(wsFile);
2061
+ const parsed = jsYaml.load(content);
2062
+ if (parsed?.packages) return parsed.packages;
2063
+ }
2064
+ }
2065
+ try {
2066
+ const workspaces = (await readJson(resolve(rootDir, "package.json"))).workspaces;
2067
+ if (Array.isArray(workspaces)) return workspaces;
2068
+ if (workspaces && typeof workspaces === "object" && "packages" in workspaces) {
2069
+ const pkgs = workspaces.packages;
2070
+ if (Array.isArray(pkgs)) return pkgs;
2071
+ }
2072
+ } catch {}
2073
+ return [];
2074
+ }
2075
+ /** Load catalog definitions from pnpm-workspace.yaml or root package.json */
2076
+ async function loadCatalogs(rootDir, pm) {
2077
+ const catalogs = /* @__PURE__ */ new Map();
2078
+ if (pm === "pnpm") {
2079
+ const wsFile = resolve(rootDir, "pnpm-workspace.yaml");
2080
+ if (await exists(wsFile)) {
2081
+ const content = await readText(wsFile);
2082
+ const parsed = jsYaml.load(content);
2083
+ if (parsed?.catalog) catalogs.set("", parsed.catalog);
2084
+ if (parsed?.catalogs) for (const [name, deps] of Object.entries(parsed.catalogs)) catalogs.set(name, deps);
2085
+ }
2086
+ }
2087
+ try {
2088
+ const pkg = await readJson(resolve(rootDir, "package.json"));
2089
+ if (pkg.catalog && typeof pkg.catalog === "object") catalogs.set("", pkg.catalog);
2090
+ if (pkg.catalogs && typeof pkg.catalogs === "object") for (const [name, deps] of Object.entries(pkg.catalogs)) catalogs.set(name, deps);
2091
+ const workspaces = pkg.workspaces;
2092
+ if (workspaces && typeof workspaces === "object" && !Array.isArray(workspaces)) {
2093
+ const ws = workspaces;
2094
+ if (ws.catalog && typeof ws.catalog === "object") catalogs.set("", ws.catalog);
2095
+ if (ws.catalogs && typeof ws.catalogs === "object") for (const [name, deps] of Object.entries(ws.catalogs)) catalogs.set(name, deps);
2096
+ }
2097
+ } catch {}
2098
+ return catalogs;
2099
+ }
2100
+ /** Resolve a specific dependency's catalog: reference */
2101
+ function resolveCatalogDep(depName, range, catalogs) {
2102
+ if (!range.startsWith("catalog:")) return null;
2103
+ const catalogName = range.slice(8).trim() || "";
2104
+ const catalog = catalogs.get(catalogName);
2105
+ if (!catalog) return null;
2106
+ return catalog[depName] ?? null;
2107
+ }
2108
+ //#endregion
2109
+ export { jsYaml as i, detectWorkspaces as n, resolveCatalogDep as r, detectPackageManager as t };