@ucdjs/release-scripts 0.1.0-beta.35 → 0.1.0-beta.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,128 +1,57 @@
1
1
  import { t as Eta } from "./eta-DAZlmVBQ.mjs";
2
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { join, relative } from "node:path";
4
- import farver from "farver";
5
- import prompts from "prompts";
4
+ import { getCommits, groupByType } from "commit-parser";
6
5
  import process from "node:process";
7
6
  import readline from "node:readline";
7
+ import farver from "farver";
8
8
  import mri from "mri";
9
9
  import { exec } from "tinyexec";
10
10
  import { dedent } from "@luxass/utils";
11
- import { getCommits, groupByType } from "commit-parser";
11
+ import prompts from "prompts";
12
12
  import { compare, gt } from "semver";
13
13
 
14
- //#region src/operations/semver.ts
15
- function isValidSemver(version) {
16
- return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
17
- }
18
- function getNextVersion(currentVersion, bump) {
19
- if (bump === "none") return currentVersion;
20
- if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
21
- const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
22
- if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
23
- const [, major, minor, patch] = match;
24
- let newMajor = Number.parseInt(major, 10);
25
- let newMinor = Number.parseInt(minor, 10);
26
- let newPatch = Number.parseInt(patch, 10);
27
- switch (bump) {
28
- case "major":
29
- newMajor += 1;
30
- newMinor = 0;
31
- newPatch = 0;
32
- break;
33
- case "minor":
34
- newMinor += 1;
35
- newPatch = 0;
36
- break;
37
- case "patch":
38
- newPatch += 1;
39
- break;
14
+ //#region src/operations/changelog-format.ts
15
+ function formatCommitLine({ commit, owner, repo, authors }) {
16
+ const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.hash}`;
17
+ let line = `${commit.description}`;
18
+ const references = commit.references ?? [];
19
+ for (const ref of references) {
20
+ if (!ref.value) continue;
21
+ const number = Number.parseInt(ref.value.replace(/^#/, ""), 10);
22
+ if (Number.isNaN(number)) continue;
23
+ if (ref.type === "issue") {
24
+ line += ` ([Issue ${ref.value}](https://github.com/${owner}/${repo}/issues/${number}))`;
25
+ continue;
26
+ }
27
+ line += ` ([PR ${ref.value}](https://github.com/${owner}/${repo}/pull/${number}))`;
40
28
  }
41
- return `${newMajor}.${newMinor}.${newPatch}`;
42
- }
43
- function calculateBumpType(oldVersion, newVersion) {
44
- if (!isValidSemver(oldVersion) || !isValidSemver(newVersion)) throw new Error(`Cannot calculate bump type for invalid semver: ${oldVersion} or ${newVersion}`);
45
- const oldParts = oldVersion.split(".").map(Number);
46
- const newParts = newVersion.split(".").map(Number);
47
- if (newParts[0] > oldParts[0]) return "major";
48
- if (newParts[1] > oldParts[1]) return "minor";
49
- if (newParts[2] > oldParts[2]) return "patch";
50
- return "none";
29
+ line += ` ([${commit.shortHash}](${commitUrl}))`;
30
+ if (authors.length > 0) {
31
+ const authorList = authors.map((author) => author.login ? `[@${author.login}](https://github.com/${author.login})` : author.name).join(", ");
32
+ line += ` (by ${authorList})`;
33
+ }
34
+ return line;
51
35
  }
52
-
53
- //#endregion
54
- //#region src/core/prompts.ts
55
- async function selectPackagePrompt(packages) {
56
- const response = await prompts({
57
- type: "multiselect",
58
- name: "selectedPackages",
59
- message: "Select packages to release",
60
- choices: packages.map((pkg) => ({
61
- title: `${pkg.name} (${farver.bold(pkg.version)})`,
62
- value: pkg.name,
63
- selected: true
64
- })),
65
- min: 1,
66
- hint: "Space to select/deselect. Return to submit.",
67
- instructions: false
36
+ function buildTemplateGroups(options) {
37
+ const { commits, owner, repo, types, commitAuthors } = options;
38
+ const grouped = groupByType(commits, {
39
+ includeNonConventional: false,
40
+ mergeKeys: Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.types ?? [key]]))
41
+ });
42
+ return Object.entries(types).map(([key, value]) => {
43
+ const formattedCommits = (grouped.get(key) ?? []).map((commit) => ({ line: formatCommitLine({
44
+ commit,
45
+ owner,
46
+ repo,
47
+ authors: commitAuthors.get(commit.hash) ?? []
48
+ }) }));
49
+ return {
50
+ name: key,
51
+ title: value.title,
52
+ commits: formattedCommits
53
+ };
68
54
  });
69
- if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
70
- return response.selectedPackages;
71
- }
72
- async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggestedVersion) {
73
- const answers = await prompts([{
74
- type: "autocomplete",
75
- name: "version",
76
- message: `${pkg.name}: ${farver.green(pkg.version)}`,
77
- choices: [
78
- {
79
- value: "skip",
80
- title: `skip ${farver.dim("(no change)")}`
81
- },
82
- {
83
- value: "major",
84
- title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}`
85
- },
86
- {
87
- value: "minor",
88
- title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}`
89
- },
90
- {
91
- value: "patch",
92
- title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
93
- },
94
- {
95
- value: "suggested",
96
- title: `suggested ${farver.bold(suggestedVersion)}`
97
- },
98
- {
99
- value: "as-is",
100
- title: `as-is ${farver.dim("(keep current version)")}`
101
- },
102
- {
103
- value: "custom",
104
- title: "custom"
105
- }
106
- ],
107
- initial: suggestedVersion === currentVersion ? 0 : 4
108
- }, {
109
- type: (prev) => prev === "custom" ? "text" : null,
110
- name: "custom",
111
- message: "Enter the new version number:",
112
- initial: suggestedVersion,
113
- validate: (custom) => {
114
- if (isValidSemver(custom)) return true;
115
- return "That's not a valid version number";
116
- }
117
- }]);
118
- if (!answers.version) return null;
119
- if (answers.version === "skip") return null;
120
- else if (answers.version === "suggested") return suggestedVersion;
121
- else if (answers.version === "custom") {
122
- if (!answers.custom) return null;
123
- return answers.custom;
124
- } else if (answers.version === "as-is") return currentVersion;
125
- else return getNextVersion(pkg.version, answers.version);
126
55
  }
127
56
 
128
57
  //#endregion
@@ -210,118 +139,135 @@ if (isDryRun || isVerbose || isForce) {
210
139
  }
211
140
 
212
141
  //#endregion
213
- //#region src/types/result.ts
214
- function ok(value) {
215
- return {
216
- ok: true,
217
- value
218
- };
219
- }
220
- function err(error) {
221
- return {
222
- ok: false,
223
- error
224
- };
225
- }
226
-
227
- //#endregion
228
- //#region src/core/workspace.ts
229
- async function discoverWorkspacePackages(workspaceRoot, options) {
230
- let workspaceOptions;
231
- let explicitPackages;
232
- if (options.packages == null || options.packages === true) workspaceOptions = { excludePrivate: false };
233
- else if (Array.isArray(options.packages)) {
234
- workspaceOptions = {
235
- excludePrivate: false,
236
- include: options.packages
237
- };
238
- explicitPackages = options.packages;
239
- } else {
240
- workspaceOptions = options.packages;
241
- if (options.packages.include) explicitPackages = options.packages.include;
142
+ //#region src/core/github.ts
143
+ var GitHubClient = class {
144
+ owner;
145
+ repo;
146
+ githubToken;
147
+ apiBase = "https://api.github.com";
148
+ constructor({ owner, repo, githubToken }) {
149
+ this.owner = owner;
150
+ this.repo = repo;
151
+ this.githubToken = githubToken;
242
152
  }
243
- let workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
244
- if (explicitPackages) {
245
- const foundNames = new Set(workspacePackages.map((p) => p.name));
246
- const missing = explicitPackages.filter((p) => !foundNames.has(p));
247
- if (missing.length > 0) exitWithError(`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`, "Check your package names or run 'pnpm ls' to see available packages");
153
+ async request(path, init = {}) {
154
+ const url = path.startsWith("http") ? path : `${this.apiBase}${path}`;
155
+ const res = await fetch(url, {
156
+ ...init,
157
+ headers: {
158
+ ...init.headers,
159
+ "Accept": "application/vnd.github.v3+json",
160
+ "Authorization": `token ${this.githubToken}`,
161
+ "User-Agent": "ucdjs-release-scripts (+https://github.com/ucdjs/ucdjs-release-scripts)"
162
+ }
163
+ });
164
+ if (!res.ok) {
165
+ const errorText = await res.text();
166
+ throw new Error(`GitHub API request failed with status ${res.status}: ${errorText || "No response body"}`);
167
+ }
168
+ if (res.status === 204) return;
169
+ return res.json();
248
170
  }
249
- const isPackagePromptEnabled = options.prompts?.packages !== false;
250
- if (!isCI && isPackagePromptEnabled && !explicitPackages) {
251
- const selectedNames = await selectPackagePrompt(workspacePackages);
252
- workspacePackages = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
171
+ async getExistingPullRequest(branch) {
172
+ const head = branch.includes(":") ? branch : `${this.owner}:${branch}`;
173
+ const endpoint = `/repos/${this.owner}/${this.repo}/pulls?state=open&head=${encodeURIComponent(head)}`;
174
+ logger.verbose(`Requesting pull request for branch: ${branch} (url: ${this.apiBase}${endpoint})`);
175
+ const pulls = await this.request(endpoint);
176
+ if (!Array.isArray(pulls) || pulls.length === 0) return null;
177
+ const firstPullRequest = pulls[0];
178
+ if (typeof firstPullRequest !== "object" || firstPullRequest === null || !("number" in firstPullRequest) || typeof firstPullRequest.number !== "number" || !("title" in firstPullRequest) || typeof firstPullRequest.title !== "string" || !("body" in firstPullRequest) || typeof firstPullRequest.body !== "string" || !("draft" in firstPullRequest) || typeof firstPullRequest.draft !== "boolean" || !("html_url" in firstPullRequest) || typeof firstPullRequest.html_url !== "string") throw new TypeError("Pull request data validation failed");
179
+ const pullRequest = {
180
+ number: firstPullRequest.number,
181
+ title: firstPullRequest.title,
182
+ body: firstPullRequest.body,
183
+ draft: firstPullRequest.draft,
184
+ html_url: firstPullRequest.html_url,
185
+ head: "head" in firstPullRequest && typeof firstPullRequest.head === "object" && firstPullRequest.head !== null && "sha" in firstPullRequest.head && typeof firstPullRequest.head.sha === "string" ? { sha: firstPullRequest.head.sha } : void 0
186
+ };
187
+ logger.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
188
+ return pullRequest;
253
189
  }
254
- return workspacePackages;
255
- }
256
- function toWorkspaceError(operation, error) {
257
- return {
258
- type: "workspace",
259
- operation,
260
- message: error instanceof Error ? error.message : String(error)
261
- };
262
- }
263
- async function wrapWorkspace(operation, fn) {
264
- try {
265
- return ok(await fn());
266
- } catch (error) {
267
- return err(toWorkspaceError(operation, error));
190
+ async upsertPullRequest({ title, body, head, base, pullNumber }) {
191
+ const isUpdate = typeof pullNumber === "number";
192
+ const endpoint = isUpdate ? `/repos/${this.owner}/${this.repo}/pulls/${pullNumber}` : `/repos/${this.owner}/${this.repo}/pulls`;
193
+ const requestBody = isUpdate ? {
194
+ title,
195
+ body
196
+ } : {
197
+ title,
198
+ body,
199
+ head,
200
+ base,
201
+ draft: true
202
+ };
203
+ logger.verbose(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${this.apiBase}${endpoint})`);
204
+ const pr = await this.request(endpoint, {
205
+ method: isUpdate ? "PATCH" : "POST",
206
+ body: JSON.stringify(requestBody)
207
+ });
208
+ if (typeof pr !== "object" || pr === null || !("number" in pr) || typeof pr.number !== "number" || !("title" in pr) || typeof pr.title !== "string" || !("body" in pr) || typeof pr.body !== "string" || !("draft" in pr) || typeof pr.draft !== "boolean" || !("html_url" in pr) || typeof pr.html_url !== "string") throw new TypeError("Pull request data validation failed");
209
+ const action = isUpdate ? "Updated" : "Created";
210
+ logger.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
211
+ return {
212
+ number: pr.number,
213
+ title: pr.title,
214
+ body: pr.body,
215
+ draft: pr.draft,
216
+ html_url: pr.html_url
217
+ };
268
218
  }
269
- }
270
- function createWorkspaceOperations(overrides = {}) {
271
- return { discoverWorkspacePackages: (workspaceRoot, options) => wrapWorkspace("discoverWorkspacePackages", async () => {
272
- if (overrides.discoverWorkspacePackages) return overrides.discoverWorkspacePackages(workspaceRoot, options);
273
- return discoverWorkspacePackages(workspaceRoot, options);
274
- }) };
275
- }
276
- async function findWorkspacePackages(workspaceRoot, options) {
277
- try {
278
- const result = await run("pnpm", [
279
- "-r",
280
- "ls",
281
- "--json"
282
- ], { nodeOptions: {
283
- cwd: workspaceRoot,
284
- stdio: "pipe"
285
- } });
286
- const rawProjects = JSON.parse(result.stdout);
287
- const allPackageNames = new Set(rawProjects.map((p) => p.name));
288
- const excludedPackages = /* @__PURE__ */ new Set();
289
- const promises = rawProjects.map(async (rawProject) => {
290
- const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
291
- const packageJson = JSON.parse(content);
292
- if (!shouldIncludePackage(packageJson, options)) {
293
- excludedPackages.add(rawProject.name);
294
- return null;
295
- }
296
- return {
297
- name: rawProject.name,
298
- version: rawProject.version,
299
- path: rawProject.path,
300
- packageJson,
301
- workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => {
302
- return allPackageNames.has(dep);
303
- }),
304
- workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => {
305
- return allPackageNames.has(dep);
306
- })
307
- };
219
+ async setCommitStatus({ sha, state, targetUrl, description, context }) {
220
+ const endpoint = `/repos/${this.owner}/${this.repo}/statuses/${sha}`;
221
+ logger.verbose(`Setting commit status on ${sha} to ${state} (url: ${this.apiBase}${endpoint})`);
222
+ await this.request(endpoint, {
223
+ method: "POST",
224
+ body: JSON.stringify({
225
+ state,
226
+ target_url: targetUrl,
227
+ description: description || "",
228
+ context
229
+ })
308
230
  });
309
- const packages = await Promise.all(promises);
310
- if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
311
- return packages.filter((pkg) => pkg !== null);
312
- } catch (err) {
313
- logger.error("Error discovering workspace packages:", err);
314
- throw err;
231
+ logger.info(`Commit status set to ${farver.cyan(state)} for ${farver.gray(sha.substring(0, 7))}`);
315
232
  }
316
- }
317
- function shouldIncludePackage(pkg, options) {
318
- if (!options) return true;
319
- if (options.excludePrivate && pkg.private) return false;
320
- if (options.include && options.include.length > 0) {
321
- if (!options.include.includes(pkg.name)) return false;
233
+ async resolveAuthorInfo(info) {
234
+ if (info.login) return info;
235
+ try {
236
+ const q = encodeURIComponent(`${info.email} type:user in:email`);
237
+ const data = await this.request(`/search/users?q=${q}`);
238
+ if (!data.items || data.items.length === 0) return info;
239
+ info.login = data.items[0].login;
240
+ } catch (err) {
241
+ logger.warn(`Failed to resolve author info for email ${info.email}: ${err.message}`);
242
+ }
243
+ if (info.login) return info;
244
+ if (info.commits.length > 0) try {
245
+ const data = await this.request(`/repos/${this.owner}/${this.repo}/commits/${info.commits[0]}`);
246
+ if (data.author && data.author.login) info.login = data.author.login;
247
+ } catch (err) {
248
+ logger.warn(`Failed to resolve author info from commits for email ${info.email}: ${err.message}`);
249
+ }
250
+ return info;
322
251
  }
323
- if (options.exclude?.includes(pkg.name)) return false;
324
- return true;
252
+ };
253
+ function createGitHubClient(options) {
254
+ return new GitHubClient(options);
255
+ }
256
+ function dedentString(str) {
257
+ const lines = str.split("\n");
258
+ const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity);
259
+ return lines.map((line) => minIndent === Infinity ? line : line.slice(minIndent)).join("\n").trim();
260
+ }
261
+ function generatePullRequestBody(updates, body) {
262
+ const eta = new Eta();
263
+ const bodyTemplate = body ? dedentString(body) : DEFAULT_PR_BODY_TEMPLATE;
264
+ return eta.renderString(bodyTemplate, { packages: updates.map((u) => ({
265
+ name: u.package.name,
266
+ currentVersion: u.currentVersion,
267
+ newVersion: u.newVersion,
268
+ bumpType: u.bumpType,
269
+ hasDirectChanges: u.hasDirectChanges
270
+ })) });
325
271
  }
326
272
 
327
273
  //#endregion
@@ -389,6 +335,11 @@ function normalizeReleaseScriptsOptions(options) {
389
335
  githubToken: token,
390
336
  owner,
391
337
  repo,
338
+ githubClient: createGitHubClient({
339
+ owner,
340
+ repo,
341
+ githubToken: token
342
+ }),
392
343
  packages: normalizedPackages,
393
344
  branch: {
394
345
  release: branch.release ?? "release/next",
@@ -414,75 +365,29 @@ function normalizeReleaseScriptsOptions(options) {
414
365
  provenance: npm.provenance ?? true
415
366
  },
416
367
  prompts: {
417
- versions: prompts.versions ?? !isCI,
418
- packages: prompts.packages ?? !isCI
419
- }
420
- };
421
- }
422
-
423
- //#endregion
424
- //#region src/operations/changelog-format.ts
425
- function formatCommitLine({ commit, owner, repo, authors }) {
426
- const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.hash}`;
427
- let line = `${commit.description}`;
428
- const references = commit.references ?? [];
429
- for (const ref of references) {
430
- if (!ref.value) continue;
431
- const number = Number.parseInt(ref.value.replace(/^#/, ""), 10);
432
- if (Number.isNaN(number)) continue;
433
- if (ref.type === "issue") {
434
- line += ` ([Issue ${ref.value}](https://github.com/${owner}/${repo}/issues/${number}))`;
435
- continue;
368
+ versions: prompts.versions ?? !isCI,
369
+ packages: prompts.packages ?? !isCI
436
370
  }
437
- line += ` ([PR ${ref.value}](https://github.com/${owner}/${repo}/pull/${number}))`;
438
- }
439
- line += ` ([${commit.shortHash}](${commitUrl}))`;
440
- if (authors.length > 0) {
441
- const authorList = authors.map((author) => author.login ? `[@${author.login}](https://github.com/${author.login})` : author.name).join(", ");
442
- line += ` (by ${authorList})`;
443
- }
444
- return line;
371
+ };
445
372
  }
446
- function buildTemplateGroups(options) {
447
- const { commits, owner, repo, types, commitAuthors } = options;
448
- const grouped = groupByType(commits, {
449
- includeNonConventional: false,
450
- mergeKeys: Object.fromEntries(Object.entries(types).map(([key, value]) => [key, value.types ?? [key]]))
451
- });
452
- return Object.entries(types).map(([key, value]) => {
453
- const formattedCommits = (grouped.get(key) ?? []).map((commit) => ({ line: formatCommitLine({
454
- commit,
455
- owner,
456
- repo,
457
- authors: commitAuthors.get(commit.hash) ?? []
458
- }) }));
459
- return {
460
- name: key,
461
- title: value.title,
462
- commits: formattedCommits
463
- };
464
- });
373
+
374
+ //#endregion
375
+ //#region src/types.ts
376
+ function ok(value) {
377
+ return {
378
+ ok: true,
379
+ value
380
+ };
381
+ }
382
+ function err(error) {
383
+ return {
384
+ ok: false,
385
+ error
386
+ };
465
387
  }
466
388
 
467
389
  //#endregion
468
390
  //#region src/core/git.ts
469
- /**
470
- * Check if the working directory is clean (no uncommitted changes)
471
- * @param {string} workspaceRoot - The root directory of the workspace
472
- * @returns {Promise<boolean>} A Promise resolving to true if clean, false otherwise
473
- */
474
- async function isWorkingDirectoryClean(workspaceRoot) {
475
- try {
476
- if ((await run("git", ["status", "--porcelain"], { nodeOptions: {
477
- cwd: workspaceRoot,
478
- stdio: "pipe"
479
- } })).stdout.trim() !== "") return false;
480
- return true;
481
- } catch (err) {
482
- logger.error("Error checking git status:", err);
483
- return false;
484
- }
485
- }
486
391
  function toGitError(operation, error) {
487
392
  return {
488
393
  type: "git",
@@ -491,71 +396,16 @@ function toGitError(operation, error) {
491
396
  stderr: (typeof error === "object" && error && "stderr" in error ? String(error.stderr ?? "") : void 0)?.trim() || void 0
492
397
  };
493
398
  }
494
- async function wrapGit(operation, fn) {
399
+ async function isWorkingDirectoryClean(workspaceRoot) {
495
400
  try {
496
- return ok(await fn());
401
+ return ok((await run("git", ["status", "--porcelain"], { nodeOptions: {
402
+ cwd: workspaceRoot,
403
+ stdio: "pipe"
404
+ } })).stdout.trim() === "");
497
405
  } catch (error) {
498
- return err(toGitError(operation, error));
406
+ return err(toGitError("isWorkingDirectoryClean", error));
499
407
  }
500
408
  }
501
- function createGitOperations(overrides = {}) {
502
- return {
503
- isWorkingDirectoryClean: (workspaceRoot) => wrapGit("isWorkingDirectoryClean", async () => {
504
- if (overrides.isWorkingDirectoryClean) return overrides.isWorkingDirectoryClean(workspaceRoot);
505
- return isWorkingDirectoryClean(workspaceRoot);
506
- }),
507
- doesBranchExist: (branch, workspaceRoot) => wrapGit("doesBranchExist", async () => {
508
- if (overrides.doesBranchExist) return overrides.doesBranchExist(branch, workspaceRoot);
509
- return doesBranchExist(branch, workspaceRoot);
510
- }),
511
- getCurrentBranch: (workspaceRoot) => wrapGit("getCurrentBranch", async () => {
512
- if (overrides.getCurrentBranch) return overrides.getCurrentBranch(workspaceRoot);
513
- return getCurrentBranch(workspaceRoot);
514
- }),
515
- checkoutBranch: (branch, workspaceRoot) => wrapGit("checkoutBranch", async () => {
516
- if (overrides.checkoutBranch) return overrides.checkoutBranch(branch, workspaceRoot);
517
- return checkoutBranch(branch, workspaceRoot);
518
- }),
519
- createBranch: (branch, base, workspaceRoot) => wrapGit("createBranch", async () => {
520
- if (overrides.createBranch) {
521
- await overrides.createBranch(branch, base, workspaceRoot);
522
- return;
523
- }
524
- await createBranch(branch, base, workspaceRoot);
525
- }),
526
- pullLatestChanges: (branch, workspaceRoot) => wrapGit("pullLatestChanges", async () => {
527
- if (overrides.pullLatestChanges) return overrides.pullLatestChanges(branch, workspaceRoot);
528
- return pullLatestChanges(branch, workspaceRoot);
529
- }),
530
- rebaseBranch: (ontoBranch, workspaceRoot) => wrapGit("rebaseBranch", async () => {
531
- if (overrides.rebaseBranch) {
532
- await overrides.rebaseBranch(ontoBranch, workspaceRoot);
533
- return;
534
- }
535
- await rebaseBranch(ontoBranch, workspaceRoot);
536
- }),
537
- isBranchAheadOfRemote: (branch, workspaceRoot) => wrapGit("isBranchAheadOfRemote", async () => {
538
- if (overrides.isBranchAheadOfRemote) return overrides.isBranchAheadOfRemote(branch, workspaceRoot);
539
- return isBranchAheadOfRemote(branch, workspaceRoot);
540
- }),
541
- commitChanges: (message, workspaceRoot) => wrapGit("commitChanges", async () => {
542
- if (overrides.commitChanges) return overrides.commitChanges(message, workspaceRoot);
543
- return commitChanges(message, workspaceRoot);
544
- }),
545
- pushBranch: (branch, workspaceRoot, options) => wrapGit("pushBranch", async () => {
546
- if (overrides.pushBranch) return overrides.pushBranch(branch, workspaceRoot, options);
547
- return pushBranch(branch, workspaceRoot, options);
548
- }),
549
- readFileFromGit: (workspaceRoot, ref, filePath) => wrapGit("readFileFromGit", async () => {
550
- if (overrides.readFileFromGit) return overrides.readFileFromGit(workspaceRoot, ref, filePath);
551
- return readFileFromGit(workspaceRoot, ref, filePath);
552
- }),
553
- getMostRecentPackageTag: (workspaceRoot, packageName) => wrapGit("getMostRecentPackageTag", async () => {
554
- if (overrides.getMostRecentPackageTag) return overrides.getMostRecentPackageTag(workspaceRoot, packageName);
555
- return getMostRecentPackageTag(workspaceRoot, packageName);
556
- })
557
- };
558
- }
559
409
  /**
560
410
  * Check if a git branch exists locally
561
411
  * @param {string} branch - The branch name to check
@@ -572,9 +422,9 @@ async function doesBranchExist(branch, workspaceRoot) {
572
422
  cwd: workspaceRoot,
573
423
  stdio: "pipe"
574
424
  } });
575
- return true;
425
+ return ok(true);
576
426
  } catch {
577
- return false;
427
+ return ok(false);
578
428
  }
579
429
  }
580
430
  /**
@@ -584,17 +434,16 @@ async function doesBranchExist(branch, workspaceRoot) {
584
434
  */
585
435
  async function getCurrentBranch(workspaceRoot) {
586
436
  try {
587
- return (await run("git", [
437
+ return ok((await run("git", [
588
438
  "rev-parse",
589
439
  "--abbrev-ref",
590
440
  "HEAD"
591
441
  ], { nodeOptions: {
592
442
  cwd: workspaceRoot,
593
443
  stdio: "pipe"
594
- } })).stdout.trim();
595
- } catch (err) {
596
- logger.error("Error getting current branch:", err);
597
- throw err;
444
+ } })).stdout.trim());
445
+ } catch (error) {
446
+ return err(toGitError("getCurrentBranch", error));
598
447
  }
599
448
  }
600
449
  /**
@@ -615,8 +464,9 @@ async function createBranch(branch, base, workspaceRoot) {
615
464
  cwd: workspaceRoot,
616
465
  stdio: "pipe"
617
466
  } });
618
- } catch {
619
- exitWithError(`Failed to create branch: ${branch}`, `Make sure the branch doesn't already exist and you have a clean working directory`);
467
+ return ok(void 0);
468
+ } catch (error) {
469
+ return err(toGitError("createBranch", error));
620
470
  }
621
471
  }
622
472
  async function checkoutBranch(branch, workspaceRoot) {
@@ -628,11 +478,11 @@ async function checkoutBranch(branch, workspaceRoot) {
628
478
  } })).stderr.trim().match(/Switched to branch '(.+)'/);
629
479
  if (match && match[1] === branch) {
630
480
  logger.info(`Successfully switched to branch: ${farver.green(branch)}`);
631
- return true;
481
+ return ok(true);
632
482
  }
633
- return false;
634
- } catch {
635
- return false;
483
+ return ok(false);
484
+ } catch (error) {
485
+ return err(toGitError("checkoutBranch", error));
636
486
  }
637
487
  }
638
488
  async function pullLatestChanges(branch, workspaceRoot) {
@@ -645,9 +495,9 @@ async function pullLatestChanges(branch, workspaceRoot) {
645
495
  cwd: workspaceRoot,
646
496
  stdio: "pipe"
647
497
  } });
648
- return true;
649
- } catch {
650
- return false;
498
+ return ok(true);
499
+ } catch (error) {
500
+ return err(toGitError("pullLatestChanges", error));
651
501
  }
652
502
  }
653
503
  async function rebaseBranch(ontoBranch, workspaceRoot) {
@@ -657,9 +507,9 @@ async function rebaseBranch(ontoBranch, workspaceRoot) {
657
507
  cwd: workspaceRoot,
658
508
  stdio: "pipe"
659
509
  } });
660
- return true;
661
- } catch {
662
- exitWithError(`Failed to rebase onto: ${ontoBranch}`, `You may have merge conflicts. Run 'git rebase --abort' to undo the rebase`);
510
+ return ok(void 0);
511
+ } catch (error) {
512
+ return err(toGitError("rebaseBranch", error));
663
513
  }
664
514
  }
665
515
  async function isBranchAheadOfRemote(branch, workspaceRoot) {
@@ -672,9 +522,9 @@ async function isBranchAheadOfRemote(branch, workspaceRoot) {
672
522
  cwd: workspaceRoot,
673
523
  stdio: "pipe"
674
524
  } });
675
- return Number.parseInt(result.stdout.trim(), 10) > 0;
525
+ return ok(Number.parseInt(result.stdout.trim(), 10) > 0);
676
526
  } catch {
677
- return true;
527
+ return ok(true);
678
528
  }
679
529
  }
680
530
  async function commitChanges(message, workspaceRoot) {
@@ -683,7 +533,8 @@ async function commitChanges(message, workspaceRoot) {
683
533
  cwd: workspaceRoot,
684
534
  stdio: "pipe"
685
535
  } });
686
- if (await isWorkingDirectoryClean(workspaceRoot)) return false;
536
+ const isClean = await isWorkingDirectoryClean(workspaceRoot);
537
+ if (!isClean.ok || isClean.value) return ok(false);
687
538
  logger.info(`Committing changes: ${farver.dim(message)}`);
688
539
  await runIfNotDry("git", [
689
540
  "commit",
@@ -693,9 +544,9 @@ async function commitChanges(message, workspaceRoot) {
693
544
  cwd: workspaceRoot,
694
545
  stdio: "pipe"
695
546
  } });
696
- return true;
697
- } catch {
698
- exitWithError(`Failed to commit changes`, `Make sure you have git configured properly with user.name and user.email`);
547
+ return ok(true);
548
+ } catch (error) {
549
+ return err(toGitError("commitChanges", error));
699
550
  }
700
551
  }
701
552
  async function pushBranch(branch, workspaceRoot, options) {
@@ -706,6 +557,14 @@ async function pushBranch(branch, workspaceRoot, options) {
706
557
  branch
707
558
  ];
708
559
  if (options?.forceWithLease) {
560
+ await run("git", [
561
+ "fetch",
562
+ "origin",
563
+ branch
564
+ ], { nodeOptions: {
565
+ cwd: workspaceRoot,
566
+ stdio: "pipe"
567
+ } });
709
568
  args.push("--force-with-lease");
710
569
  logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`);
711
570
  } else if (options?.force) {
@@ -716,19 +575,19 @@ async function pushBranch(branch, workspaceRoot, options) {
716
575
  cwd: workspaceRoot,
717
576
  stdio: "pipe"
718
577
  } });
719
- return true;
720
- } catch {
721
- exitWithError(`Failed to push branch: ${branch}`, `Make sure you have permission to push to the remote repository`);
578
+ return ok(true);
579
+ } catch (error) {
580
+ return err(toGitError("pushBranch", error));
722
581
  }
723
582
  }
724
583
  async function readFileFromGit(workspaceRoot, ref, filePath) {
725
584
  try {
726
- return (await run("git", ["show", `${ref}:${filePath}`], { nodeOptions: {
585
+ return ok((await run("git", ["show", `${ref}:${filePath}`], { nodeOptions: {
727
586
  cwd: workspaceRoot,
728
587
  stdio: "pipe"
729
- } })).stdout;
588
+ } })).stdout);
730
589
  } catch {
731
- return null;
590
+ return ok(null);
732
591
  }
733
592
  }
734
593
  async function getMostRecentPackageTag(workspaceRoot, packageName) {
@@ -742,11 +601,10 @@ async function getMostRecentPackageTag(workspaceRoot, packageName) {
742
601
  stdio: "pipe"
743
602
  } });
744
603
  const tags = stdout.split("\n").map((tag) => tag.trim()).filter(Boolean);
745
- if (tags.length === 0) return;
746
- return tags.reverse()[0];
747
- } catch (err) {
748
- logger.warn(`Failed to get tags for package ${packageName}: ${err.message}`);
749
- return;
604
+ if (tags.length === 0) return ok(void 0);
605
+ return ok(tags.reverse()[0]);
606
+ } catch (error) {
607
+ return err(toGitError("getMostRecentPackageTag", error));
750
608
  }
751
609
  }
752
610
  /**
@@ -793,9 +651,9 @@ async function getGroupedFilesByCommitSha(workspaceRoot, from, to) {
793
651
  if (currentSha === null) continue;
794
652
  commitsMap.get(currentSha).push(trimmedLine);
795
653
  }
796
- return commitsMap;
797
- } catch {
798
- return null;
654
+ return ok(commitsMap);
655
+ } catch (error) {
656
+ return err(toGitError("getGroupedFilesByCommitSha", error));
799
657
  }
800
658
  }
801
659
 
@@ -833,7 +691,7 @@ async function updateChangelog(options) {
833
691
  const changelogPath = join(workspacePackage.path, "CHANGELOG.md");
834
692
  const changelogRelativePath = relative(normalizedOptions.workspaceRoot, join(workspacePackage.path, "CHANGELOG.md"));
835
693
  const existingContent = await readFileFromGit(normalizedOptions.workspaceRoot, normalizedOptions.branch.default, changelogRelativePath);
836
- logger.verbose("Existing content found: ", Boolean(existingContent));
694
+ logger.verbose("Existing content found: ", existingContent.ok && Boolean(existingContent.value));
837
695
  const newEntry = await generateChangelogEntry({
838
696
  packageName: workspacePackage.name,
839
697
  version,
@@ -847,13 +705,13 @@ async function updateChangelog(options) {
847
705
  githubClient
848
706
  });
849
707
  let updatedContent;
850
- if (!existingContent) {
708
+ if (!existingContent.ok || !existingContent.value) {
851
709
  updatedContent = `# ${workspacePackage.name}\n\n${newEntry}\n`;
852
710
  await writeFile(changelogPath, updatedContent, "utf-8");
853
711
  return;
854
712
  }
855
- const parsed = parseChangelog(existingContent);
856
- const lines = existingContent.split("\n");
713
+ const parsed = parseChangelog(existingContent.value);
714
+ const lines = existingContent.value.split("\n");
857
715
  const existingVersionIndex = parsed.versions.findIndex((v) => v.version === version);
858
716
  if (existingVersionIndex !== -1) {
859
717
  const existingVersion = parsed.versions[existingVersionIndex];
@@ -944,269 +802,252 @@ function parseChangelog(content) {
944
802
  }
945
803
 
946
804
  //#endregion
947
- //#region src/core/github.ts
948
- var GitHubClient = class {
949
- owner;
950
- repo;
951
- githubToken;
952
- apiBase = "https://api.github.com";
953
- constructor({ owner, repo, githubToken }) {
954
- this.owner = owner;
955
- this.repo = repo;
956
- this.githubToken = githubToken;
957
- }
958
- async request(path, init = {}) {
959
- const url = path.startsWith("http") ? path : `${this.apiBase}${path}`;
960
- const res = await fetch(url, {
961
- ...init,
962
- headers: {
963
- ...init.headers,
964
- "Accept": "application/vnd.github.v3+json",
965
- "Authorization": `token ${this.githubToken}`,
966
- "User-Agent": "ucdjs-release-scripts (+https://github.com/ucdjs/ucdjs-release-scripts)"
967
- }
968
- });
969
- if (!res.ok) {
970
- const errorText = await res.text();
971
- throw new Error(`GitHub API request failed with status ${res.status}: ${errorText || "No response body"}`);
972
- }
973
- if (res.status === 204) return;
974
- return res.json();
975
- }
976
- async getExistingPullRequest(branch) {
977
- const head = branch.includes(":") ? branch : `${this.owner}:${branch}`;
978
- const endpoint = `/repos/${this.owner}/${this.repo}/pulls?state=open&head=${encodeURIComponent(head)}`;
979
- logger.verbose(`Requesting pull request for branch: ${branch} (url: ${this.apiBase}${endpoint})`);
980
- const pulls = await this.request(endpoint);
981
- if (!Array.isArray(pulls) || pulls.length === 0) return null;
982
- const firstPullRequest = pulls[0];
983
- if (typeof firstPullRequest !== "object" || firstPullRequest === null || !("number" in firstPullRequest) || typeof firstPullRequest.number !== "number" || !("title" in firstPullRequest) || typeof firstPullRequest.title !== "string" || !("body" in firstPullRequest) || typeof firstPullRequest.body !== "string" || !("draft" in firstPullRequest) || typeof firstPullRequest.draft !== "boolean" || !("html_url" in firstPullRequest) || typeof firstPullRequest.html_url !== "string") throw new TypeError("Pull request data validation failed");
984
- const pullRequest = {
985
- number: firstPullRequest.number,
986
- title: firstPullRequest.title,
987
- body: firstPullRequest.body,
988
- draft: firstPullRequest.draft,
989
- html_url: firstPullRequest.html_url,
990
- head: "head" in firstPullRequest && typeof firstPullRequest.head === "object" && firstPullRequest.head !== null && "sha" in firstPullRequest.head && typeof firstPullRequest.head.sha === "string" ? { sha: firstPullRequest.head.sha } : void 0
991
- };
992
- logger.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
993
- return pullRequest;
994
- }
995
- async upsertPullRequest({ title, body, head, base, pullNumber }) {
996
- const isUpdate = typeof pullNumber === "number";
997
- const endpoint = isUpdate ? `/repos/${this.owner}/${this.repo}/pulls/${pullNumber}` : `/repos/${this.owner}/${this.repo}/pulls`;
998
- const requestBody = isUpdate ? {
999
- title,
1000
- body
1001
- } : {
1002
- title,
1003
- body,
1004
- head,
1005
- base,
1006
- draft: true
1007
- };
1008
- logger.verbose(`${isUpdate ? "Updating" : "Creating"} pull request (url: ${this.apiBase}${endpoint})`);
1009
- const pr = await this.request(endpoint, {
1010
- method: isUpdate ? "PATCH" : "POST",
1011
- body: JSON.stringify(requestBody)
1012
- });
1013
- if (typeof pr !== "object" || pr === null || !("number" in pr) || typeof pr.number !== "number" || !("title" in pr) || typeof pr.title !== "string" || !("body" in pr) || typeof pr.body !== "string" || !("draft" in pr) || typeof pr.draft !== "boolean" || !("html_url" in pr) || typeof pr.html_url !== "string") throw new TypeError("Pull request data validation failed");
1014
- const action = isUpdate ? "Updated" : "Created";
1015
- logger.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
1016
- return {
1017
- number: pr.number,
1018
- title: pr.title,
1019
- body: pr.body,
1020
- draft: pr.draft,
1021
- html_url: pr.html_url
1022
- };
1023
- }
1024
- async setCommitStatus({ sha, state, targetUrl, description, context }) {
1025
- const endpoint = `/repos/${this.owner}/${this.repo}/statuses/${sha}`;
1026
- logger.verbose(`Setting commit status on ${sha} to ${state} (url: ${this.apiBase}${endpoint})`);
1027
- await this.request(endpoint, {
1028
- method: "POST",
1029
- body: JSON.stringify({
1030
- state,
1031
- target_url: targetUrl,
1032
- description: description || "",
1033
- context
1034
- })
1035
- });
1036
- logger.info(`Commit status set to ${farver.cyan(state)} for ${farver.gray(sha.substring(0, 7))}`);
805
+ //#region src/operations/semver.ts
806
+ function isValidSemver(version) {
807
+ return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
808
+ }
809
+ function getNextVersion(currentVersion, bump) {
810
+ if (bump === "none") return currentVersion;
811
+ if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
812
+ const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
813
+ if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
814
+ const [, major, minor, patch] = match;
815
+ let newMajor = Number.parseInt(major, 10);
816
+ let newMinor = Number.parseInt(minor, 10);
817
+ let newPatch = Number.parseInt(patch, 10);
818
+ switch (bump) {
819
+ case "major":
820
+ newMajor += 1;
821
+ newMinor = 0;
822
+ newPatch = 0;
823
+ break;
824
+ case "minor":
825
+ newMinor += 1;
826
+ newPatch = 0;
827
+ break;
828
+ case "patch":
829
+ newPatch += 1;
830
+ break;
1037
831
  }
1038
- async resolveAuthorInfo(info) {
1039
- if (info.login) return info;
1040
- try {
1041
- const q = encodeURIComponent(`${info.email} type:user in:email`);
1042
- const data = await this.request(`/search/users?q=${q}`);
1043
- if (!data.items || data.items.length === 0) return info;
1044
- info.login = data.items[0].login;
1045
- } catch (err) {
1046
- logger.warn(`Failed to resolve author info for email ${info.email}: ${err.message}`);
1047
- }
1048
- if (info.login) return info;
1049
- if (info.commits.length > 0) try {
1050
- const data = await this.request(`/repos/${this.owner}/${this.repo}/commits/${info.commits[0]}`);
1051
- if (data.author && data.author.login) info.login = data.author.login;
1052
- } catch (err) {
1053
- logger.warn(`Failed to resolve author info from commits for email ${info.email}: ${err.message}`);
832
+ return `${newMajor}.${newMinor}.${newPatch}`;
833
+ }
834
+ function calculateBumpType(oldVersion, newVersion) {
835
+ if (!isValidSemver(oldVersion) || !isValidSemver(newVersion)) throw new Error(`Cannot calculate bump type for invalid semver: ${oldVersion} or ${newVersion}`);
836
+ const oldParts = oldVersion.split(".").map(Number);
837
+ const newParts = newVersion.split(".").map(Number);
838
+ if (newParts[0] > oldParts[0]) return "major";
839
+ if (newParts[1] > oldParts[1]) return "minor";
840
+ if (newParts[2] > oldParts[2]) return "patch";
841
+ return "none";
842
+ }
843
+
844
+ //#endregion
845
+ //#region src/core/prompts.ts
846
+ async function selectPackagePrompt(packages) {
847
+ const response = await prompts({
848
+ type: "multiselect",
849
+ name: "selectedPackages",
850
+ message: "Select packages to release",
851
+ choices: packages.map((pkg) => ({
852
+ title: `${pkg.name} (${farver.bold(pkg.version)})`,
853
+ value: pkg.name,
854
+ selected: true
855
+ })),
856
+ min: 1,
857
+ hint: "Space to select/deselect. Return to submit.",
858
+ instructions: false
859
+ });
860
+ if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
861
+ return response.selectedPackages;
862
+ }
863
+ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggestedVersion) {
864
+ const answers = await prompts([{
865
+ type: "autocomplete",
866
+ name: "version",
867
+ message: `${pkg.name}: ${farver.green(pkg.version)}`,
868
+ choices: [
869
+ {
870
+ value: "skip",
871
+ title: `skip ${farver.dim("(no change)")}`
872
+ },
873
+ {
874
+ value: "major",
875
+ title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}`
876
+ },
877
+ {
878
+ value: "minor",
879
+ title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}`
880
+ },
881
+ {
882
+ value: "patch",
883
+ title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
884
+ },
885
+ {
886
+ value: "suggested",
887
+ title: `suggested ${farver.bold(suggestedVersion)}`
888
+ },
889
+ {
890
+ value: "as-is",
891
+ title: `as-is ${farver.dim("(keep current version)")}`
892
+ },
893
+ {
894
+ value: "custom",
895
+ title: "custom"
896
+ }
897
+ ],
898
+ initial: suggestedVersion === currentVersion ? 0 : 4
899
+ }, {
900
+ type: (prev) => prev === "custom" ? "text" : null,
901
+ name: "custom",
902
+ message: "Enter the new version number:",
903
+ initial: suggestedVersion,
904
+ validate: (custom) => {
905
+ if (isValidSemver(custom)) return true;
906
+ return "That's not a valid version number";
1054
907
  }
1055
- return info;
1056
- }
1057
- };
1058
- function createGitHubClient(options) {
1059
- return new GitHubClient(options);
908
+ }]);
909
+ if (!answers.version) return null;
910
+ if (answers.version === "skip") return null;
911
+ else if (answers.version === "suggested") return suggestedVersion;
912
+ else if (answers.version === "custom") {
913
+ if (!answers.custom) return null;
914
+ return answers.custom;
915
+ } else if (answers.version === "as-is") return currentVersion;
916
+ else return getNextVersion(pkg.version, answers.version);
1060
917
  }
1061
- function toGitHubError(operation, error) {
918
+
919
+ //#endregion
920
+ //#region src/core/workspace.ts
921
+ function toWorkspaceError(operation, error) {
1062
922
  return {
1063
- type: "github",
923
+ type: "workspace",
1064
924
  operation,
1065
925
  message: error instanceof Error ? error.message : String(error)
1066
926
  };
1067
927
  }
1068
- async function wrapGitHub(operation, fn) {
928
+ async function discoverWorkspacePackages(workspaceRoot, options) {
929
+ let workspaceOptions;
930
+ let explicitPackages;
931
+ if (options.packages == null || options.packages === true) workspaceOptions = { excludePrivate: false };
932
+ else if (Array.isArray(options.packages)) {
933
+ workspaceOptions = {
934
+ excludePrivate: false,
935
+ include: options.packages
936
+ };
937
+ explicitPackages = options.packages;
938
+ } else {
939
+ workspaceOptions = options.packages;
940
+ if (options.packages.include) explicitPackages = options.packages.include;
941
+ }
942
+ let workspacePackages;
1069
943
  try {
1070
- return ok(await fn());
944
+ workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
1071
945
  } catch (error) {
1072
- return err(toGitHubError(operation, error));
946
+ return err(toWorkspaceError("discoverWorkspacePackages", error));
947
+ }
948
+ if (explicitPackages) {
949
+ const foundNames = new Set(workspacePackages.map((p) => p.name));
950
+ const missing = explicitPackages.filter((p) => !foundNames.has(p));
951
+ if (missing.length > 0) exitWithError(`Package${missing.length > 1 ? "s" : ""} not found in workspace: ${missing.join(", ")}`, "Check your package names or run 'pnpm ls' to see available packages");
952
+ }
953
+ const isPackagePromptEnabled = options.prompts?.packages !== false;
954
+ if (!isCI && isPackagePromptEnabled && !explicitPackages) {
955
+ const selectedNames = await selectPackagePrompt(workspacePackages);
956
+ workspacePackages = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
1073
957
  }
958
+ return ok(workspacePackages);
1074
959
  }
1075
- function createGitHubOperations(options, overrides = {}) {
1076
- const client = createGitHubClient(options);
1077
- return {
1078
- getExistingPullRequest: (branch) => wrapGitHub("getExistingPullRequest", async () => {
1079
- if (overrides.getExistingPullRequest) return overrides.getExistingPullRequest(branch);
1080
- return client.getExistingPullRequest(branch);
1081
- }),
1082
- upsertPullRequest: (input) => wrapGitHub("upsertPullRequest", async () => {
1083
- if (overrides.upsertPullRequest) return overrides.upsertPullRequest(input);
1084
- return client.upsertPullRequest(input);
1085
- }),
1086
- setCommitStatus: (input) => wrapGitHub("setCommitStatus", async () => {
1087
- if (overrides.setCommitStatus) {
1088
- await overrides.setCommitStatus(input);
1089
- return;
960
+ async function findWorkspacePackages(workspaceRoot, options) {
961
+ try {
962
+ const result = await run("pnpm", [
963
+ "-r",
964
+ "ls",
965
+ "--json"
966
+ ], { nodeOptions: {
967
+ cwd: workspaceRoot,
968
+ stdio: "pipe"
969
+ } });
970
+ const rawProjects = JSON.parse(result.stdout);
971
+ const allPackageNames = new Set(rawProjects.map((p) => p.name));
972
+ const excludedPackages = /* @__PURE__ */ new Set();
973
+ const promises = rawProjects.map(async (rawProject) => {
974
+ const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
975
+ const packageJson = JSON.parse(content);
976
+ if (!shouldIncludePackage(packageJson, options)) {
977
+ excludedPackages.add(rawProject.name);
978
+ return null;
1090
979
  }
1091
- await client.setCommitStatus(input);
1092
- }),
1093
- resolveAuthorInfo: (info) => wrapGitHub("resolveAuthorInfo", async () => {
1094
- if (overrides.resolveAuthorInfo) return overrides.resolveAuthorInfo(info);
1095
- return client.resolveAuthorInfo(info);
1096
- })
1097
- };
1098
- }
1099
- function dedentString(str) {
1100
- const lines = str.split("\n");
1101
- const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity);
1102
- return lines.map((line) => minIndent === Infinity ? line : line.slice(minIndent)).join("\n").trim();
980
+ return {
981
+ name: rawProject.name,
982
+ version: rawProject.version,
983
+ path: rawProject.path,
984
+ packageJson,
985
+ workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => {
986
+ return allPackageNames.has(dep);
987
+ }),
988
+ workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => {
989
+ return allPackageNames.has(dep);
990
+ })
991
+ };
992
+ });
993
+ const packages = await Promise.all(promises);
994
+ if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
995
+ return packages.filter((pkg) => pkg !== null);
996
+ } catch (err) {
997
+ logger.error("Error discovering workspace packages:", err);
998
+ throw err;
999
+ }
1103
1000
  }
1104
- function generatePullRequestBody(updates, body) {
1105
- const eta = new Eta();
1106
- const bodyTemplate = body ? dedentString(body) : DEFAULT_PR_BODY_TEMPLATE;
1107
- return eta.renderString(bodyTemplate, { packages: updates.map((u) => ({
1108
- name: u.package.name,
1109
- currentVersion: u.currentVersion,
1110
- newVersion: u.newVersion,
1111
- bumpType: u.bumpType,
1112
- hasDirectChanges: u.hasDirectChanges
1113
- })) });
1001
+ function shouldIncludePackage(pkg, options) {
1002
+ if (!options) return true;
1003
+ if (options.excludePrivate && pkg.private) return false;
1004
+ if (options.include && options.include.length > 0) {
1005
+ if (!options.include.includes(pkg.name)) return false;
1006
+ }
1007
+ if (options.exclude?.includes(pkg.name)) return false;
1008
+ return true;
1114
1009
  }
1115
1010
 
1116
1011
  //#endregion
1117
1012
  //#region src/operations/branch.ts
1118
1013
  async function prepareReleaseBranch(options) {
1119
- const { git, workspaceRoot, releaseBranch, defaultBranch } = options;
1120
- const currentBranch = await git.getCurrentBranch(workspaceRoot);
1014
+ const { workspaceRoot, releaseBranch, defaultBranch } = options;
1015
+ const currentBranch = await getCurrentBranch(workspaceRoot);
1121
1016
  if (!currentBranch.ok) return currentBranch;
1122
1017
  if (currentBranch.value !== defaultBranch) return err({
1123
1018
  type: "git",
1124
1019
  operation: "validateBranch",
1125
1020
  message: `Current branch is '${currentBranch.value}'. Please switch to '${defaultBranch}'.`
1126
1021
  });
1127
- const branchExists = await git.doesBranchExist(releaseBranch, workspaceRoot);
1022
+ const branchExists = await doesBranchExist(releaseBranch, workspaceRoot);
1128
1023
  if (!branchExists.ok) return branchExists;
1129
1024
  if (!branchExists.value) {
1130
- const created = await git.createBranch(releaseBranch, defaultBranch, workspaceRoot);
1025
+ const created = await createBranch(releaseBranch, defaultBranch, workspaceRoot);
1131
1026
  if (!created.ok) return created;
1132
1027
  }
1133
- const checkedOut = await git.checkoutBranch(releaseBranch, workspaceRoot);
1028
+ const checkedOut = await checkoutBranch(releaseBranch, workspaceRoot);
1134
1029
  if (!checkedOut.ok) return checkedOut;
1135
1030
  if (branchExists.value) {
1136
- const pulled = await git.pullLatestChanges(releaseBranch, workspaceRoot);
1031
+ const pulled = await pullLatestChanges(releaseBranch, workspaceRoot);
1137
1032
  if (!pulled.ok) return pulled;
1138
1033
  if (!pulled.value) logger.warn("Failed to pull latest changes, continuing anyway.");
1139
1034
  }
1140
- const rebased = await git.rebaseBranch(defaultBranch, workspaceRoot);
1035
+ const rebased = await rebaseBranch(defaultBranch, workspaceRoot);
1141
1036
  if (!rebased.ok) return rebased;
1142
1037
  return ok(void 0);
1143
1038
  }
1144
1039
  async function syncReleaseChanges(options) {
1145
- const { git, workspaceRoot, releaseBranch, commitMessage, hasChanges } = options;
1146
- const committed = hasChanges ? await git.commitChanges(commitMessage, workspaceRoot) : ok(false);
1040
+ const { workspaceRoot, releaseBranch, commitMessage, hasChanges } = options;
1041
+ const committed = hasChanges ? await commitChanges(commitMessage, workspaceRoot) : ok(false);
1147
1042
  if (!committed.ok) return committed;
1148
- const isAhead = await git.isBranchAheadOfRemote(releaseBranch, workspaceRoot);
1043
+ const isAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
1149
1044
  if (!isAhead.ok) return isAhead;
1150
1045
  if (!committed.value && !isAhead.value) return ok(false);
1151
- const pushed = await git.pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
1046
+ const pushed = await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
1152
1047
  if (!pushed.ok) return pushed;
1153
1048
  return ok(true);
1154
1049
  }
1155
1050
 
1156
- //#endregion
1157
- //#region src/operations/calculate.ts
1158
- async function calculateUpdates(options) {
1159
- const { versioning, workspacePackages, workspaceRoot, showPrompt, overrides, globalCommitMode } = options;
1160
- const grouped = await versioning.getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages);
1161
- if (!grouped.ok) return grouped;
1162
- const global = await versioning.getGlobalCommitsPerPackage(workspaceRoot, grouped.value, workspacePackages, globalCommitMode);
1163
- if (!global.ok) return global;
1164
- const updates = await versioning.calculateAndPrepareVersionUpdates({
1165
- workspacePackages,
1166
- packageCommits: grouped.value,
1167
- workspaceRoot,
1168
- showPrompt,
1169
- globalCommitsPerPackage: global.value,
1170
- overrides
1171
- });
1172
- if (!updates.ok) return updates;
1173
- return updates;
1174
- }
1175
- function ensureHasPackages(packages) {
1176
- if (packages.length === 0) return err({
1177
- type: "git",
1178
- operation: "discoverPackages",
1179
- message: "No packages found to release"
1180
- });
1181
- return {
1182
- ok: true,
1183
- value: packages
1184
- };
1185
- }
1186
-
1187
- //#endregion
1188
- //#region src/operations/pr.ts
1189
- async function syncPullRequest(options) {
1190
- const { github, releaseBranch, defaultBranch, pullRequestTitle, pullRequestBody, updates } = options;
1191
- const existing = await github.getExistingPullRequest(releaseBranch);
1192
- if (!existing.ok) return existing;
1193
- const doesExist = !!existing.value;
1194
- const title = existing.value?.title || pullRequestTitle || "chore: update package versions";
1195
- const body = generatePullRequestBody(updates, pullRequestBody);
1196
- const pr = await github.upsertPullRequest({
1197
- pullNumber: existing.value?.number,
1198
- title,
1199
- body,
1200
- head: releaseBranch,
1201
- base: defaultBranch
1202
- });
1203
- if (!pr.ok) return pr;
1204
- return ok({
1205
- pullRequest: pr.value,
1206
- created: !doesExist
1207
- });
1208
- }
1209
-
1210
1051
  //#endregion
1211
1052
  //#region src/versioning/commits.ts
1212
1053
  /**
@@ -1220,7 +1061,8 @@ async function syncPullRequest(options) {
1220
1061
  async function getWorkspacePackageGroupedCommits(workspaceRoot, packages) {
1221
1062
  const changedPackages = /* @__PURE__ */ new Map();
1222
1063
  const promises = packages.map(async (pkg) => {
1223
- const lastTag = await getMostRecentPackageTag(workspaceRoot, pkg.name);
1064
+ const lastTagResult = await getMostRecentPackageTag(workspaceRoot, pkg.name);
1065
+ const lastTag = lastTagResult.ok ? lastTagResult.value : void 0;
1224
1066
  const allCommits = await getCommits({
1225
1067
  from: lastTag,
1226
1068
  to: "HEAD",
@@ -1320,17 +1162,17 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
1320
1162
  }
1321
1163
  logger.verbose("Fetching files for commits range", `${farver.cyan(commitRange.oldest)}..${farver.cyan(commitRange.newest)}`);
1322
1164
  const commitFilesMap = await getGroupedFilesByCommitSha(workspaceRoot, commitRange.oldest, commitRange.newest);
1323
- if (!commitFilesMap) {
1165
+ if (!commitFilesMap.ok) {
1324
1166
  logger.warn("Failed to get commit file list, returning empty global commits");
1325
1167
  return result;
1326
1168
  }
1327
- logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.size)} commits in ONE git call`);
1169
+ logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.value.size)} commits in ONE git call`);
1328
1170
  const packagePaths = new Set(allPackages.map((p) => p.path));
1329
1171
  for (const [pkgName, commits] of packageCommits) {
1330
1172
  const globalCommitsAffectingPackage = [];
1331
1173
  logger.verbose("Filtering global commits for package", `${farver.bold(pkgName)} from ${farver.cyan(commits.length)} commits`);
1332
1174
  for (const commit of commits) {
1333
- const files = commitFilesMap.get(commit.shortHash);
1175
+ const files = commitFilesMap.value.get(commit.shortHash);
1334
1176
  if (!files) continue;
1335
1177
  if (isGlobalCommit(workspaceRoot, files, packagePaths)) globalCommitsAffectingPackage.push(commit);
1336
1178
  }
@@ -1341,7 +1183,7 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
1341
1183
  }
1342
1184
  const dependencyCommits = [];
1343
1185
  for (const commit of globalCommitsAffectingPackage) {
1344
- const files = commitFilesMap.get(commit.shortHash);
1186
+ const files = commitFilesMap.value.get(commit.shortHash);
1345
1187
  if (!files) continue;
1346
1188
  if (files.some((file) => DEPENDENCY_FILES.includes(file.startsWith("./") ? file.slice(2) : file))) {
1347
1189
  logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`);
@@ -1663,79 +1505,89 @@ function getDependencyUpdates(pkg, allUpdates) {
1663
1505
  }
1664
1506
 
1665
1507
  //#endregion
1666
- //#region src/versioning/operations.ts
1667
- function createVersioningOperations() {
1668
- return {
1669
- getWorkspacePackageGroupedCommits: async (workspaceRoot, packages) => {
1670
- try {
1671
- return {
1672
- ok: true,
1673
- value: await getWorkspacePackageGroupedCommits(workspaceRoot, packages)
1674
- };
1675
- } catch (error) {
1676
- return {
1677
- ok: false,
1678
- error: {
1679
- type: "git",
1680
- operation: "getWorkspacePackageGroupedCommits",
1681
- message: String(error)
1682
- }
1683
- };
1684
- }
1685
- },
1686
- getGlobalCommitsPerPackage: async (workspaceRoot, packageCommits, packages, mode) => {
1687
- try {
1688
- return {
1689
- ok: true,
1690
- value: await getGlobalCommitsPerPackage(workspaceRoot, packageCommits, packages, mode)
1691
- };
1692
- } catch (error) {
1693
- return {
1694
- ok: false,
1695
- error: {
1696
- type: "git",
1697
- operation: "getGlobalCommitsPerPackage",
1698
- message: String(error)
1699
- }
1700
- };
1508
+ //#region src/operations/calculate.ts
1509
+ async function calculateUpdates(options) {
1510
+ const { workspacePackages, workspaceRoot, showPrompt, overrides, globalCommitMode } = options;
1511
+ try {
1512
+ const grouped = await getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages);
1513
+ return ok(await calculateAndPrepareVersionUpdates({
1514
+ workspacePackages,
1515
+ packageCommits: grouped,
1516
+ workspaceRoot,
1517
+ showPrompt,
1518
+ globalCommitsPerPackage: await getGlobalCommitsPerPackage(workspaceRoot, grouped, workspacePackages, globalCommitMode),
1519
+ overrides
1520
+ }));
1521
+ } catch (error) {
1522
+ return err({
1523
+ type: "git",
1524
+ operation: "calculateUpdates",
1525
+ message: error instanceof Error ? error.message : String(error)
1526
+ });
1527
+ }
1528
+ }
1529
+ function ensureHasPackages(packages) {
1530
+ if (packages.length === 0) return err({
1531
+ type: "git",
1532
+ operation: "discoverWorkspacePackages",
1533
+ message: "No packages found to release"
1534
+ });
1535
+ return ok(packages);
1536
+ }
1537
+
1538
+ //#endregion
1539
+ //#region src/operations/pr.ts
1540
+ async function syncPullRequest(options) {
1541
+ const { github, releaseBranch, defaultBranch, pullRequestTitle, pullRequestBody, updates } = options;
1542
+ let existing = null;
1543
+ try {
1544
+ existing = await github.getExistingPullRequest(releaseBranch);
1545
+ } catch (error) {
1546
+ return {
1547
+ ok: false,
1548
+ error: {
1549
+ type: "github",
1550
+ operation: "getExistingPullRequest",
1551
+ message: error instanceof Error ? error.message : String(error)
1701
1552
  }
1702
- },
1703
- calculateAndPrepareVersionUpdates: async (payload) => {
1704
- try {
1705
- return {
1706
- ok: true,
1707
- value: await calculateAndPrepareVersionUpdates(payload)
1708
- };
1709
- } catch (error) {
1710
- return {
1711
- ok: false,
1712
- error: {
1713
- type: "git",
1714
- operation: "calculateAndPrepareVersionUpdates",
1715
- message: String(error)
1716
- }
1717
- };
1553
+ };
1554
+ }
1555
+ const doesExist = !!existing;
1556
+ const title = existing?.title || pullRequestTitle || "chore: update package versions";
1557
+ const body = generatePullRequestBody(updates, pullRequestBody);
1558
+ let pr = null;
1559
+ try {
1560
+ pr = await github.upsertPullRequest({
1561
+ pullNumber: existing?.number,
1562
+ title,
1563
+ body,
1564
+ head: releaseBranch,
1565
+ base: defaultBranch
1566
+ });
1567
+ } catch (error) {
1568
+ return {
1569
+ ok: false,
1570
+ error: {
1571
+ type: "github",
1572
+ operation: "upsertPullRequest",
1573
+ message: error instanceof Error ? error.message : String(error)
1718
1574
  }
1719
- }
1720
- };
1575
+ };
1576
+ }
1577
+ return ok({
1578
+ pullRequest: pr,
1579
+ created: !doesExist
1580
+ });
1721
1581
  }
1722
1582
 
1723
1583
  //#endregion
1724
1584
  //#region src/workflows/prepare.ts
1725
1585
  async function prepareWorkflow(options) {
1726
- const gitOps = createGitOperations();
1727
- const githubOps = createGitHubOperations({
1728
- owner: options.owner,
1729
- repo: options.repo,
1730
- githubToken: options.githubToken
1731
- });
1732
- const workspaceOps = createWorkspaceOperations();
1733
- const versioningOps = createVersioningOperations();
1734
1586
  if (options.safeguards) {
1735
- const clean = await gitOps.isWorkingDirectoryClean(options.workspaceRoot);
1587
+ const clean = await isWorkingDirectoryClean(options.workspaceRoot);
1736
1588
  if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
1737
1589
  }
1738
- const discovered = await workspaceOps.discoverWorkspacePackages(options.workspaceRoot, options);
1590
+ const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
1739
1591
  if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
1740
1592
  const ensured = ensureHasPackages(discovered.value);
1741
1593
  if (!ensured.ok) {
@@ -1751,7 +1603,6 @@ async function prepareWorkflow(options) {
1751
1603
  }
1752
1604
  logger.emptyLine();
1753
1605
  const prepareBranchResult = await prepareReleaseBranch({
1754
- git: gitOps,
1755
1606
  workspaceRoot: options.workspaceRoot,
1756
1607
  releaseBranch: options.branch.release,
1757
1608
  defaultBranch: options.branch.default
@@ -1767,7 +1618,6 @@ async function prepareWorkflow(options) {
1767
1618
  logger.info("No existing version overrides file found. Continuing...");
1768
1619
  }
1769
1620
  const updatesResult = await calculateUpdates({
1770
- versioning: versioningOps,
1771
1621
  workspacePackages,
1772
1622
  workspaceRoot: options.workspaceRoot,
1773
1623
  showPrompt: options.prompts?.versions !== false,
@@ -1814,13 +1664,11 @@ async function prepareWorkflow(options) {
1814
1664
  await applyUpdates();
1815
1665
  if (options.changelog?.enabled) {
1816
1666
  logger.step("Updating changelogs");
1817
- const groupedPackageCommits = await versioningOps.getWorkspacePackageGroupedCommits(options.workspaceRoot, workspacePackages);
1818
- if (!groupedPackageCommits.ok) exitWithError(groupedPackageCommits.error.message);
1819
- const globalCommitsPerPackage = await versioningOps.getGlobalCommitsPerPackage(options.workspaceRoot, groupedPackageCommits.value, workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode);
1820
- if (!globalCommitsPerPackage.ok) exitWithError(globalCommitsPerPackage.error.message);
1667
+ const groupedPackageCommits = await getWorkspacePackageGroupedCommits(options.workspaceRoot, workspacePackages);
1668
+ const globalCommitsPerPackage = await getGlobalCommitsPerPackage(options.workspaceRoot, groupedPackageCommits, workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode);
1821
1669
  const changelogPromises = allUpdates.map((update) => {
1822
- const pkgCommits = groupedPackageCommits.value.get(update.package.name) || [];
1823
- const globalCommits = globalCommitsPerPackage.value.get(update.package.name) || [];
1670
+ const pkgCommits = groupedPackageCommits.get(update.package.name) || [];
1671
+ const globalCommits = globalCommitsPerPackage.get(update.package.name) || [];
1824
1672
  const allCommits = [...pkgCommits, ...globalCommits];
1825
1673
  if (allCommits.length === 0) {
1826
1674
  logger.verbose(`No commits for ${update.package.name}, skipping changelog`);
@@ -1832,11 +1680,7 @@ async function prepareWorkflow(options) {
1832
1680
  ...options,
1833
1681
  workspaceRoot: options.workspaceRoot
1834
1682
  },
1835
- githubClient: createGitHubClient({
1836
- owner: options.owner,
1837
- repo: options.repo,
1838
- githubToken: options.githubToken
1839
- }),
1683
+ githubClient: options.githubClient,
1840
1684
  workspacePackage: update.package,
1841
1685
  version: update.newVersion,
1842
1686
  previousVersion: update.currentVersion !== "0.0.0" ? update.currentVersion : void 0,
@@ -1848,7 +1692,6 @@ async function prepareWorkflow(options) {
1848
1692
  logger.success(`Updated ${updates.length} changelog(s)`);
1849
1693
  }
1850
1694
  const hasChangesToPush = await syncReleaseChanges({
1851
- git: gitOps,
1852
1695
  workspaceRoot: options.workspaceRoot,
1853
1696
  releaseBranch: options.branch.release,
1854
1697
  commitMessage: "chore: update release versions",
@@ -1857,7 +1700,7 @@ async function prepareWorkflow(options) {
1857
1700
  if (!hasChangesToPush.ok) exitWithError(hasChangesToPush.error.message);
1858
1701
  if (!hasChangesToPush.value) {
1859
1702
  const prResult = await syncPullRequest({
1860
- github: githubOps,
1703
+ github: options.githubClient,
1861
1704
  releaseBranch: options.branch.release,
1862
1705
  defaultBranch: options.branch.default,
1863
1706
  pullRequestTitle: options.pullRequest?.title,
@@ -1877,7 +1720,7 @@ async function prepareWorkflow(options) {
1877
1720
  return null;
1878
1721
  }
1879
1722
  const prResult = await syncPullRequest({
1880
- github: githubOps,
1723
+ github: options.githubClient,
1881
1724
  releaseBranch: options.branch.release,
1882
1725
  defaultBranch: options.branch.default,
1883
1726
  pullRequestTitle: options.pullRequest?.title,
@@ -1889,6 +1732,8 @@ async function prepareWorkflow(options) {
1889
1732
  logger.section("🚀 Pull Request");
1890
1733
  logger.success(`Pull request ${prResult.value.created ? "created" : "updated"}: ${prResult.value.pullRequest.html_url}`);
1891
1734
  }
1735
+ const returnToDefault = await checkoutBranch(options.branch.default, options.workspaceRoot);
1736
+ if (!returnToDefault.ok || !returnToDefault.value) exitWithError(`Failed to checkout branch: ${options.branch.default}`);
1892
1737
  return {
1893
1738
  updates: allUpdates,
1894
1739
  prUrl: prResult.value.pullRequest?.html_url,
@@ -1896,12 +1741,6 @@ async function prepareWorkflow(options) {
1896
1741
  };
1897
1742
  }
1898
1743
 
1899
- //#endregion
1900
- //#region src/prepare.ts
1901
- async function release(options) {
1902
- return prepareWorkflow(options);
1903
- }
1904
-
1905
1744
  //#endregion
1906
1745
  //#region src/workflows/publish.ts
1907
1746
  async function publishWorkflow(options) {
@@ -1909,50 +1748,30 @@ async function publishWorkflow(options) {
1909
1748
  logger.verbose("Publish options:", options);
1910
1749
  }
1911
1750
 
1912
- //#endregion
1913
- //#region src/publish.ts
1914
- async function publish(options) {
1915
- return publishWorkflow(options);
1916
- }
1917
-
1918
- //#endregion
1919
- //#region src/operations/discover.ts
1920
- async function discoverPackages({ workspace, workspaceRoot, options }) {
1921
- return workspace.discoverWorkspacePackages(workspaceRoot, options);
1922
- }
1923
-
1924
1751
  //#endregion
1925
1752
  //#region src/workflows/verify.ts
1926
1753
  async function verifyWorkflow(options) {
1927
- const gitOps = createGitOperations();
1928
- const githubOps = createGitHubOperations({
1929
- owner: options.owner,
1930
- repo: options.repo,
1931
- githubToken: options.githubToken
1932
- });
1933
- const workspaceOps = createWorkspaceOperations();
1934
1754
  if (options.safeguards) {
1935
- const clean = await gitOps.isWorkingDirectoryClean(options.workspaceRoot);
1755
+ const clean = await isWorkingDirectoryClean(options.workspaceRoot);
1936
1756
  if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
1937
1757
  }
1938
1758
  const releaseBranch = options.branch.release;
1939
1759
  const defaultBranch = options.branch.default;
1940
- const releasePr = await githubOps.getExistingPullRequest(releaseBranch);
1941
- if (!releasePr.ok) exitWithError(releasePr.error.message);
1942
- if (!releasePr.value || !releasePr.value.head) {
1760
+ const releasePr = await options.githubClient.getExistingPullRequest(releaseBranch);
1761
+ if (!releasePr || !releasePr.head) {
1943
1762
  logger.warn(`No open release pull request found for branch "${releaseBranch}". Nothing to verify.`);
1944
1763
  return;
1945
1764
  }
1946
- logger.info(`Found release PR #${releasePr.value.number}. Verifying against default branch "${defaultBranch}"...`);
1947
- const originalBranch = await gitOps.getCurrentBranch(options.workspaceRoot);
1765
+ logger.info(`Found release PR #${releasePr.number}. Verifying against default branch "${defaultBranch}"...`);
1766
+ const originalBranch = await getCurrentBranch(options.workspaceRoot);
1948
1767
  if (!originalBranch.ok) exitWithError(originalBranch.error.message);
1949
1768
  if (originalBranch.value !== defaultBranch) {
1950
- const checkout = await gitOps.checkoutBranch(defaultBranch, options.workspaceRoot);
1769
+ const checkout = await checkoutBranch(defaultBranch, options.workspaceRoot);
1951
1770
  if (!checkout.ok || !checkout.value) exitWithError(`Failed to checkout branch: ${defaultBranch}`);
1952
1771
  }
1953
1772
  let existingOverrides = {};
1954
1773
  try {
1955
- const overridesContent = await gitOps.readFileFromGit(options.workspaceRoot, releasePr.value.head.sha, ucdjsReleaseOverridesPath);
1774
+ const overridesContent = await readFileFromGit(options.workspaceRoot, releasePr.head.sha, ucdjsReleaseOverridesPath);
1956
1775
  if (overridesContent.ok && overridesContent.value) {
1957
1776
  existingOverrides = JSON.parse(overridesContent.value);
1958
1777
  logger.info("Found existing version overrides file on release branch.");
@@ -1960,11 +1779,7 @@ async function verifyWorkflow(options) {
1960
1779
  } catch {
1961
1780
  logger.info("No version overrides file found on release branch. Continuing...");
1962
1781
  }
1963
- const discovered = await discoverPackages({
1964
- workspace: workspaceOps,
1965
- workspaceRoot: options.workspaceRoot,
1966
- options
1967
- });
1782
+ const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
1968
1783
  if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
1969
1784
  const ensured = ensureHasPackages(discovered.value);
1970
1785
  if (!ensured.ok) {
@@ -1973,7 +1788,6 @@ async function verifyWorkflow(options) {
1973
1788
  }
1974
1789
  const mainPackages = ensured.value;
1975
1790
  const updatesResult = await calculateUpdates({
1976
- versioning: createVersioningOperations(),
1977
1791
  workspacePackages: mainPackages,
1978
1792
  workspaceRoot: options.workspaceRoot,
1979
1793
  showPrompt: false,
@@ -1986,13 +1800,13 @@ async function verifyWorkflow(options) {
1986
1800
  const prVersionMap = /* @__PURE__ */ new Map();
1987
1801
  for (const pkg of mainPackages) {
1988
1802
  const pkgJsonPath = relative(options.workspaceRoot, join(pkg.path, "package.json"));
1989
- const pkgJsonContent = await gitOps.readFileFromGit(options.workspaceRoot, releasePr.value.head.sha, pkgJsonPath);
1803
+ const pkgJsonContent = await readFileFromGit(options.workspaceRoot, releasePr.head.sha, pkgJsonPath);
1990
1804
  if (pkgJsonContent.ok && pkgJsonContent.value) {
1991
1805
  const pkgJson = JSON.parse(pkgJsonContent.value);
1992
1806
  prVersionMap.set(pkg.name, pkgJson.version);
1993
1807
  }
1994
1808
  }
1995
- if (originalBranch.value !== defaultBranch) await gitOps.checkoutBranch(originalBranch.value, options.workspaceRoot);
1809
+ if (originalBranch.value !== defaultBranch) await checkoutBranch(originalBranch.value, options.workspaceRoot);
1996
1810
  let isOutOfSync = false;
1997
1811
  for (const [pkgName, expectedVersion] of expectedVersionMap.entries()) {
1998
1812
  const prVersion = prVersionMap.get(pkgName);
@@ -2007,51 +1821,49 @@ async function verifyWorkflow(options) {
2007
1821
  }
2008
1822
  const statusContext = "ucdjs/release-verify";
2009
1823
  if (isOutOfSync) {
2010
- await githubOps.setCommitStatus({
2011
- sha: releasePr.value.head.sha,
1824
+ await options.githubClient.setCommitStatus({
1825
+ sha: releasePr.head.sha,
2012
1826
  state: "failure",
2013
1827
  context: statusContext,
2014
1828
  description: "Release PR is out of sync with the default branch. Please re-run the release process."
2015
1829
  });
2016
1830
  logger.error("Verification failed. Commit status set to 'failure'.");
2017
1831
  } else {
2018
- await githubOps.setCommitStatus({
2019
- sha: releasePr.value.head.sha,
1832
+ await options.githubClient.setCommitStatus({
1833
+ sha: releasePr.head.sha,
2020
1834
  state: "success",
2021
1835
  context: statusContext,
2022
1836
  description: "Release PR is up to date.",
2023
- targetUrl: `https://github.com/${options.owner}/${options.repo}/pull/${releasePr.value.number}`
1837
+ targetUrl: `https://github.com/${options.owner}/${options.repo}/pull/${releasePr.number}`
2024
1838
  });
2025
1839
  logger.success("Verification successful. Commit status set to 'success'.");
2026
1840
  }
2027
1841
  }
2028
1842
 
2029
- //#endregion
2030
- //#region src/verify.ts
2031
- async function verify(options) {
2032
- return verifyWorkflow(options);
2033
- }
2034
-
2035
1843
  //#endregion
2036
1844
  //#region src/index.ts
2037
1845
  async function createReleaseScripts(options) {
2038
1846
  const normalizedOptions = normalizeReleaseScriptsOptions(options);
2039
1847
  return {
2040
1848
  async verify() {
2041
- return verify(normalizedOptions);
1849
+ return verifyWorkflow(normalizedOptions);
2042
1850
  },
2043
1851
  async prepare() {
2044
- return release(normalizedOptions);
1852
+ return prepareWorkflow(normalizedOptions);
2045
1853
  },
2046
1854
  async publish() {
2047
- return publish(normalizedOptions);
1855
+ return publishWorkflow(normalizedOptions);
2048
1856
  },
2049
1857
  packages: {
2050
1858
  async list() {
2051
- return discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
1859
+ const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
1860
+ if (!result.ok) throw new Error(result.error.message);
1861
+ return result.value;
2052
1862
  },
2053
1863
  async get(packageName) {
2054
- return (await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions)).find((p) => p.name === packageName);
1864
+ const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
1865
+ if (!result.ok) throw new Error(result.error.message);
1866
+ return result.value.find((p) => p.name === packageName);
2055
1867
  }
2056
1868
  }
2057
1869
  };