@varlock/bumpy 0.0.0 → 0.0.2

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,251 @@
1
+ import { n as log, t as colorize } from "./logger-C2dEe5Su.mjs";
2
+ import { a as loadConfig } from "./config-BkwIEaQg.mjs";
3
+ import { n as detectWorkspaces } from "./package-manager-DcI5TdDE.mjs";
4
+ import { n as discoverWorkspace } from "./workspace-CxEKakDm.mjs";
5
+ import { t as DependencyGraph } from "./dep-graph-E-9-eQ2J.mjs";
6
+ import { n as runArgsAsync, o as tryRunArgs } from "./shell-Dj7JRD_q.mjs";
7
+ import { a as pushWithTags, i as listTags, r as hasUncommittedChanges } from "./git-CGHVXXKw.mjs";
8
+ import { t as publishPackages } from "./publish-pipeline-ChnqW8nR.mjs";
9
+ //#region src/core/github-release.ts
10
+ /** Get the current HEAD commit SHA */
11
+ function getHeadSha(rootDir) {
12
+ return tryRunArgs([
13
+ "git",
14
+ "rev-parse",
15
+ "HEAD"
16
+ ], { cwd: rootDir });
17
+ }
18
+ /** Create individual GitHub releases for each published package */
19
+ async function createIndividualReleases(releases, changesets, rootDir, opts = {}) {
20
+ if (!isGhAvailable()) {
21
+ log.dim(" gh CLI not found — skipping GitHub releases");
22
+ return;
23
+ }
24
+ const headSha = getHeadSha(rootDir);
25
+ for (const release of releases) {
26
+ const tag = `${release.name}@${release.newVersion}`;
27
+ const body = buildReleaseBody(release, changesets);
28
+ const title = `${release.name} v${release.newVersion}`;
29
+ if (opts.dryRun) {
30
+ log.dim(` Would create GitHub release: ${title}`);
31
+ continue;
32
+ }
33
+ try {
34
+ const args = [
35
+ "gh",
36
+ "release",
37
+ "create",
38
+ tag,
39
+ "--title",
40
+ title,
41
+ "--notes",
42
+ body
43
+ ];
44
+ if (headSha) args.push("--target", headSha);
45
+ await runArgsAsync(args, { cwd: rootDir });
46
+ log.dim(` Created GitHub release: ${title}`);
47
+ } catch (err) {
48
+ log.warn(` Failed to create GitHub release for ${tag}: ${err instanceof Error ? err.message : err}`);
49
+ }
50
+ }
51
+ }
52
+ /** Create a single aggregated GitHub release for all published packages */
53
+ async function createAggregateRelease(releases, changesets, rootDir, opts = {}) {
54
+ if (!isGhAvailable()) {
55
+ log.dim(" gh CLI not found — skipping GitHub release");
56
+ return;
57
+ }
58
+ if (releases.length === 0) return;
59
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
60
+ const { tag, title } = resolveAggregateTagAndTitle(date, listTags(`release-${date}*`, { cwd: rootDir }), opts.title);
61
+ const body = buildAggregateBody(releases, changesets);
62
+ if (opts.dryRun) {
63
+ log.dim(` Would create aggregate GitHub release: ${title}`);
64
+ log.dim(` Tag: ${tag}`);
65
+ return;
66
+ }
67
+ try {
68
+ tryRunArgs([
69
+ "git",
70
+ "tag",
71
+ tag
72
+ ], { cwd: rootDir });
73
+ const headSha = getHeadSha(rootDir);
74
+ const args = [
75
+ "gh",
76
+ "release",
77
+ "create",
78
+ tag,
79
+ "--title",
80
+ title,
81
+ "--notes",
82
+ body
83
+ ];
84
+ if (headSha) args.push("--target", headSha);
85
+ await runArgsAsync(args, { cwd: rootDir });
86
+ log.success(`Created aggregate GitHub release: ${title}`);
87
+ } catch (err) {
88
+ log.warn(`Failed to create aggregate GitHub release: ${err instanceof Error ? err.message : err}`);
89
+ }
90
+ }
91
+ function buildReleaseBody(release, changesets) {
92
+ const lines = [];
93
+ const relevant = changesets.filter((cs) => release.changesets.includes(cs.id));
94
+ if (relevant.length > 0) {
95
+ for (const cs of relevant) if (cs.summary) lines.push(`- ${cs.summary.split("\n")[0]}`);
96
+ }
97
+ if (release.isDependencyBump && relevant.length === 0) lines.push("- Updated dependencies");
98
+ return lines.join("\n") || "No changelog entries.";
99
+ }
100
+ function buildAggregateBody(releases, changesets) {
101
+ const lines = [];
102
+ const groups = [
103
+ ["Major Changes", releases.filter((r) => r.type === "major")],
104
+ ["Minor Changes", releases.filter((r) => r.type === "minor")],
105
+ ["Patch Changes", releases.filter((r) => r.type === "patch")]
106
+ ];
107
+ for (const [heading, group] of groups) {
108
+ if (group.length === 0) continue;
109
+ lines.push(`## ${heading}\n`);
110
+ for (const release of group) {
111
+ lines.push(`### ${release.name} v${release.newVersion}\n`);
112
+ const relevant = changesets.filter((cs) => release.changesets.includes(cs.id));
113
+ if (relevant.length > 0) {
114
+ for (const cs of relevant) if (cs.summary) lines.push(`- ${cs.summary.split("\n")[0]}`);
115
+ } else if (release.isDependencyBump) lines.push("- Updated dependencies");
116
+ else if (release.isCascadeBump) lines.push("- Version bump via cascade rule");
117
+ lines.push("");
118
+ }
119
+ }
120
+ return lines.join("\n").trim() || "No changelog entries.";
121
+ }
122
+ /** Compute the aggregate release tag and title, appending -n suffix if a tag for the same date already exists */
123
+ function resolveAggregateTagAndTitle(date, existingTags, titleTemplate) {
124
+ const baseTag = `release-${date}`;
125
+ const suffix = existingTags.length === 0 ? "" : `-${existingTags.length + 1}`;
126
+ return {
127
+ tag: `${baseTag}${suffix}`,
128
+ title: (titleTemplate || "Release {{date}}").replace("{{date}}", `${date}${suffix}`)
129
+ };
130
+ }
131
+ function isGhAvailable() {
132
+ return tryRunArgs(["gh", "--version"]) !== null;
133
+ }
134
+ //#endregion
135
+ //#region src/commands/publish.ts
136
+ /**
137
+ * Publish packages that have been versioned but not yet published.
138
+ * Detects unpublished versions by comparing package.json versions against npm registry.
139
+ */
140
+ async function publishCommand(rootDir, opts) {
141
+ const config = await loadConfig(rootDir);
142
+ const { packages, catalogs } = await discoverWorkspace(rootDir, config);
143
+ const { packageManager: detectedPm } = await detectWorkspaces(rootDir);
144
+ const depGraph = new DependencyGraph(packages);
145
+ if (!opts.dryRun && hasUncommittedChanges({ cwd: rootDir })) {
146
+ log.warn("You have uncommitted changes. Commit or stash them before publishing.");
147
+ process.exit(1);
148
+ }
149
+ let toPublish = await findUnpublishedPackages(packages, config);
150
+ if (opts.filter) {
151
+ const { matchGlob } = await import("./config-BkwIEaQg.mjs").then((n) => n.t);
152
+ const patterns = opts.filter.split(",").map((p) => p.trim());
153
+ toPublish = toPublish.filter((r) => patterns.some((p) => matchGlob(r.name, p)));
154
+ }
155
+ if (toPublish.length === 0) {
156
+ log.info("No unpublished packages found.");
157
+ return;
158
+ }
159
+ const releasePlan = {
160
+ changesets: [],
161
+ releases: toPublish
162
+ };
163
+ if (opts.dryRun) log.bold("Dry run — would publish:");
164
+ else log.bold("Publishing:");
165
+ for (const r of toPublish) console.log(` ${r.name}@${colorize(r.newVersion, "cyan")}`);
166
+ console.log();
167
+ const result = await publishPackages(releasePlan, packages, depGraph, config, rootDir, {
168
+ dryRun: opts.dryRun,
169
+ tag: opts.tag
170
+ }, catalogs, detectedPm);
171
+ if (result.published.length > 0) log.success(`Published ${result.published.length} package(s)`);
172
+ if (result.skipped.length > 0) log.dim(`Skipped ${result.skipped.length}: ${result.skipped.map((s) => s.name).join(", ")}`);
173
+ if (result.failed.length > 0) {
174
+ log.error(`Failed ${result.failed.length}: ${result.failed.map((f) => `${f.name} (${f.error})`).join(", ")}`);
175
+ process.exit(1);
176
+ }
177
+ if (!opts.dryRun && !opts.noPush && result.published.length > 0) try {
178
+ log.step("Pushing tags...");
179
+ pushWithTags({ cwd: rootDir });
180
+ log.success("Pushed tags to remote");
181
+ } catch (err) {
182
+ log.warn(`Failed to push tags: ${err instanceof Error ? err.message : err}`);
183
+ }
184
+ if (result.published.length > 0) {
185
+ const publishedReleases = releasePlan.releases.filter((r) => result.published.some((p) => p.name === r.name));
186
+ const aggConfig = config.aggregateRelease;
187
+ const isAggregate = aggConfig === true || typeof aggConfig === "object" && aggConfig.enabled;
188
+ const aggTitle = typeof aggConfig === "object" ? aggConfig.title : void 0;
189
+ if (isAggregate) await createAggregateRelease(publishedReleases, releasePlan.changesets, rootDir, {
190
+ dryRun: opts.dryRun,
191
+ title: aggTitle
192
+ });
193
+ else await createIndividualReleases(publishedReleases, releasePlan.changesets, rootDir, { dryRun: opts.dryRun });
194
+ }
195
+ }
196
+ /**
197
+ * Find packages whose current version is not yet published.
198
+ *
199
+ * Detection strategy (per package):
200
+ * 1. Custom `checkPublished` command → run it, compare output to current version
201
+ * 2. `skipNpmPublish` or custom `publishCommand` → check git tags
202
+ * 3. Default → check npm registry via `npm info`
203
+ */
204
+ async function findUnpublishedPackages(packages, _config) {
205
+ const unpublished = [];
206
+ for (const [name, pkg] of packages) {
207
+ if (pkg.private && !pkg.bumpy?.publishCommand) continue;
208
+ if (pkg.version === "0.0.0") continue;
209
+ if (!await checkIfPublished(name, pkg.version, pkg.bumpy)) unpublished.push({
210
+ name,
211
+ type: "patch",
212
+ oldVersion: pkg.version,
213
+ newVersion: pkg.version,
214
+ changesets: [],
215
+ isDependencyBump: false,
216
+ isCascadeBump: false
217
+ });
218
+ }
219
+ return unpublished;
220
+ }
221
+ async function checkIfPublished(name, version, pkgConfig) {
222
+ const { runAsync, runArgsAsync, tryRunArgs } = await import("./shell-Dj7JRD_q.mjs").then((n) => n.i);
223
+ if (pkgConfig?.checkPublished) try {
224
+ return (await runAsync(pkgConfig.checkPublished)).trim() === version;
225
+ } catch {
226
+ return false;
227
+ }
228
+ if (pkgConfig?.skipNpmPublish || pkgConfig?.publishCommand) {
229
+ const tag = `${name}@${version}`;
230
+ return tryRunArgs([
231
+ "git",
232
+ "tag",
233
+ "-l",
234
+ tag
235
+ ]) === tag;
236
+ }
237
+ try {
238
+ const args = [
239
+ "npm",
240
+ "info",
241
+ `${name}@${version}`,
242
+ "version"
243
+ ];
244
+ if (pkgConfig?.registry) args.push("--registry", pkgConfig.registry);
245
+ return await runArgsAsync(args) === version;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ //#endregion
251
+ export { publishCommand };
@@ -0,0 +1,277 @@
1
+ import { n as log, t as colorize } from "./logger-C2dEe5Su.mjs";
2
+ import { a as readJson, c as writeJson } from "./fs-0AtnPUUe.mjs";
3
+ import { r as resolveCatalogDep } from "./package-manager-DcI5TdDE.mjs";
4
+ import { a as sq, n as runArgsAsync, o as tryRunArgs, r as runAsync } from "./shell-Dj7JRD_q.mjs";
5
+ import { r as stripProtocol } from "./semver-BTzYh8vc.mjs";
6
+ import { o as tagExists, t as createTag } from "./git-CGHVXXKw.mjs";
7
+ import { resolve } from "node:path";
8
+ import { unlink } from "node:fs/promises";
9
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
10
+ //#region src/core/publish-pipeline.ts
11
+ /**
12
+ * Detect which CI OIDC provider is available for npm trusted publishing.
13
+ * Returns the provider name or null if none detected.
14
+ *
15
+ * Supported providers:
16
+ * - GitHub Actions: `ACTIONS_ID_TOKEN_REQUEST_URL` (set when `id-token: write` permission is granted)
17
+ * - GitLab CI: `GITLAB_CI` + `NPM_ID_TOKEN`
18
+ * - CircleCI: `CIRCLECI` + `NPM_ID_TOKEN`
19
+ */
20
+ function detectOidcProvider() {
21
+ if (process.env.ACTIONS_ID_TOKEN_REQUEST_URL) return "github-actions";
22
+ if (process.env.GITLAB_CI && process.env.NPM_ID_TOKEN) return "gitlab";
23
+ if (process.env.CIRCLECI && process.env.NPM_ID_TOKEN) return "circleci";
24
+ return null;
25
+ }
26
+ const OIDC_NPM_UPGRADE_HINTS = {
27
+ "github-actions": "Add `actions/setup-node@v6` with `node-version: lts/*` to your workflow",
28
+ gitlab: "Use a Node.js image with npm >= 11.5.1 or run `npm install -g npm@latest`",
29
+ circleci: "Use a Node.js image with npm >= 11.5.1 or run `sudo npm install -g npm@latest`"
30
+ };
31
+ /**
32
+ * Set up npm authentication for publishing.
33
+ *
34
+ * Handles three scenarios:
35
+ * 1. **Trusted publishing (OIDC)** — GitHub Actions, GitLab CI, or CircleCI with OIDC configured.
36
+ * npm >= 11.5.1 authenticates automatically via OIDC token exchange.
37
+ * No secret needed, but we check the npm version and warn if too old.
38
+ * 2. **Token-based auth** — `NPM_TOKEN` or `NODE_AUTH_TOKEN` env var.
39
+ * Writes a project-level `.npmrc` so npm can authenticate.
40
+ * 3. **Pre-configured** — user already has `.npmrc` with auth (e.g. via `actions/setup-node`).
41
+ */
42
+ function setupNpmAuth(rootDir, publishManager) {
43
+ if (publishManager !== "npm") return;
44
+ const npmrcPath = resolve(rootDir, ".npmrc");
45
+ const existingNpmrc = existsSync(npmrcPath) ? readFileSync(npmrcPath, "utf-8") : "";
46
+ if (existingNpmrc.includes(":_authToken=")) {
47
+ log.dim(" Using existing .npmrc auth configuration");
48
+ return;
49
+ }
50
+ const oidcProvider = detectOidcProvider();
51
+ if (oidcProvider) {
52
+ const npmVersion = tryRunArgs(["npm", "--version"]);
53
+ if (npmVersion) {
54
+ const [major, minor, patch] = npmVersion.split(".").map(Number);
55
+ if (!(major > 11 || major === 11 && (minor > 5 || minor === 5 && patch >= 1))) {
56
+ log.warn(` npm ${npmVersion} detected — trusted publishing (OIDC) requires npm >= 11.5.1`);
57
+ log.warn(` ${OIDC_NPM_UPGRADE_HINTS[oidcProvider]}`);
58
+ } else log.dim(` OIDC detected (${oidcProvider}) — npm ${npmVersion} will authenticate via trusted publishing`);
59
+ }
60
+ return;
61
+ }
62
+ const token = process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN;
63
+ if (token) {
64
+ if (process.env.NPM_TOKEN && !process.env.NODE_AUTH_TOKEN) process.env.NODE_AUTH_TOKEN = token;
65
+ const authLine = "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}";
66
+ if (existingNpmrc) appendFileSync(npmrcPath, `\n${authLine}\n`);
67
+ else writeFileSync(npmrcPath, `${authLine}\n`);
68
+ log.dim(" Configured .npmrc with auth token");
69
+ return;
70
+ }
71
+ if (process.env.CI) {
72
+ log.warn(" No npm authentication detected. Publishing will likely fail.");
73
+ log.warn(" Options:");
74
+ log.warn(" • Trusted publishing (OIDC): add `id-token: write` permission + npm >= 11.5.1");
75
+ log.warn(" • Token auth: set NPM_TOKEN or NODE_AUTH_TOKEN environment variable");
76
+ log.warn(" • Manual: add `actions/setup-node` with `registry-url` to your workflow");
77
+ }
78
+ }
79
+ /**
80
+ * Publish all packages in the release plan.
81
+ * Order: topological (dependencies published before dependents).
82
+ */
83
+ async function publishPackages(releasePlan, packages, depGraph, config, rootDir, opts = {}, catalogs = /* @__PURE__ */ new Map(), detectedPm = "npm") {
84
+ const result = {
85
+ published: [],
86
+ skipped: [],
87
+ failed: []
88
+ };
89
+ const publishConfig = config.publish;
90
+ setupNpmAuth(rootDir, publishConfig.publishManager);
91
+ const packManager = publishConfig.packManager === "auto" ? detectedPm : publishConfig.packManager;
92
+ const topoOrder = depGraph.topologicalSort(packages);
93
+ const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
94
+ const ordered = [];
95
+ for (const name of topoOrder) {
96
+ const release = releaseMap.get(name);
97
+ if (release) ordered.push(release);
98
+ }
99
+ for (const release of ordered) {
100
+ const pkg = packages.get(release.name);
101
+ const pkgConfig = pkg.bumpy || {};
102
+ if (pkg.private && !pkgConfig.publishCommand) {
103
+ if (config.privatePackages.tag) createGitTag(release, rootDir, opts);
104
+ result.skipped.push({
105
+ name: release.name,
106
+ reason: "private"
107
+ });
108
+ continue;
109
+ }
110
+ log.step(`Publishing ${colorize(release.name, "cyan")}@${release.newVersion}`);
111
+ try {
112
+ if (pkgConfig.buildCommand) {
113
+ log.dim(` Building...`);
114
+ if (!opts.dryRun) await runAsync(pkgConfig.buildCommand, { cwd: pkg.dir });
115
+ }
116
+ if (pkgConfig.publishCommand || publishConfig.protocolResolution === "in-place") await resolveProtocolsInPlace(pkg, packages, releasePlan, catalogs);
117
+ if (pkgConfig.publishCommand) {
118
+ const commands = Array.isArray(pkgConfig.publishCommand) ? pkgConfig.publishCommand : [pkgConfig.publishCommand];
119
+ for (const cmd of commands) {
120
+ const expanded = cmd.replace(/\{\{version\}\}/g, sq(release.newVersion)).replace(/\{\{name\}\}/g, sq(release.name));
121
+ log.dim(` Running: ${expanded}`);
122
+ if (!opts.dryRun) await runAsync(expanded, { cwd: pkg.dir });
123
+ }
124
+ } else if (!pkgConfig.skipNpmPublish) if (publishConfig.protocolResolution === "pack") await packThenPublish(pkg, pkgConfig, config, packManager, opts);
125
+ else await npmPublishDirect(pkg, pkgConfig, config, opts);
126
+ else {
127
+ result.skipped.push({
128
+ name: release.name,
129
+ reason: "skipNpmPublish"
130
+ });
131
+ createGitTag(release, rootDir, opts);
132
+ continue;
133
+ }
134
+ createGitTag(release, rootDir, opts);
135
+ result.published.push({
136
+ name: release.name,
137
+ version: release.newVersion
138
+ });
139
+ log.success(` Published ${release.name}@${release.newVersion}`);
140
+ } catch (err) {
141
+ const errMsg = err instanceof Error ? err.message : String(err);
142
+ log.error(` Failed to publish ${release.name}: ${errMsg}`);
143
+ result.failed.push({
144
+ name: release.name,
145
+ error: errMsg
146
+ });
147
+ }
148
+ }
149
+ return result;
150
+ }
151
+ /**
152
+ * Pack with the PM (which resolves workspace:/catalog: protocols into the tarball),
153
+ * then publish the tarball with npm (which supports OIDC/provenance).
154
+ */
155
+ async function packThenPublish(pkg, pkgConfig, config, packManager, opts) {
156
+ const packArgs = getPackArgs(packManager);
157
+ log.dim(` Packing with: ${packArgs.join(" ")}`);
158
+ if (opts.dryRun) {
159
+ const publishArgs = buildPublishArgs(pkg, pkgConfig, config, opts, "<tarball>");
160
+ log.dim(` Would publish with: ${publishArgs.join(" ")}`);
161
+ return;
162
+ }
163
+ const tarball = parseTarballPath(await runArgsAsync(packArgs, { cwd: pkg.dir }), pkg.dir);
164
+ try {
165
+ const publishArgs = buildPublishArgs(pkg, pkgConfig, config, opts, tarball);
166
+ log.dim(` Publishing: ${publishArgs.join(" ")}`);
167
+ await runArgsAsync(publishArgs, { cwd: pkg.dir });
168
+ } finally {
169
+ try {
170
+ await unlink(tarball);
171
+ } catch {}
172
+ }
173
+ }
174
+ /** Publish directly from the package directory (no tarball) */
175
+ async function npmPublishDirect(pkg, pkgConfig, config, opts) {
176
+ const args = buildPublishArgs(pkg, pkgConfig, config, opts);
177
+ log.dim(` Running: ${args.join(" ")}`);
178
+ if (!opts.dryRun) await runArgsAsync(args, { cwd: pkg.dir });
179
+ }
180
+ function getPackArgs(pm) {
181
+ switch (pm) {
182
+ case "pnpm": return ["pnpm", "pack"];
183
+ case "bun": return [
184
+ "bun",
185
+ "pm",
186
+ "pack"
187
+ ];
188
+ case "yarn": return ["yarn", "pack"];
189
+ default: return ["npm", "pack"];
190
+ }
191
+ }
192
+ function buildPublishArgs(pkg, pkgConfig, config, opts, tarball) {
193
+ const publishManager = config.publish.publishManager;
194
+ const args = [];
195
+ if (publishManager === "yarn") args.push("yarn", "npm", "publish");
196
+ else args.push(publishManager, "publish");
197
+ if (tarball) args.push(tarball);
198
+ const access = pkgConfig?.access || config.access;
199
+ args.push("--access", access);
200
+ if (pkgConfig?.registry) args.push("--registry", pkgConfig.registry);
201
+ if (opts.tag) args.push("--tag", opts.tag);
202
+ if (config.publish.publishArgs.length > 0) args.push(...config.publish.publishArgs);
203
+ return args;
204
+ }
205
+ /**
206
+ * Parse the tarball path from pack command output.
207
+ * Each PM has different output formats:
208
+ * npm/pnpm: tarball filename on the last line
209
+ * bun: tarball filename mid-output, summary lines after
210
+ * yarn: 'success Wrote tarball to "/path/to/foo.tgz".'
211
+ */
212
+ function parseTarballPath(output, cwd) {
213
+ const tgzMatch = output.match(/(?:^|["'\s])([^\s"']*\.tgz)/m);
214
+ if (tgzMatch) {
215
+ const tarball = tgzMatch[1];
216
+ return tarball.startsWith("/") ? tarball : resolve(cwd, tarball);
217
+ }
218
+ const lines = output.trim().split("\n").filter(Boolean);
219
+ const lastLine = lines[lines.length - 1]?.trim() || "";
220
+ return lastLine.startsWith("/") ? lastLine : resolve(cwd, lastLine);
221
+ }
222
+ function createGitTag(release, rootDir, opts) {
223
+ const tag = `${release.name}@${release.newVersion}`;
224
+ if (opts.dryRun) {
225
+ log.dim(` Would create tag: ${tag}`);
226
+ return;
227
+ }
228
+ if (tagExists(tag, { cwd: rootDir })) {
229
+ log.dim(` Tag ${tag} already exists, skipping`);
230
+ return;
231
+ }
232
+ createTag(tag, { cwd: rootDir });
233
+ log.dim(` Tagged: ${tag}`);
234
+ }
235
+ /**
236
+ * Resolve workspace:/catalog: protocols by rewriting package.json in-place.
237
+ * Used for custom publish commands and "in-place" protocolResolution mode.
238
+ */
239
+ async function resolveProtocolsInPlace(pkg, packages, releasePlan, catalogs) {
240
+ const pkgJsonPath = resolve(pkg.dir, "package.json");
241
+ const pkgJson = await readJson(pkgJsonPath);
242
+ let modified = false;
243
+ const releaseMap = new Map(releasePlan.releases.map((r) => [r.name, r]));
244
+ for (const depField of [
245
+ "dependencies",
246
+ "devDependencies",
247
+ "peerDependencies",
248
+ "optionalDependencies"
249
+ ]) {
250
+ const deps = pkgJson[depField];
251
+ if (!deps) continue;
252
+ for (const [depName, range] of Object.entries(deps)) {
253
+ let resolved = null;
254
+ if (range.startsWith("catalog:")) {
255
+ resolved = resolveCatalogDep(depName, range, catalogs);
256
+ if (!resolved) {
257
+ log.warn(` Could not resolve ${depName}: "${range}" — catalog entry not found`);
258
+ continue;
259
+ }
260
+ } else if (range.startsWith("workspace:")) {
261
+ const cleanRange = stripProtocol(range);
262
+ if (cleanRange === "*" || cleanRange === "^" || cleanRange === "~") {
263
+ const depPkg = packages.get(depName);
264
+ const version = releaseMap.get(depName)?.newVersion || depPkg?.version || "0.0.0";
265
+ resolved = `${cleanRange === "*" ? "^" : cleanRange}${version}`;
266
+ } else resolved = cleanRange;
267
+ }
268
+ if (resolved) {
269
+ deps[depName] = resolved;
270
+ modified = true;
271
+ }
272
+ }
273
+ }
274
+ if (modified) await writeJson(pkgJsonPath, pkgJson);
275
+ }
276
+ //#endregion
277
+ export { publishPackages as t };
@@ -0,0 +1,173 @@
1
+ import { g as parseIsolatedBump, h as maxBump, l as DEFAULT_BUMP_RULES, m as hasCascade, p as bumpLevel, s as matchGlob } from "./config-BkwIEaQg.mjs";
2
+ import { n as satisfies, r as stripProtocol, t as bumpVersion } from "./semver-BTzYh8vc.mjs";
3
+ //#region src/core/release-plan.ts
4
+ /**
5
+ * Build a release plan from pending changesets, the dependency graph, and config.
6
+ * This is the core algorithm of bumpy.
7
+ */
8
+ function assembleReleasePlan(changesets, packages, depGraph, config) {
9
+ if (changesets.length === 0) return {
10
+ changesets: [],
11
+ releases: []
12
+ };
13
+ const planned = /* @__PURE__ */ new Map();
14
+ const cascadeOverrides = /* @__PURE__ */ new Map();
15
+ for (const cs of changesets) for (const release of cs.releases) {
16
+ if (!packages.has(release.name)) continue;
17
+ const { bump, isolated } = parseIsolatedBump(release.type);
18
+ const existing = planned.get(release.name);
19
+ if (existing) {
20
+ existing.type = maxBump(existing.type, bump);
21
+ if (!isolated) existing.isolated = false;
22
+ existing.changesets.add(cs.id);
23
+ } else planned.set(release.name, {
24
+ type: bump,
25
+ isolated,
26
+ isDependencyBump: false,
27
+ isCascadeBump: false,
28
+ changesets: new Set([cs.id])
29
+ });
30
+ if (hasCascade(release)) {
31
+ if (!cascadeOverrides.has(release.name)) cascadeOverrides.set(release.name, /* @__PURE__ */ new Map());
32
+ const overrides = cascadeOverrides.get(release.name);
33
+ for (const [pattern, bumpType] of Object.entries(release.cascade)) {
34
+ const existing = overrides.get(pattern);
35
+ overrides.set(pattern, maxBump(existing, bumpType));
36
+ }
37
+ }
38
+ }
39
+ for (const group of config.fixed) {
40
+ let groupBump;
41
+ let groupIsolated = true;
42
+ for (const nameOrGlob of group) for (const [name, bump] of planned) if (matchGlob(name, nameOrGlob)) {
43
+ groupBump = maxBump(groupBump, bump.type);
44
+ if (!bump.isolated) groupIsolated = false;
45
+ }
46
+ if (!groupBump) continue;
47
+ for (const nameOrGlob of group) for (const [name] of packages) {
48
+ if (!matchGlob(name, nameOrGlob)) continue;
49
+ const existing = planned.get(name);
50
+ if (existing) {
51
+ existing.type = groupBump;
52
+ existing.isolated = groupIsolated;
53
+ } else planned.set(name, {
54
+ type: groupBump,
55
+ isolated: groupIsolated,
56
+ isDependencyBump: false,
57
+ isCascadeBump: false,
58
+ changesets: /* @__PURE__ */ new Set()
59
+ });
60
+ }
61
+ }
62
+ for (const group of config.linked) {
63
+ let groupBump;
64
+ for (const nameOrGlob of group) for (const [name, bump] of planned) if (matchGlob(name, nameOrGlob)) groupBump = maxBump(groupBump, bump.type);
65
+ if (!groupBump) continue;
66
+ for (const nameOrGlob of group) for (const [name] of packages) {
67
+ if (!matchGlob(name, nameOrGlob)) continue;
68
+ const existing = planned.get(name);
69
+ if (existing) existing.type = groupBump;
70
+ }
71
+ }
72
+ let changed = true;
73
+ let iterations = 0;
74
+ const MAX_ITERATIONS = 100;
75
+ while (changed && iterations < MAX_ITERATIONS) {
76
+ changed = false;
77
+ iterations++;
78
+ for (const [pkgName, bump] of planned) {
79
+ if (bump.isolated) continue;
80
+ const csOverrides = cascadeOverrides.get(pkgName);
81
+ if (csOverrides) for (const [pattern, cascadeBumpType] of csOverrides) for (const [targetName] of packages) {
82
+ if (!matchGlob(targetName, pattern)) continue;
83
+ if (applyBump(planned, targetName, cascadeBumpType, false, true, bump.changesets)) changed = true;
84
+ }
85
+ const cascadeTo = packages.get(pkgName)?.bumpy?.cascadeTo;
86
+ if (cascadeTo) for (const [pattern, rule] of Object.entries(cascadeTo)) {
87
+ if (!shouldTrigger(bump.type, rule.trigger)) continue;
88
+ const cascadeBump = rule.bumpAs === "match" ? bump.type : rule.bumpAs;
89
+ for (const [targetName] of packages) {
90
+ if (!matchGlob(targetName, pattern)) continue;
91
+ if (applyBump(planned, targetName, cascadeBump, false, true, bump.changesets)) changed = true;
92
+ }
93
+ }
94
+ const dependents = depGraph.getDependents(pkgName);
95
+ for (const dep of dependents) {
96
+ const rule = resolveRule(dep.name, pkgName, dep.depType, packages, config);
97
+ if (!shouldTrigger(bump.type, rule.trigger)) continue;
98
+ if (config.updateInternalDependencies === "out-of-range") {
99
+ if (satisfies(bumpVersion(packages.get(pkgName).version, bump.type), stripProtocol(dep.versionRange))) continue;
100
+ }
101
+ if (config.updateInternalDependencies === "none") continue;
102
+ if (config.updateInternalDependencies === "minor" && bumpLevel(bump.type) < bumpLevel("minor")) continue;
103
+ const depBump = rule.bumpAs === "match" ? bump.type : rule.bumpAs;
104
+ if (applyBump(planned, dep.name, depBump, true, false, bump.changesets)) changed = true;
105
+ }
106
+ }
107
+ }
108
+ const releases = [];
109
+ for (const [name, bump] of planned) {
110
+ const pkg = packages.get(name);
111
+ if (!pkg) continue;
112
+ const newVersion = bumpVersion(pkg.version, bump.type);
113
+ releases.push({
114
+ name,
115
+ type: bump.type,
116
+ oldVersion: pkg.version,
117
+ newVersion,
118
+ changesets: [...bump.changesets],
119
+ isDependencyBump: bump.isDependencyBump,
120
+ isCascadeBump: bump.isCascadeBump
121
+ });
122
+ }
123
+ releases.sort((a, b) => a.name.localeCompare(b.name));
124
+ return {
125
+ changesets,
126
+ releases
127
+ };
128
+ }
129
+ /** Apply a bump to a package, upgrading if already planned. Returns true if anything changed. */
130
+ function applyBump(planned, name, type, isDependencyBump, isCascadeBump, sourceChangesets) {
131
+ const existing = planned.get(name);
132
+ if (existing) {
133
+ const newType = maxBump(existing.type, type);
134
+ if (newType === existing.type) return false;
135
+ existing.type = newType;
136
+ if (isDependencyBump) existing.isDependencyBump = true;
137
+ if (isCascadeBump) existing.isCascadeBump = true;
138
+ for (const cs of sourceChangesets) existing.changesets.add(cs);
139
+ return true;
140
+ }
141
+ planned.set(name, {
142
+ type,
143
+ isolated: false,
144
+ isDependencyBump,
145
+ isCascadeBump,
146
+ changesets: new Set(sourceChangesets)
147
+ });
148
+ return true;
149
+ }
150
+ /** Check if a bump level meets the trigger threshold */
151
+ function shouldTrigger(bumpType, trigger) {
152
+ if (trigger === "none") return false;
153
+ return bumpLevel(bumpType) >= bumpLevel(trigger);
154
+ }
155
+ /**
156
+ * Resolve the dependency bump rule for a specific dependent/dependency pair.
157
+ * Priority: specificDependencyRules > per-package depType rules > global depType rules > defaults
158
+ */
159
+ function resolveRule(dependentName, dependencyName, depType, packages, config) {
160
+ const dependent = packages.get(dependentName);
161
+ if (dependent?.bumpy?.specificDependencyRules?.[dependencyName]) return dependent.bumpy.specificDependencyRules[dependencyName];
162
+ if (dependent?.bumpy?.specificDependencyRules) {
163
+ for (const [pattern, rule] of Object.entries(dependent.bumpy.specificDependencyRules)) if (matchGlob(dependencyName, pattern)) return rule;
164
+ }
165
+ if (dependent?.bumpy?.dependencyBumpRules?.[depType]) return dependent.bumpy.dependencyBumpRules[depType];
166
+ if (config.dependencyBumpRules[depType]) return config.dependencyBumpRules[depType];
167
+ return DEFAULT_BUMP_RULES[depType] || {
168
+ trigger: "patch",
169
+ bumpAs: "patch"
170
+ };
171
+ }
172
+ //#endregion
173
+ export { assembleReleasePlan as t };