@varlock/bumpy 0.0.0 → 0.0.1

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.
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "bumpy",
3
+ "version": "0.0.1",
4
+ "description": "AI-assisted changeset creation for bumpy monorepo versioning",
5
+ "author": {
6
+ "name": "DMNO",
7
+ "url": "https://github.com/dmno-dev"
8
+ },
9
+ "repository": "https://github.com/dmno-dev/bumpy",
10
+ "license": "MIT",
11
+ "keywords": ["monorepo", "versioning", "changelog", "changesets"],
12
+ "skills": "./skills/"
13
+ }
@@ -0,0 +1,131 @@
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 };
@@ -0,0 +1,82 @@
1
+ import { n as log } from "./logger-ZqggsyGZ.mjs";
2
+ import { l as writeText, n as exists, t as ensureDir } from "./fs-DbNNEyzq.mjs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/commands/ai.ts
7
+ const SUPPORTED_TARGETS = [
8
+ "opencode",
9
+ "cursor",
10
+ "codex"
11
+ ];
12
+ async function aiSetupCommand(rootDir, opts) {
13
+ const target = opts.target;
14
+ if (!target) {
15
+ 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
+ process.exit(1);
18
+ }
19
+ if (!SUPPORTED_TARGETS.includes(target)) {
20
+ log.error(`Unknown target: "${target}". Supported: ${SUPPORTED_TARGETS.join(", ")}`);
21
+ process.exit(1);
22
+ }
23
+ const promptContent = await loadPromptTemplate();
24
+ switch (target) {
25
+ case "opencode":
26
+ await setupOpenCode(rootDir, promptContent);
27
+ break;
28
+ case "cursor":
29
+ await setupCursor(rootDir, promptContent);
30
+ break;
31
+ case "codex":
32
+ await setupCodex(rootDir, promptContent);
33
+ break;
34
+ }
35
+ }
36
+ async function loadPromptTemplate() {
37
+ return (await readFile(resolve(dirname(fileURLToPath(import.meta.url)), "../../skills/add-change/SKILL.md"), "utf-8")).replace(/^---\n[\s\S]*?\n---\n\n?/, "");
38
+ }
39
+ /** Install as an OpenCode custom command */
40
+ async function setupOpenCode(rootDir, promptContent) {
41
+ const commandsDir = resolve(rootDir, ".opencode", "commands");
42
+ const targetPath = resolve(commandsDir, "add-bumpy-change.md");
43
+ await ensureDir(commandsDir);
44
+ if (await exists(targetPath)) log.warn(".opencode/commands/add-bumpy-change.md already exists — overwriting");
45
+ await writeText(targetPath, `---
46
+ description: Create a bumpy changeset to track package version bumps
47
+ ---
48
+
49
+ ${promptContent}`);
50
+ log.success("Installed OpenCode command");
51
+ log.dim(" Created .opencode/commands/add-bumpy-change.md");
52
+ log.dim(" Usage: type /add-bumpy-change in OpenCode");
53
+ }
54
+ /** Install as a Cursor rule */
55
+ async function setupCursor(rootDir, promptContent) {
56
+ const rulesDir = resolve(rootDir, ".cursor", "rules");
57
+ const targetPath = resolve(rulesDir, "add-bumpy-change.mdc");
58
+ await ensureDir(rulesDir);
59
+ if (await exists(targetPath)) log.warn(".cursor/rules/add-bumpy-change.mdc already exists — overwriting");
60
+ await writeText(targetPath, `---
61
+ description: Create a bumpy changeset to track package version bumps
62
+ globs:
63
+ alwaysApply: false
64
+ ---
65
+
66
+ ${promptContent}`);
67
+ log.success("Installed Cursor rule");
68
+ log.dim(" Created .cursor/rules/add-bumpy-change.mdc");
69
+ log.dim(" The rule will be suggested when relevant, or you can reference it manually");
70
+ }
71
+ /** Install as a Codex instruction */
72
+ async function setupCodex(rootDir, promptContent) {
73
+ const targetPath = resolve(rootDir, ".codex", "add-bumpy-change.md");
74
+ await ensureDir(resolve(rootDir, ".codex"));
75
+ if (await exists(targetPath)) log.warn(".codex/add-bumpy-change.md already exists — overwriting");
76
+ await writeText(targetPath, promptContent);
77
+ log.success("Installed Codex instruction");
78
+ log.dim(" Created .codex/add-bumpy-change.md");
79
+ log.dim(" Reference this file in your AGENTS.md or pass it as context");
80
+ }
81
+ //#endregion
82
+ export { aiSetupCommand };
@@ -0,0 +1,132 @@
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";
5
+ //#region src/core/changelog.ts
6
+ /** Default formatter — version heading, date, bullet points */
7
+ const defaultFormatter = (ctx) => {
8
+ const { release, changesets, date } = ctx;
9
+ const lines = [];
10
+ lines.push(`## ${release.newVersion}`);
11
+ lines.push("");
12
+ lines.push(`_${date}_`);
13
+ 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");
18
+ lines.push(`- ${summaryLines[0]}`);
19
+ for (let i = 1; i < summaryLines.length; i++) if (summaryLines[i].trim()) lines.push(` ${summaryLines[i]}`);
20
+ }
21
+ }
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");
24
+ lines.push("");
25
+ return lines.join("\n");
26
+ };
27
+ const BUILTIN_FORMATTERS = {
28
+ default: defaultFormatter,
29
+ github: async () => {
30
+ const { createGithubFormatter } = await import("./changelog-github-n-3zV1p9.mjs");
31
+ return createGithubFormatter();
32
+ }
33
+ };
34
+ /**
35
+ * Load a changelog formatter from config.
36
+ * Supports: "default", "./path/to/formatter.ts", or a module name.
37
+ */
38
+ async function loadFormatter(changelog, rootDir) {
39
+ const [name, options] = Array.isArray(changelog) ? changelog : [changelog, {}];
40
+ if (typeof name === "string" && BUILTIN_FORMATTERS[name]) {
41
+ const builtin = BUILTIN_FORMATTERS[name];
42
+ if (typeof builtin === "function" && builtin.length === 0) return builtin();
43
+ return builtin;
44
+ }
45
+ if (name === "github") {
46
+ const { createGithubFormatter } = await import("./changelog-github-n-3zV1p9.mjs");
47
+ return createGithubFormatter(options);
48
+ }
49
+ if (typeof name === "string") try {
50
+ const mod = await (name.startsWith(".") ? import(resolve(rootDir, name)) : import(name));
51
+ const exported = mod.default || mod.changelogFormatter;
52
+ if (typeof exported === "function") {
53
+ const result = exported(options);
54
+ if (typeof result === "function") return result;
55
+ return exported;
56
+ }
57
+ throw new Error(`Changelog module "${name}" does not export a function`);
58
+ } catch (err) {
59
+ log.warn(`Failed to load changelog formatter "${name}": ${err instanceof Error ? err.message : err}`);
60
+ log.warn("Falling back to default formatter");
61
+ return defaultFormatter;
62
+ }
63
+ return defaultFormatter;
64
+ }
65
+ /** Generate a changelog entry using the configured formatter */
66
+ async function generateChangelogEntry(release, changesets, formatter = defaultFormatter, date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]) {
67
+ return formatter({
68
+ release,
69
+ changesets,
70
+ date
71
+ });
72
+ }
73
+ /** Prepend a new entry to an existing CHANGELOG.md content */
74
+ function prependToChangelog(existingContent, newEntry) {
75
+ const headerMatch = existingContent.match(/^# /m);
76
+ if (headerMatch && headerMatch.index !== void 0) {
77
+ const afterTitle = existingContent.indexOf("\n##");
78
+ if (afterTitle !== -1) return existingContent.slice(0, afterTitle + 1) + "\n" + newEntry + existingContent.slice(afterTitle + 1);
79
+ return existingContent.trimEnd() + "\n\n" + newEntry;
80
+ }
81
+ return "# Changelog\n\n" + newEntry;
82
+ }
83
+ //#endregion
84
+ //#region src/core/apply-release-plan.ts
85
+ /** Apply the release plan: bump versions, update changelogs, delete changesets */
86
+ async function applyReleasePlan(releasePlan, packages, rootDir, config) {
87
+ const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
88
+ const formatter = await loadFormatter(config.changelog, rootDir);
89
+ for (const release of releasePlan.releases) {
90
+ const pkgJsonPath = resolve(packages.get(release.name).dir, "package.json");
91
+ const pkgJson = await readJson(pkgJsonPath);
92
+ pkgJson.version = release.newVersion;
93
+ for (const depField of [
94
+ "dependencies",
95
+ "devDependencies",
96
+ "peerDependencies",
97
+ "optionalDependencies"
98
+ ]) {
99
+ const deps = pkgJson[depField];
100
+ if (!deps) continue;
101
+ for (const [depName, range] of Object.entries(deps)) {
102
+ const depRelease = releaseMap.get(depName);
103
+ if (!depRelease) continue;
104
+ deps[depName] = updateRange(range, depRelease.newVersion);
105
+ }
106
+ }
107
+ await writeJson(pkgJsonPath, pkgJson);
108
+ }
109
+ for (const release of releasePlan.releases) {
110
+ const changelogPath = resolve(packages.get(release.name).dir, "CHANGELOG.md");
111
+ const entry = await generateChangelogEntry(release, releasePlan.changesets, formatter);
112
+ let existingContent = "";
113
+ if (await exists(changelogPath)) existingContent = await readText(changelogPath);
114
+ await writeText(changelogPath, prependToChangelog(existingContent, entry));
115
+ }
116
+ await deleteChangesets(rootDir, releasePlan.changesets.map((cs) => cs.id));
117
+ }
118
+ /** Update a version range to include a new version, preserving the range prefix */
119
+ function updateRange(range, newVersion) {
120
+ let protocol = "";
121
+ let cleanRange = range;
122
+ const protoMatch = range.match(/^(workspace:|catalog:)/);
123
+ if (protoMatch) {
124
+ protocol = protoMatch[1];
125
+ cleanRange = range.slice(protocol.length);
126
+ }
127
+ const prefix = cleanRange.match(/^(\^|~|>=|>|<=|<|=)?/)?.[1] ?? "^";
128
+ if (cleanRange === "*" || cleanRange === "") return range;
129
+ return `${protocol}${prefix}${newVersion}`;
130
+ }
131
+ //#endregion
132
+ export { prependToChangelog as a, loadFormatter as i, defaultFormatter as n, generateChangelogEntry as r, applyReleasePlan as t };
@@ -0,0 +1,59 @@
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 };
@@ -0,0 +1,75 @@
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 };
@@ -0,0 +1,57 @@
1
+ import { n as log, t as colorize } from "./logger-ZqggsyGZ.mjs";
2
+ import { a as loadConfig } from "./config-CJ2orhTL.mjs";
3
+ import { n as discoverWorkspace } from "./workspace-mVjawG8g.mjs";
4
+ import { r as readChangesets } from "./changeset-ClCYsChu.mjs";
5
+ import { i as tryRun } from "./shell-DPlltpzb.mjs";
6
+ import { relative } from "node:path";
7
+ //#region src/commands/check.ts
8
+ /**
9
+ * Local check: detect which packages have changed on this branch
10
+ * and verify they have corresponding changesets.
11
+ * Designed for pre-push hooks — no GitHub API needed.
12
+ */
13
+ async function checkCommand(rootDir) {
14
+ const config = await loadConfig(rootDir);
15
+ const { packages } = await discoverWorkspace(rootDir, config);
16
+ const changesets = await readChangesets(rootDir);
17
+ const coveredPackages = /* @__PURE__ */ new Set();
18
+ for (const cs of changesets) for (const release of cs.releases) coveredPackages.add(release.name);
19
+ const baseBranch = config.baseBranch;
20
+ const changedFiles = getChangedFiles(rootDir, baseBranch);
21
+ if (changedFiles.length === 0) {
22
+ log.info("No changed files detected.");
23
+ return;
24
+ }
25
+ const changedPackages = findChangedPackages(changedFiles, packages, rootDir);
26
+ if (changedPackages.length === 0) {
27
+ log.info("No managed packages have changed.");
28
+ return;
29
+ }
30
+ const missing = changedPackages.filter((name) => !coveredPackages.has(name));
31
+ if (missing.length === 0) {
32
+ log.success(`All ${changedPackages.length} changed package(s) have changesets.`);
33
+ return;
34
+ }
35
+ log.warn(`${missing.length} changed package(s) missing changesets:\n`);
36
+ for (const name of missing) console.log(` ${colorize(name, "yellow")}`);
37
+ console.log();
38
+ log.dim("Run `bumpy add` to create a changeset, or `bumpy add --empty` if no release is needed.");
39
+ process.exit(1);
40
+ }
41
+ /** Get files changed on this branch compared to the base branch */
42
+ function getChangedFiles(rootDir, baseBranch) {
43
+ const diff = tryRun(`git diff --name-only ${tryRun(`git merge-base HEAD origin/${baseBranch}`, { cwd: rootDir }) || `origin/${baseBranch}`}`, { cwd: rootDir });
44
+ if (!diff) return [];
45
+ return diff.split("\n").filter(Boolean);
46
+ }
47
+ /** Map changed files to the packages they belong to */
48
+ function findChangedPackages(changedFiles, packages, rootDir) {
49
+ const changed = /* @__PURE__ */ new Set();
50
+ for (const file of changedFiles) for (const [name, pkg] of packages) {
51
+ const pkgRelDir = relative(rootDir, pkg.dir);
52
+ if (file.startsWith(pkgRelDir + "/")) changed.add(name);
53
+ }
54
+ return [...changed];
55
+ }
56
+ //#endregion
57
+ export { checkCommand };