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

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",
@@ -411,7 +362,9 @@ function normalizeReleaseScriptsOptions(options) {
411
362
  } : DEFAULT_TYPES,
412
363
  npm: {
413
364
  otp: npm.otp,
414
- provenance: npm.provenance ?? true
365
+ provenance: npm.provenance ?? true,
366
+ access: npm.access ?? "public",
367
+ runBuild: npm.runBuild ?? true
415
368
  },
416
369
  prompts: {
417
370
  versions: prompts.versions ?? !isCI,
@@ -421,68 +374,22 @@ function normalizeReleaseScriptsOptions(options) {
421
374
  }
422
375
 
423
376
  //#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;
436
- }
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;
377
+ //#region src/types.ts
378
+ function ok(value) {
379
+ return {
380
+ ok: true,
381
+ value
382
+ };
445
383
  }
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
- });
384
+ function err(error) {
385
+ return {
386
+ ok: false,
387
+ error
388
+ };
465
389
  }
466
390
 
467
391
  //#endregion
468
392
  //#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
393
  function toGitError(operation, error) {
487
394
  return {
488
395
  type: "git",
@@ -491,71 +398,16 @@ function toGitError(operation, error) {
491
398
  stderr: (typeof error === "object" && error && "stderr" in error ? String(error.stderr ?? "") : void 0)?.trim() || void 0
492
399
  };
493
400
  }
494
- async function wrapGit(operation, fn) {
401
+ async function isWorkingDirectoryClean(workspaceRoot) {
495
402
  try {
496
- return ok(await fn());
403
+ return ok((await run("git", ["status", "--porcelain"], { nodeOptions: {
404
+ cwd: workspaceRoot,
405
+ stdio: "pipe"
406
+ } })).stdout.trim() === "");
497
407
  } catch (error) {
498
- return err(toGitError(operation, error));
408
+ return err(toGitError("isWorkingDirectoryClean", error));
499
409
  }
500
410
  }
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
411
  /**
560
412
  * Check if a git branch exists locally
561
413
  * @param {string} branch - The branch name to check
@@ -572,9 +424,9 @@ async function doesBranchExist(branch, workspaceRoot) {
572
424
  cwd: workspaceRoot,
573
425
  stdio: "pipe"
574
426
  } });
575
- return true;
427
+ return ok(true);
576
428
  } catch {
577
- return false;
429
+ return ok(false);
578
430
  }
579
431
  }
580
432
  /**
@@ -584,17 +436,16 @@ async function doesBranchExist(branch, workspaceRoot) {
584
436
  */
585
437
  async function getCurrentBranch(workspaceRoot) {
586
438
  try {
587
- return (await run("git", [
439
+ return ok((await run("git", [
588
440
  "rev-parse",
589
441
  "--abbrev-ref",
590
442
  "HEAD"
591
443
  ], { nodeOptions: {
592
444
  cwd: workspaceRoot,
593
445
  stdio: "pipe"
594
- } })).stdout.trim();
595
- } catch (err) {
596
- logger.error("Error getting current branch:", err);
597
- throw err;
446
+ } })).stdout.trim());
447
+ } catch (error) {
448
+ return err(toGitError("getCurrentBranch", error));
598
449
  }
599
450
  }
600
451
  /**
@@ -615,8 +466,9 @@ async function createBranch(branch, base, workspaceRoot) {
615
466
  cwd: workspaceRoot,
616
467
  stdio: "pipe"
617
468
  } });
618
- } catch {
619
- exitWithError(`Failed to create branch: ${branch}`, `Make sure the branch doesn't already exist and you have a clean working directory`);
469
+ return ok(void 0);
470
+ } catch (error) {
471
+ return err(toGitError("createBranch", error));
620
472
  }
621
473
  }
622
474
  async function checkoutBranch(branch, workspaceRoot) {
@@ -628,11 +480,23 @@ async function checkoutBranch(branch, workspaceRoot) {
628
480
  } })).stderr.trim().match(/Switched to branch '(.+)'/);
629
481
  if (match && match[1] === branch) {
630
482
  logger.info(`Successfully switched to branch: ${farver.green(branch)}`);
631
- return true;
483
+ return ok(true);
632
484
  }
633
- return false;
634
- } catch {
635
- return false;
485
+ return ok(false);
486
+ } catch (error) {
487
+ const gitError = toGitError("checkoutBranch", error);
488
+ logger.error(`Git checkout failed: ${gitError.message}`);
489
+ if (gitError.stderr) logger.error(`Git stderr: ${gitError.stderr}`);
490
+ try {
491
+ const branchResult = await run("git", ["branch", "-a"], { nodeOptions: {
492
+ cwd: workspaceRoot,
493
+ stdio: "pipe"
494
+ } });
495
+ logger.verbose(`Available branches:\n${branchResult.stdout}`);
496
+ } catch {
497
+ logger.verbose("Could not list available branches");
498
+ }
499
+ return err(gitError);
636
500
  }
637
501
  }
638
502
  async function pullLatestChanges(branch, workspaceRoot) {
@@ -645,9 +509,9 @@ async function pullLatestChanges(branch, workspaceRoot) {
645
509
  cwd: workspaceRoot,
646
510
  stdio: "pipe"
647
511
  } });
648
- return true;
649
- } catch {
650
- return false;
512
+ return ok(true);
513
+ } catch (error) {
514
+ return err(toGitError("pullLatestChanges", error));
651
515
  }
652
516
  }
653
517
  async function rebaseBranch(ontoBranch, workspaceRoot) {
@@ -657,9 +521,9 @@ async function rebaseBranch(ontoBranch, workspaceRoot) {
657
521
  cwd: workspaceRoot,
658
522
  stdio: "pipe"
659
523
  } });
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`);
524
+ return ok(void 0);
525
+ } catch (error) {
526
+ return err(toGitError("rebaseBranch", error));
663
527
  }
664
528
  }
665
529
  async function isBranchAheadOfRemote(branch, workspaceRoot) {
@@ -672,9 +536,9 @@ async function isBranchAheadOfRemote(branch, workspaceRoot) {
672
536
  cwd: workspaceRoot,
673
537
  stdio: "pipe"
674
538
  } });
675
- return Number.parseInt(result.stdout.trim(), 10) > 0;
539
+ return ok(Number.parseInt(result.stdout.trim(), 10) > 0);
676
540
  } catch {
677
- return true;
541
+ return ok(true);
678
542
  }
679
543
  }
680
544
  async function commitChanges(message, workspaceRoot) {
@@ -683,7 +547,8 @@ async function commitChanges(message, workspaceRoot) {
683
547
  cwd: workspaceRoot,
684
548
  stdio: "pipe"
685
549
  } });
686
- if (await isWorkingDirectoryClean(workspaceRoot)) return false;
550
+ const isClean = await isWorkingDirectoryClean(workspaceRoot);
551
+ if (!isClean.ok || isClean.value) return ok(false);
687
552
  logger.info(`Committing changes: ${farver.dim(message)}`);
688
553
  await runIfNotDry("git", [
689
554
  "commit",
@@ -693,9 +558,9 @@ async function commitChanges(message, workspaceRoot) {
693
558
  cwd: workspaceRoot,
694
559
  stdio: "pipe"
695
560
  } });
696
- return true;
697
- } catch {
698
- exitWithError(`Failed to commit changes`, `Make sure you have git configured properly with user.name and user.email`);
561
+ return ok(true);
562
+ } catch (error) {
563
+ return err(toGitError("commitChanges", error));
699
564
  }
700
565
  }
701
566
  async function pushBranch(branch, workspaceRoot, options) {
@@ -706,6 +571,14 @@ async function pushBranch(branch, workspaceRoot, options) {
706
571
  branch
707
572
  ];
708
573
  if (options?.forceWithLease) {
574
+ await run("git", [
575
+ "fetch",
576
+ "origin",
577
+ branch
578
+ ], { nodeOptions: {
579
+ cwd: workspaceRoot,
580
+ stdio: "pipe"
581
+ } });
709
582
  args.push("--force-with-lease");
710
583
  logger.info(`Pushing branch: ${farver.green(branch)} ${farver.dim("(with lease)")}`);
711
584
  } else if (options?.force) {
@@ -716,19 +589,19 @@ async function pushBranch(branch, workspaceRoot, options) {
716
589
  cwd: workspaceRoot,
717
590
  stdio: "pipe"
718
591
  } });
719
- return true;
720
- } catch {
721
- exitWithError(`Failed to push branch: ${branch}`, `Make sure you have permission to push to the remote repository`);
592
+ return ok(true);
593
+ } catch (error) {
594
+ return err(toGitError("pushBranch", error));
722
595
  }
723
596
  }
724
597
  async function readFileFromGit(workspaceRoot, ref, filePath) {
725
598
  try {
726
- return (await run("git", ["show", `${ref}:${filePath}`], { nodeOptions: {
599
+ return ok((await run("git", ["show", `${ref}:${filePath}`], { nodeOptions: {
727
600
  cwd: workspaceRoot,
728
601
  stdio: "pipe"
729
- } })).stdout;
602
+ } })).stdout);
730
603
  } catch {
731
- return null;
604
+ return ok(null);
732
605
  }
733
606
  }
734
607
  async function getMostRecentPackageTag(workspaceRoot, packageName) {
@@ -742,11 +615,10 @@ async function getMostRecentPackageTag(workspaceRoot, packageName) {
742
615
  stdio: "pipe"
743
616
  } });
744
617
  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;
618
+ if (tags.length === 0) return ok(void 0);
619
+ return ok(tags.reverse()[0]);
620
+ } catch (error) {
621
+ return err(toGitError("getMostRecentPackageTag", error));
750
622
  }
751
623
  }
752
624
  /**
@@ -793,11 +665,65 @@ async function getGroupedFilesByCommitSha(workspaceRoot, from, to) {
793
665
  if (currentSha === null) continue;
794
666
  commitsMap.get(currentSha).push(trimmedLine);
795
667
  }
796
- return commitsMap;
797
- } catch {
798
- return null;
668
+ return ok(commitsMap);
669
+ } catch (error) {
670
+ return err(toGitError("getGroupedFilesByCommitSha", error));
671
+ }
672
+ }
673
+ /**
674
+ * Create a git tag for a package release
675
+ * @param packageName - The package name (e.g., "@scope/name")
676
+ * @param version - The version to tag (e.g., "1.2.3")
677
+ * @param workspaceRoot - The root directory of the workspace
678
+ * @returns Result indicating success or failure
679
+ */
680
+ async function createPackageTag(packageName, version, workspaceRoot) {
681
+ const tagName = `${packageName}@${version}`;
682
+ try {
683
+ logger.info(`Creating tag: ${farver.green(tagName)}`);
684
+ await runIfNotDry("git", ["tag", tagName], { nodeOptions: {
685
+ cwd: workspaceRoot,
686
+ stdio: "pipe"
687
+ } });
688
+ return ok(void 0);
689
+ } catch (error) {
690
+ return err(toGitError("createPackageTag", error));
691
+ }
692
+ }
693
+ /**
694
+ * Push a specific tag to the remote repository
695
+ * @param tagName - The tag name to push
696
+ * @param workspaceRoot - The root directory of the workspace
697
+ * @returns Result indicating success or failure
698
+ */
699
+ async function pushTag(tagName, workspaceRoot) {
700
+ try {
701
+ logger.info(`Pushing tag: ${farver.green(tagName)}`);
702
+ await runIfNotDry("git", [
703
+ "push",
704
+ "origin",
705
+ tagName
706
+ ], { nodeOptions: {
707
+ cwd: workspaceRoot,
708
+ stdio: "pipe"
709
+ } });
710
+ return ok(void 0);
711
+ } catch (error) {
712
+ return err(toGitError("pushTag", error));
799
713
  }
800
714
  }
715
+ /**
716
+ * Create and push a package tag in one operation
717
+ * @param packageName - The package name
718
+ * @param version - The version to tag
719
+ * @param workspaceRoot - The root directory of the workspace
720
+ * @returns Result indicating success or failure
721
+ */
722
+ async function createAndPushPackageTag(packageName, version, workspaceRoot) {
723
+ const createResult = await createPackageTag(packageName, version, workspaceRoot);
724
+ if (!createResult.ok) return createResult;
725
+ return pushTag(`${packageName}@${version}`, workspaceRoot);
726
+ }
801
727
 
802
728
  //#endregion
803
729
  //#region src/core/changelog.ts
@@ -833,7 +759,7 @@ async function updateChangelog(options) {
833
759
  const changelogPath = join(workspacePackage.path, "CHANGELOG.md");
834
760
  const changelogRelativePath = relative(normalizedOptions.workspaceRoot, join(workspacePackage.path, "CHANGELOG.md"));
835
761
  const existingContent = await readFileFromGit(normalizedOptions.workspaceRoot, normalizedOptions.branch.default, changelogRelativePath);
836
- logger.verbose("Existing content found: ", Boolean(existingContent));
762
+ logger.verbose("Existing content found: ", existingContent.ok && Boolean(existingContent.value));
837
763
  const newEntry = await generateChangelogEntry({
838
764
  packageName: workspacePackage.name,
839
765
  version,
@@ -847,13 +773,13 @@ async function updateChangelog(options) {
847
773
  githubClient
848
774
  });
849
775
  let updatedContent;
850
- if (!existingContent) {
776
+ if (!existingContent.ok || !existingContent.value) {
851
777
  updatedContent = `# ${workspacePackage.name}\n\n${newEntry}\n`;
852
778
  await writeFile(changelogPath, updatedContent, "utf-8");
853
779
  return;
854
780
  }
855
- const parsed = parseChangelog(existingContent);
856
- const lines = existingContent.split("\n");
781
+ const parsed = parseChangelog(existingContent.value);
782
+ const lines = existingContent.value.split("\n");
857
783
  const existingVersionIndex = parsed.versions.findIndex((v) => v.version === version);
858
784
  if (existingVersionIndex !== -1) {
859
785
  const existingVersion = parsed.versions[existingVersionIndex];
@@ -944,269 +870,252 @@ function parseChangelog(content) {
944
870
  }
945
871
 
946
872
  //#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;
873
+ //#region src/operations/semver.ts
874
+ function isValidSemver(version) {
875
+ return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
876
+ }
877
+ function getNextVersion(currentVersion, bump) {
878
+ if (bump === "none") return currentVersion;
879
+ if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
880
+ const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
881
+ if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
882
+ const [, major, minor, patch] = match;
883
+ let newMajor = Number.parseInt(major, 10);
884
+ let newMinor = Number.parseInt(minor, 10);
885
+ let newPatch = Number.parseInt(patch, 10);
886
+ switch (bump) {
887
+ case "major":
888
+ newMajor += 1;
889
+ newMinor = 0;
890
+ newPatch = 0;
891
+ break;
892
+ case "minor":
893
+ newMinor += 1;
894
+ newPatch = 0;
895
+ break;
896
+ case "patch":
897
+ newPatch += 1;
898
+ break;
957
899
  }
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)"
900
+ return `${newMajor}.${newMinor}.${newPatch}`;
901
+ }
902
+ function calculateBumpType(oldVersion, newVersion) {
903
+ if (!isValidSemver(oldVersion) || !isValidSemver(newVersion)) throw new Error(`Cannot calculate bump type for invalid semver: ${oldVersion} or ${newVersion}`);
904
+ const oldParts = oldVersion.split(".").map(Number);
905
+ const newParts = newVersion.split(".").map(Number);
906
+ if (newParts[0] > oldParts[0]) return "major";
907
+ if (newParts[1] > oldParts[1]) return "minor";
908
+ if (newParts[2] > oldParts[2]) return "patch";
909
+ return "none";
910
+ }
911
+
912
+ //#endregion
913
+ //#region src/core/prompts.ts
914
+ async function selectPackagePrompt(packages) {
915
+ const response = await prompts({
916
+ type: "multiselect",
917
+ name: "selectedPackages",
918
+ message: "Select packages to release",
919
+ choices: packages.map((pkg) => ({
920
+ title: `${pkg.name} (${farver.bold(pkg.version)})`,
921
+ value: pkg.name,
922
+ selected: true
923
+ })),
924
+ min: 1,
925
+ hint: "Space to select/deselect. Return to submit.",
926
+ instructions: false
927
+ });
928
+ if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
929
+ return response.selectedPackages;
930
+ }
931
+ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggestedVersion) {
932
+ const answers = await prompts([{
933
+ type: "autocomplete",
934
+ name: "version",
935
+ message: `${pkg.name}: ${farver.green(pkg.version)}`,
936
+ choices: [
937
+ {
938
+ value: "skip",
939
+ title: `skip ${farver.dim("(no change)")}`
940
+ },
941
+ {
942
+ value: "major",
943
+ title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}`
944
+ },
945
+ {
946
+ value: "minor",
947
+ title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}`
948
+ },
949
+ {
950
+ value: "patch",
951
+ title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
952
+ },
953
+ {
954
+ value: "suggested",
955
+ title: `suggested ${farver.bold(suggestedVersion)}`
956
+ },
957
+ {
958
+ value: "as-is",
959
+ title: `as-is ${farver.dim("(keep current version)")}`
960
+ },
961
+ {
962
+ value: "custom",
963
+ title: "custom"
967
964
  }
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))}`);
1037
- }
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}`);
965
+ ],
966
+ initial: suggestedVersion === currentVersion ? 0 : 4
967
+ }, {
968
+ type: (prev) => prev === "custom" ? "text" : null,
969
+ name: "custom",
970
+ message: "Enter the new version number:",
971
+ initial: suggestedVersion,
972
+ validate: (custom) => {
973
+ if (isValidSemver(custom)) return true;
974
+ return "That's not a valid version number";
1054
975
  }
1055
- return info;
1056
- }
1057
- };
1058
- function createGitHubClient(options) {
1059
- return new GitHubClient(options);
976
+ }]);
977
+ if (!answers.version) return null;
978
+ if (answers.version === "skip") return null;
979
+ else if (answers.version === "suggested") return suggestedVersion;
980
+ else if (answers.version === "custom") {
981
+ if (!answers.custom) return null;
982
+ return answers.custom;
983
+ } else if (answers.version === "as-is") return currentVersion;
984
+ else return getNextVersion(pkg.version, answers.version);
1060
985
  }
1061
- function toGitHubError(operation, error) {
986
+
987
+ //#endregion
988
+ //#region src/core/workspace.ts
989
+ function toWorkspaceError(operation, error) {
1062
990
  return {
1063
- type: "github",
991
+ type: "workspace",
1064
992
  operation,
1065
993
  message: error instanceof Error ? error.message : String(error)
1066
994
  };
1067
995
  }
1068
- async function wrapGitHub(operation, fn) {
996
+ async function discoverWorkspacePackages(workspaceRoot, options) {
997
+ let workspaceOptions;
998
+ let explicitPackages;
999
+ if (options.packages == null || options.packages === true) workspaceOptions = { excludePrivate: false };
1000
+ else if (Array.isArray(options.packages)) {
1001
+ workspaceOptions = {
1002
+ excludePrivate: false,
1003
+ include: options.packages
1004
+ };
1005
+ explicitPackages = options.packages;
1006
+ } else {
1007
+ workspaceOptions = options.packages;
1008
+ if (options.packages.include) explicitPackages = options.packages.include;
1009
+ }
1010
+ let workspacePackages;
1069
1011
  try {
1070
- return ok(await fn());
1012
+ workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
1071
1013
  } catch (error) {
1072
- return err(toGitHubError(operation, error));
1014
+ return err(toWorkspaceError("discoverWorkspacePackages", error));
1015
+ }
1016
+ if (explicitPackages) {
1017
+ const foundNames = new Set(workspacePackages.map((p) => p.name));
1018
+ const missing = explicitPackages.filter((p) => !foundNames.has(p));
1019
+ 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");
1020
+ }
1021
+ const isPackagePromptEnabled = options.prompts?.packages !== false;
1022
+ if (!isCI && isPackagePromptEnabled && !explicitPackages) {
1023
+ const selectedNames = await selectPackagePrompt(workspacePackages);
1024
+ workspacePackages = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
1073
1025
  }
1026
+ return ok(workspacePackages);
1074
1027
  }
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;
1028
+ async function findWorkspacePackages(workspaceRoot, options) {
1029
+ try {
1030
+ const result = await run("pnpm", [
1031
+ "-r",
1032
+ "ls",
1033
+ "--json"
1034
+ ], { nodeOptions: {
1035
+ cwd: workspaceRoot,
1036
+ stdio: "pipe"
1037
+ } });
1038
+ const rawProjects = JSON.parse(result.stdout);
1039
+ const allPackageNames = new Set(rawProjects.map((p) => p.name));
1040
+ const excludedPackages = /* @__PURE__ */ new Set();
1041
+ const promises = rawProjects.map(async (rawProject) => {
1042
+ const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
1043
+ const packageJson = JSON.parse(content);
1044
+ if (!shouldIncludePackage(packageJson, options)) {
1045
+ excludedPackages.add(rawProject.name);
1046
+ return null;
1090
1047
  }
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();
1048
+ return {
1049
+ name: rawProject.name,
1050
+ version: rawProject.version,
1051
+ path: rawProject.path,
1052
+ packageJson,
1053
+ workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => {
1054
+ return allPackageNames.has(dep);
1055
+ }),
1056
+ workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => {
1057
+ return allPackageNames.has(dep);
1058
+ })
1059
+ };
1060
+ });
1061
+ const packages = await Promise.all(promises);
1062
+ if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
1063
+ return packages.filter((pkg) => pkg !== null);
1064
+ } catch (err) {
1065
+ logger.error("Error discovering workspace packages:", err);
1066
+ throw err;
1067
+ }
1103
1068
  }
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
- })) });
1069
+ function shouldIncludePackage(pkg, options) {
1070
+ if (!options) return true;
1071
+ if (options.excludePrivate && pkg.private) return false;
1072
+ if (options.include && options.include.length > 0) {
1073
+ if (!options.include.includes(pkg.name)) return false;
1074
+ }
1075
+ if (options.exclude?.includes(pkg.name)) return false;
1076
+ return true;
1114
1077
  }
1115
1078
 
1116
1079
  //#endregion
1117
1080
  //#region src/operations/branch.ts
1118
1081
  async function prepareReleaseBranch(options) {
1119
- const { git, workspaceRoot, releaseBranch, defaultBranch } = options;
1120
- const currentBranch = await git.getCurrentBranch(workspaceRoot);
1082
+ const { workspaceRoot, releaseBranch, defaultBranch } = options;
1083
+ const currentBranch = await getCurrentBranch(workspaceRoot);
1121
1084
  if (!currentBranch.ok) return currentBranch;
1122
1085
  if (currentBranch.value !== defaultBranch) return err({
1123
1086
  type: "git",
1124
1087
  operation: "validateBranch",
1125
1088
  message: `Current branch is '${currentBranch.value}'. Please switch to '${defaultBranch}'.`
1126
1089
  });
1127
- const branchExists = await git.doesBranchExist(releaseBranch, workspaceRoot);
1090
+ const branchExists = await doesBranchExist(releaseBranch, workspaceRoot);
1128
1091
  if (!branchExists.ok) return branchExists;
1129
1092
  if (!branchExists.value) {
1130
- const created = await git.createBranch(releaseBranch, defaultBranch, workspaceRoot);
1093
+ const created = await createBranch(releaseBranch, defaultBranch, workspaceRoot);
1131
1094
  if (!created.ok) return created;
1132
1095
  }
1133
- const checkedOut = await git.checkoutBranch(releaseBranch, workspaceRoot);
1096
+ const checkedOut = await checkoutBranch(releaseBranch, workspaceRoot);
1134
1097
  if (!checkedOut.ok) return checkedOut;
1135
1098
  if (branchExists.value) {
1136
- const pulled = await git.pullLatestChanges(releaseBranch, workspaceRoot);
1099
+ const pulled = await pullLatestChanges(releaseBranch, workspaceRoot);
1137
1100
  if (!pulled.ok) return pulled;
1138
1101
  if (!pulled.value) logger.warn("Failed to pull latest changes, continuing anyway.");
1139
1102
  }
1140
- const rebased = await git.rebaseBranch(defaultBranch, workspaceRoot);
1103
+ const rebased = await rebaseBranch(defaultBranch, workspaceRoot);
1141
1104
  if (!rebased.ok) return rebased;
1142
1105
  return ok(void 0);
1143
1106
  }
1144
1107
  async function syncReleaseChanges(options) {
1145
- const { git, workspaceRoot, releaseBranch, commitMessage, hasChanges } = options;
1146
- const committed = hasChanges ? await git.commitChanges(commitMessage, workspaceRoot) : ok(false);
1108
+ const { workspaceRoot, releaseBranch, commitMessage, hasChanges } = options;
1109
+ const committed = hasChanges ? await commitChanges(commitMessage, workspaceRoot) : ok(false);
1147
1110
  if (!committed.ok) return committed;
1148
- const isAhead = await git.isBranchAheadOfRemote(releaseBranch, workspaceRoot);
1111
+ const isAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
1149
1112
  if (!isAhead.ok) return isAhead;
1150
1113
  if (!committed.value && !isAhead.value) return ok(false);
1151
- const pushed = await git.pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
1114
+ const pushed = await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
1152
1115
  if (!pushed.ok) return pushed;
1153
1116
  return ok(true);
1154
1117
  }
1155
1118
 
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
1119
  //#endregion
1211
1120
  //#region src/versioning/commits.ts
1212
1121
  /**
@@ -1220,7 +1129,8 @@ async function syncPullRequest(options) {
1220
1129
  async function getWorkspacePackageGroupedCommits(workspaceRoot, packages) {
1221
1130
  const changedPackages = /* @__PURE__ */ new Map();
1222
1131
  const promises = packages.map(async (pkg) => {
1223
- const lastTag = await getMostRecentPackageTag(workspaceRoot, pkg.name);
1132
+ const lastTagResult = await getMostRecentPackageTag(workspaceRoot, pkg.name);
1133
+ const lastTag = lastTagResult.ok ? lastTagResult.value : void 0;
1224
1134
  const allCommits = await getCommits({
1225
1135
  from: lastTag,
1226
1136
  to: "HEAD",
@@ -1320,17 +1230,17 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
1320
1230
  }
1321
1231
  logger.verbose("Fetching files for commits range", `${farver.cyan(commitRange.oldest)}..${farver.cyan(commitRange.newest)}`);
1322
1232
  const commitFilesMap = await getGroupedFilesByCommitSha(workspaceRoot, commitRange.oldest, commitRange.newest);
1323
- if (!commitFilesMap) {
1233
+ if (!commitFilesMap.ok) {
1324
1234
  logger.warn("Failed to get commit file list, returning empty global commits");
1325
1235
  return result;
1326
1236
  }
1327
- logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.size)} commits in ONE git call`);
1237
+ logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.value.size)} commits in ONE git call`);
1328
1238
  const packagePaths = new Set(allPackages.map((p) => p.path));
1329
1239
  for (const [pkgName, commits] of packageCommits) {
1330
1240
  const globalCommitsAffectingPackage = [];
1331
1241
  logger.verbose("Filtering global commits for package", `${farver.bold(pkgName)} from ${farver.cyan(commits.length)} commits`);
1332
1242
  for (const commit of commits) {
1333
- const files = commitFilesMap.get(commit.shortHash);
1243
+ const files = commitFilesMap.value.get(commit.shortHash);
1334
1244
  if (!files) continue;
1335
1245
  if (isGlobalCommit(workspaceRoot, files, packagePaths)) globalCommitsAffectingPackage.push(commit);
1336
1246
  }
@@ -1341,7 +1251,7 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
1341
1251
  }
1342
1252
  const dependencyCommits = [];
1343
1253
  for (const commit of globalCommitsAffectingPackage) {
1344
- const files = commitFilesMap.get(commit.shortHash);
1254
+ const files = commitFilesMap.value.get(commit.shortHash);
1345
1255
  if (!files) continue;
1346
1256
  if (files.some((file) => DEPENDENCY_FILES.includes(file.startsWith("./") ? file.slice(2) : file))) {
1347
1257
  logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`);
@@ -1439,6 +1349,51 @@ function getAllAffectedPackages(graph, changedPackages) {
1439
1349
  return affected;
1440
1350
  }
1441
1351
  /**
1352
+ * Calculate the order in which packages should be published
1353
+ *
1354
+ * Performs topological sorting to ensure dependencies are published before dependents.
1355
+ * Assigns a "level" to each package based on its depth in the dependency tree.
1356
+ *
1357
+ * This is used by the publish command to publish packages in the correct order.
1358
+ *
1359
+ * @param graph - Dependency graph
1360
+ * @param packagesToPublish - Set of package names to publish
1361
+ * @returns Array of packages in publish order with their dependency level
1362
+ */
1363
+ function getPackagePublishOrder(graph, packagesToPublish) {
1364
+ const result = [];
1365
+ const visited = /* @__PURE__ */ new Set();
1366
+ const toUpdate = new Set(packagesToPublish);
1367
+ const packagesToProcess = new Set(packagesToPublish);
1368
+ for (const pkg of packagesToPublish) {
1369
+ const deps = graph.dependents.get(pkg);
1370
+ if (deps) for (const dep of deps) {
1371
+ packagesToProcess.add(dep);
1372
+ toUpdate.add(dep);
1373
+ }
1374
+ }
1375
+ function visit(pkgName, level) {
1376
+ if (visited.has(pkgName)) return;
1377
+ visited.add(pkgName);
1378
+ const pkg = graph.packages.get(pkgName);
1379
+ if (!pkg) return;
1380
+ const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
1381
+ let maxDepLevel = level;
1382
+ for (const dep of allDeps) if (toUpdate.has(dep)) {
1383
+ visit(dep, level);
1384
+ const depResult = result.find((r) => r.package.name === dep);
1385
+ if (depResult && depResult.level >= maxDepLevel) maxDepLevel = depResult.level + 1;
1386
+ }
1387
+ result.push({
1388
+ package: pkg,
1389
+ level: maxDepLevel
1390
+ });
1391
+ }
1392
+ for (const pkg of toUpdate) visit(pkg, 0);
1393
+ result.sort((a, b) => a.level - b.level);
1394
+ return result;
1395
+ }
1396
+ /**
1442
1397
  * Create version updates for all packages affected by dependency changes
1443
1398
  *
1444
1399
  * When a package is updated, all packages that depend on it should also be updated.
@@ -1663,79 +1618,89 @@ function getDependencyUpdates(pkg, allUpdates) {
1663
1618
  }
1664
1619
 
1665
1620
  //#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
- };
1621
+ //#region src/operations/calculate.ts
1622
+ async function calculateUpdates(options) {
1623
+ const { workspacePackages, workspaceRoot, showPrompt, overrides, globalCommitMode } = options;
1624
+ try {
1625
+ const grouped = await getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages);
1626
+ return ok(await calculateAndPrepareVersionUpdates({
1627
+ workspacePackages,
1628
+ packageCommits: grouped,
1629
+ workspaceRoot,
1630
+ showPrompt,
1631
+ globalCommitsPerPackage: await getGlobalCommitsPerPackage(workspaceRoot, grouped, workspacePackages, globalCommitMode),
1632
+ overrides
1633
+ }));
1634
+ } catch (error) {
1635
+ return err({
1636
+ type: "git",
1637
+ operation: "calculateUpdates",
1638
+ message: error instanceof Error ? error.message : String(error)
1639
+ });
1640
+ }
1641
+ }
1642
+ function ensureHasPackages(packages) {
1643
+ if (packages.length === 0) return err({
1644
+ type: "git",
1645
+ operation: "discoverWorkspacePackages",
1646
+ message: "No packages found to release"
1647
+ });
1648
+ return ok(packages);
1649
+ }
1650
+
1651
+ //#endregion
1652
+ //#region src/operations/pr.ts
1653
+ async function syncPullRequest(options) {
1654
+ const { github, releaseBranch, defaultBranch, pullRequestTitle, pullRequestBody, updates } = options;
1655
+ let existing = null;
1656
+ try {
1657
+ existing = await github.getExistingPullRequest(releaseBranch);
1658
+ } catch (error) {
1659
+ return {
1660
+ ok: false,
1661
+ error: {
1662
+ type: "github",
1663
+ operation: "getExistingPullRequest",
1664
+ message: error instanceof Error ? error.message : String(error)
1701
1665
  }
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
- };
1666
+ };
1667
+ }
1668
+ const doesExist = !!existing;
1669
+ const title = existing?.title || pullRequestTitle || "chore: update package versions";
1670
+ const body = generatePullRequestBody(updates, pullRequestBody);
1671
+ let pr = null;
1672
+ try {
1673
+ pr = await github.upsertPullRequest({
1674
+ pullNumber: existing?.number,
1675
+ title,
1676
+ body,
1677
+ head: releaseBranch,
1678
+ base: defaultBranch
1679
+ });
1680
+ } catch (error) {
1681
+ return {
1682
+ ok: false,
1683
+ error: {
1684
+ type: "github",
1685
+ operation: "upsertPullRequest",
1686
+ message: error instanceof Error ? error.message : String(error)
1718
1687
  }
1719
- }
1720
- };
1688
+ };
1689
+ }
1690
+ return ok({
1691
+ pullRequest: pr,
1692
+ created: !doesExist
1693
+ });
1721
1694
  }
1722
1695
 
1723
1696
  //#endregion
1724
1697
  //#region src/workflows/prepare.ts
1725
1698
  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
1699
  if (options.safeguards) {
1735
- const clean = await gitOps.isWorkingDirectoryClean(options.workspaceRoot);
1700
+ const clean = await isWorkingDirectoryClean(options.workspaceRoot);
1736
1701
  if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
1737
1702
  }
1738
- const discovered = await workspaceOps.discoverWorkspacePackages(options.workspaceRoot, options);
1703
+ const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
1739
1704
  if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
1740
1705
  const ensured = ensureHasPackages(discovered.value);
1741
1706
  if (!ensured.ok) {
@@ -1751,7 +1716,6 @@ async function prepareWorkflow(options) {
1751
1716
  }
1752
1717
  logger.emptyLine();
1753
1718
  const prepareBranchResult = await prepareReleaseBranch({
1754
- git: gitOps,
1755
1719
  workspaceRoot: options.workspaceRoot,
1756
1720
  releaseBranch: options.branch.release,
1757
1721
  defaultBranch: options.branch.default
@@ -1767,7 +1731,6 @@ async function prepareWorkflow(options) {
1767
1731
  logger.info("No existing version overrides file found. Continuing...");
1768
1732
  }
1769
1733
  const updatesResult = await calculateUpdates({
1770
- versioning: versioningOps,
1771
1734
  workspacePackages,
1772
1735
  workspaceRoot: options.workspaceRoot,
1773
1736
  showPrompt: options.prompts?.versions !== false,
@@ -1814,13 +1777,11 @@ async function prepareWorkflow(options) {
1814
1777
  await applyUpdates();
1815
1778
  if (options.changelog?.enabled) {
1816
1779
  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);
1780
+ const groupedPackageCommits = await getWorkspacePackageGroupedCommits(options.workspaceRoot, workspacePackages);
1781
+ const globalCommitsPerPackage = await getGlobalCommitsPerPackage(options.workspaceRoot, groupedPackageCommits, workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode);
1821
1782
  const changelogPromises = allUpdates.map((update) => {
1822
- const pkgCommits = groupedPackageCommits.value.get(update.package.name) || [];
1823
- const globalCommits = globalCommitsPerPackage.value.get(update.package.name) || [];
1783
+ const pkgCommits = groupedPackageCommits.get(update.package.name) || [];
1784
+ const globalCommits = globalCommitsPerPackage.get(update.package.name) || [];
1824
1785
  const allCommits = [...pkgCommits, ...globalCommits];
1825
1786
  if (allCommits.length === 0) {
1826
1787
  logger.verbose(`No commits for ${update.package.name}, skipping changelog`);
@@ -1832,11 +1793,7 @@ async function prepareWorkflow(options) {
1832
1793
  ...options,
1833
1794
  workspaceRoot: options.workspaceRoot
1834
1795
  },
1835
- githubClient: createGitHubClient({
1836
- owner: options.owner,
1837
- repo: options.repo,
1838
- githubToken: options.githubToken
1839
- }),
1796
+ githubClient: options.githubClient,
1840
1797
  workspacePackage: update.package,
1841
1798
  version: update.newVersion,
1842
1799
  previousVersion: update.currentVersion !== "0.0.0" ? update.currentVersion : void 0,
@@ -1848,7 +1805,6 @@ async function prepareWorkflow(options) {
1848
1805
  logger.success(`Updated ${updates.length} changelog(s)`);
1849
1806
  }
1850
1807
  const hasChangesToPush = await syncReleaseChanges({
1851
- git: gitOps,
1852
1808
  workspaceRoot: options.workspaceRoot,
1853
1809
  releaseBranch: options.branch.release,
1854
1810
  commitMessage: "chore: update release versions",
@@ -1857,7 +1813,7 @@ async function prepareWorkflow(options) {
1857
1813
  if (!hasChangesToPush.ok) exitWithError(hasChangesToPush.error.message);
1858
1814
  if (!hasChangesToPush.value) {
1859
1815
  const prResult = await syncPullRequest({
1860
- github: githubOps,
1816
+ github: options.githubClient,
1861
1817
  releaseBranch: options.branch.release,
1862
1818
  defaultBranch: options.branch.default,
1863
1819
  pullRequestTitle: options.pullRequest?.title,
@@ -1877,7 +1833,7 @@ async function prepareWorkflow(options) {
1877
1833
  return null;
1878
1834
  }
1879
1835
  const prResult = await syncPullRequest({
1880
- github: githubOps,
1836
+ github: options.githubClient,
1881
1837
  releaseBranch: options.branch.release,
1882
1838
  defaultBranch: options.branch.default,
1883
1839
  pullRequestTitle: options.pullRequest?.title,
@@ -1889,6 +1845,8 @@ async function prepareWorkflow(options) {
1889
1845
  logger.section("🚀 Pull Request");
1890
1846
  logger.success(`Pull request ${prResult.value.created ? "created" : "updated"}: ${prResult.value.pullRequest.html_url}`);
1891
1847
  }
1848
+ const returnToDefault = await checkoutBranch(options.branch.default, options.workspaceRoot);
1849
+ if (!returnToDefault.ok || !returnToDefault.value) exitWithError(`Failed to checkout branch: ${options.branch.default}`);
1892
1850
  return {
1893
1851
  updates: allUpdates,
1894
1852
  prUrl: prResult.value.pullRequest?.html_url,
@@ -1897,62 +1855,218 @@ async function prepareWorkflow(options) {
1897
1855
  }
1898
1856
 
1899
1857
  //#endregion
1900
- //#region src/prepare.ts
1901
- async function release(options) {
1902
- return prepareWorkflow(options);
1858
+ //#region src/core/npm.ts
1859
+ function toNPMError(operation, error, code) {
1860
+ return {
1861
+ type: "npm",
1862
+ operation,
1863
+ message: error instanceof Error ? error.message : String(error),
1864
+ code
1865
+ };
1903
1866
  }
1904
-
1905
- //#endregion
1906
- //#region src/workflows/publish.ts
1907
- async function publishWorkflow(options) {
1908
- logger.warn("Publish workflow is not implemented yet.");
1909
- logger.verbose("Publish options:", options);
1867
+ /**
1868
+ * Get the NPM registry URL
1869
+ * Respects NPM_CONFIG_REGISTRY environment variable, defaults to npmjs.org
1870
+ */
1871
+ function getRegistryURL() {
1872
+ return process.env.NPM_CONFIG_REGISTRY || "https://registry.npmjs.org";
1910
1873
  }
1911
-
1912
- //#endregion
1913
- //#region src/publish.ts
1914
- async function publish(options) {
1915
- return publishWorkflow(options);
1874
+ /**
1875
+ * Fetch package metadata from NPM registry
1876
+ * @param packageName - The package name (e.g., "lodash" or "@scope/name")
1877
+ * @returns Result with package metadata or error
1878
+ */
1879
+ async function getPackageMetadata(packageName) {
1880
+ try {
1881
+ const registry = getRegistryURL();
1882
+ const encodedName = packageName.startsWith("@") ? `@${encodeURIComponent(packageName.slice(1))}` : encodeURIComponent(packageName);
1883
+ const response = await fetch(`${registry}/${encodedName}`, { headers: { Accept: "application/json" } });
1884
+ if (!response.ok) {
1885
+ if (response.status === 404) return err(toNPMError("getPackageMetadata", `Package not found: ${packageName}`, "E404"));
1886
+ return err(toNPMError("getPackageMetadata", `HTTP ${response.status}: ${response.statusText}`));
1887
+ }
1888
+ return ok(await response.json());
1889
+ } catch (error) {
1890
+ return err(toNPMError("getPackageMetadata", error, "ENETWORK"));
1891
+ }
1892
+ }
1893
+ /**
1894
+ * Check if a specific package version exists on NPM
1895
+ * @param packageName - The package name
1896
+ * @param version - The version to check (e.g., "1.2.3")
1897
+ * @returns Result with boolean (true if version exists) or error
1898
+ */
1899
+ async function checkVersionExists(packageName, version) {
1900
+ const metadataResult = await getPackageMetadata(packageName);
1901
+ if (!metadataResult.ok) {
1902
+ if (metadataResult.error.code === "E404") return ok(false);
1903
+ return err(metadataResult.error);
1904
+ }
1905
+ return ok(version in metadataResult.value.versions);
1906
+ }
1907
+ /**
1908
+ * Build a package before publishing
1909
+ * @param packageName - The package name to build
1910
+ * @param workspaceRoot - Path to the workspace root
1911
+ * @param options - Normalized release scripts options
1912
+ * @returns Result indicating success or failure
1913
+ */
1914
+ async function buildPackage(packageName, workspaceRoot, options) {
1915
+ if (!options.npm.runBuild) return ok(void 0);
1916
+ try {
1917
+ await runIfNotDry("pnpm", [
1918
+ "--filter",
1919
+ packageName,
1920
+ "build"
1921
+ ], { nodeOptions: {
1922
+ cwd: workspaceRoot,
1923
+ stdio: "inherit"
1924
+ } });
1925
+ return ok(void 0);
1926
+ } catch (error) {
1927
+ return err(toNPMError("buildPackage", error));
1928
+ }
1929
+ }
1930
+ /**
1931
+ * Publish a package to NPM
1932
+ * Uses pnpm to handle workspace protocol and catalog: resolution automatically
1933
+ * @param packageName - The package name to publish
1934
+ * @param workspaceRoot - Path to the workspace root
1935
+ * @param options - Normalized release scripts options
1936
+ * @returns Result indicating success or failure
1937
+ */
1938
+ async function publishPackage(packageName, workspaceRoot, options) {
1939
+ const args = [
1940
+ "--filter",
1941
+ packageName,
1942
+ "publish",
1943
+ "--access",
1944
+ options.npm.access,
1945
+ "--no-git-checks"
1946
+ ];
1947
+ if (options.npm.otp) args.push("--otp", options.npm.otp);
1948
+ if (process.env.NPM_CONFIG_TAG) args.push("--tag", process.env.NPM_CONFIG_TAG);
1949
+ const env = { ...process.env };
1950
+ if (options.npm.provenance) env.NPM_CONFIG_PROVENANCE = "true";
1951
+ try {
1952
+ await runIfNotDry("pnpm", args, { nodeOptions: {
1953
+ cwd: workspaceRoot,
1954
+ stdio: "inherit",
1955
+ env
1956
+ } });
1957
+ return ok(void 0);
1958
+ } catch (error) {
1959
+ const errorMessage = error instanceof Error ? error.message : String(error);
1960
+ return err(toNPMError("publishPackage", error, errorMessage.includes("E403") ? "E403" : errorMessage.includes("EPUBLISHCONFLICT") ? "EPUBLISHCONFLICT" : errorMessage.includes("EOTP") ? "EOTP" : void 0));
1961
+ }
1916
1962
  }
1917
1963
 
1918
1964
  //#endregion
1919
- //#region src/operations/discover.ts
1920
- async function discoverPackages({ workspace, workspaceRoot, options }) {
1921
- return workspace.discoverWorkspacePackages(workspaceRoot, options);
1965
+ //#region src/workflows/publish.ts
1966
+ async function publishWorkflow(options) {
1967
+ logger.section("📦 Publishing Packages");
1968
+ const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
1969
+ if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
1970
+ const workspacePackages = discovered.value;
1971
+ logger.item(`Found ${workspacePackages.length} packages in workspace`);
1972
+ const graph = buildPackageDependencyGraph(workspacePackages);
1973
+ const publicPackages = workspacePackages.filter((pkg) => !pkg.packageJson.private);
1974
+ logger.item(`Publishing ${publicPackages.length} public packages (private packages excluded)`);
1975
+ if (publicPackages.length === 0) {
1976
+ logger.warn("No public packages to publish");
1977
+ return;
1978
+ }
1979
+ const publishOrder = getPackagePublishOrder(graph, new Set(publicPackages.map((p) => p.name)));
1980
+ const status = {
1981
+ published: [],
1982
+ skipped: [],
1983
+ failed: []
1984
+ };
1985
+ for (const order of publishOrder) {
1986
+ const pkg = order.package;
1987
+ const version = pkg.version;
1988
+ const packageName = pkg.name;
1989
+ logger.section(`📦 ${farver.cyan(packageName)} ${farver.gray(`(level ${order.level})`)}`);
1990
+ logger.step(`Checking if ${farver.cyan(`${packageName}@${version}`)} exists on NPM...`);
1991
+ const existsResult = await checkVersionExists(packageName, version);
1992
+ if (!existsResult.ok) {
1993
+ logger.error(`Failed to check version: ${existsResult.error.message}`);
1994
+ status.failed.push(packageName);
1995
+ exitWithError(`Publishing failed for ${packageName}: ${existsResult.error.message}`, "Check your network connection and NPM registry access");
1996
+ }
1997
+ if (existsResult.value) {
1998
+ logger.info(`Version ${farver.cyan(version)} already exists on NPM, skipping`);
1999
+ status.skipped.push(packageName);
2000
+ continue;
2001
+ }
2002
+ if (options.npm.runBuild) {
2003
+ logger.step(`Building ${farver.cyan(packageName)}...`);
2004
+ const buildResult = await buildPackage(packageName, options.workspaceRoot, options);
2005
+ if (!buildResult.ok) {
2006
+ logger.error(`Failed to build package: ${buildResult.error.message}`);
2007
+ status.failed.push(packageName);
2008
+ exitWithError(`Publishing failed for ${packageName}: build failed`, "Check your build scripts and dependencies");
2009
+ }
2010
+ }
2011
+ logger.step(`Publishing ${farver.cyan(`${packageName}@${version}`)} to NPM...`);
2012
+ const publishResult = await publishPackage(packageName, options.workspaceRoot, options);
2013
+ if (!publishResult.ok) {
2014
+ logger.error(`Failed to publish: ${publishResult.error.message}`);
2015
+ status.failed.push(packageName);
2016
+ let hint;
2017
+ if (publishResult.error.code === "E403") hint = "Authentication failed. Ensure your NPM token or OIDC configuration is correct";
2018
+ else if (publishResult.error.code === "EPUBLISHCONFLICT") hint = "Version conflict. The version may have been published recently";
2019
+ else if (publishResult.error.code === "EOTP") hint = "2FA/OTP required. Provide the otp option or use OIDC authentication";
2020
+ exitWithError(`Publishing failed for ${packageName}`, hint);
2021
+ }
2022
+ logger.success(`Published ${farver.cyan(`${packageName}@${version}`)}`);
2023
+ status.published.push(packageName);
2024
+ logger.step(`Creating git tag ${farver.cyan(`${packageName}@${version}`)}...`);
2025
+ const tagResult = await createAndPushPackageTag(packageName, version, options.workspaceRoot);
2026
+ if (!tagResult.ok) {
2027
+ logger.error(`Failed to create/push tag: ${tagResult.error.message}`);
2028
+ logger.warn(`Package was published but tag was not created. You may need to create it manually.`);
2029
+ } else logger.success(`Created and pushed tag ${farver.cyan(`${packageName}@${version}`)}`);
2030
+ }
2031
+ logger.section("📊 Publishing Summary");
2032
+ logger.item(`${farver.green("✓")} Published: ${status.published.length} package(s)`);
2033
+ if (status.published.length > 0) for (const pkg of status.published) logger.item(` ${farver.green("•")} ${pkg}`);
2034
+ if (status.skipped.length > 0) {
2035
+ logger.item(`${farver.yellow("⚠")} Skipped (already exists): ${status.skipped.length} package(s)`);
2036
+ for (const pkg of status.skipped) logger.item(` ${farver.yellow("•")} ${pkg}`);
2037
+ }
2038
+ if (status.failed.length > 0) {
2039
+ logger.item(`${farver.red("✖")} Failed: ${status.failed.length} package(s)`);
2040
+ for (const pkg of status.failed) logger.item(` ${farver.red("•")} ${pkg}`);
2041
+ }
2042
+ if (status.failed.length > 0) exitWithError(`Publishing completed with ${status.failed.length} failure(s)`);
2043
+ logger.success("All packages published successfully!");
1922
2044
  }
1923
2045
 
1924
2046
  //#endregion
1925
2047
  //#region src/workflows/verify.ts
1926
2048
  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
2049
  if (options.safeguards) {
1935
- const clean = await gitOps.isWorkingDirectoryClean(options.workspaceRoot);
2050
+ const clean = await isWorkingDirectoryClean(options.workspaceRoot);
1936
2051
  if (!clean.ok || !clean.value) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
1937
2052
  }
1938
2053
  const releaseBranch = options.branch.release;
1939
2054
  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) {
2055
+ const releasePr = await options.githubClient.getExistingPullRequest(releaseBranch);
2056
+ if (!releasePr || !releasePr.head) {
1943
2057
  logger.warn(`No open release pull request found for branch "${releaseBranch}". Nothing to verify.`);
1944
2058
  return;
1945
2059
  }
1946
- logger.info(`Found release PR #${releasePr.value.number}. Verifying against default branch "${defaultBranch}"...`);
1947
- const originalBranch = await gitOps.getCurrentBranch(options.workspaceRoot);
2060
+ logger.info(`Found release PR #${releasePr.number}. Verifying against default branch "${defaultBranch}"...`);
2061
+ const originalBranch = await getCurrentBranch(options.workspaceRoot);
1948
2062
  if (!originalBranch.ok) exitWithError(originalBranch.error.message);
1949
2063
  if (originalBranch.value !== defaultBranch) {
1950
- const checkout = await gitOps.checkoutBranch(defaultBranch, options.workspaceRoot);
2064
+ const checkout = await checkoutBranch(defaultBranch, options.workspaceRoot);
1951
2065
  if (!checkout.ok || !checkout.value) exitWithError(`Failed to checkout branch: ${defaultBranch}`);
1952
2066
  }
1953
2067
  let existingOverrides = {};
1954
2068
  try {
1955
- const overridesContent = await gitOps.readFileFromGit(options.workspaceRoot, releasePr.value.head.sha, ucdjsReleaseOverridesPath);
2069
+ const overridesContent = await readFileFromGit(options.workspaceRoot, releasePr.head.sha, ucdjsReleaseOverridesPath);
1956
2070
  if (overridesContent.ok && overridesContent.value) {
1957
2071
  existingOverrides = JSON.parse(overridesContent.value);
1958
2072
  logger.info("Found existing version overrides file on release branch.");
@@ -1960,11 +2074,7 @@ async function verifyWorkflow(options) {
1960
2074
  } catch {
1961
2075
  logger.info("No version overrides file found on release branch. Continuing...");
1962
2076
  }
1963
- const discovered = await discoverPackages({
1964
- workspace: workspaceOps,
1965
- workspaceRoot: options.workspaceRoot,
1966
- options
1967
- });
2077
+ const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
1968
2078
  if (!discovered.ok) exitWithError(`Failed to discover packages: ${discovered.error.message}`);
1969
2079
  const ensured = ensureHasPackages(discovered.value);
1970
2080
  if (!ensured.ok) {
@@ -1973,7 +2083,6 @@ async function verifyWorkflow(options) {
1973
2083
  }
1974
2084
  const mainPackages = ensured.value;
1975
2085
  const updatesResult = await calculateUpdates({
1976
- versioning: createVersioningOperations(),
1977
2086
  workspacePackages: mainPackages,
1978
2087
  workspaceRoot: options.workspaceRoot,
1979
2088
  showPrompt: false,
@@ -1986,13 +2095,13 @@ async function verifyWorkflow(options) {
1986
2095
  const prVersionMap = /* @__PURE__ */ new Map();
1987
2096
  for (const pkg of mainPackages) {
1988
2097
  const pkgJsonPath = relative(options.workspaceRoot, join(pkg.path, "package.json"));
1989
- const pkgJsonContent = await gitOps.readFileFromGit(options.workspaceRoot, releasePr.value.head.sha, pkgJsonPath);
2098
+ const pkgJsonContent = await readFileFromGit(options.workspaceRoot, releasePr.head.sha, pkgJsonPath);
1990
2099
  if (pkgJsonContent.ok && pkgJsonContent.value) {
1991
2100
  const pkgJson = JSON.parse(pkgJsonContent.value);
1992
2101
  prVersionMap.set(pkg.name, pkgJson.version);
1993
2102
  }
1994
2103
  }
1995
- if (originalBranch.value !== defaultBranch) await gitOps.checkoutBranch(originalBranch.value, options.workspaceRoot);
2104
+ if (originalBranch.value !== defaultBranch) await checkoutBranch(originalBranch.value, options.workspaceRoot);
1996
2105
  let isOutOfSync = false;
1997
2106
  for (const [pkgName, expectedVersion] of expectedVersionMap.entries()) {
1998
2107
  const prVersion = prVersionMap.get(pkgName);
@@ -2007,51 +2116,49 @@ async function verifyWorkflow(options) {
2007
2116
  }
2008
2117
  const statusContext = "ucdjs/release-verify";
2009
2118
  if (isOutOfSync) {
2010
- await githubOps.setCommitStatus({
2011
- sha: releasePr.value.head.sha,
2119
+ await options.githubClient.setCommitStatus({
2120
+ sha: releasePr.head.sha,
2012
2121
  state: "failure",
2013
2122
  context: statusContext,
2014
2123
  description: "Release PR is out of sync with the default branch. Please re-run the release process."
2015
2124
  });
2016
2125
  logger.error("Verification failed. Commit status set to 'failure'.");
2017
2126
  } else {
2018
- await githubOps.setCommitStatus({
2019
- sha: releasePr.value.head.sha,
2127
+ await options.githubClient.setCommitStatus({
2128
+ sha: releasePr.head.sha,
2020
2129
  state: "success",
2021
2130
  context: statusContext,
2022
2131
  description: "Release PR is up to date.",
2023
- targetUrl: `https://github.com/${options.owner}/${options.repo}/pull/${releasePr.value.number}`
2132
+ targetUrl: `https://github.com/${options.owner}/${options.repo}/pull/${releasePr.number}`
2024
2133
  });
2025
2134
  logger.success("Verification successful. Commit status set to 'success'.");
2026
2135
  }
2027
2136
  }
2028
2137
 
2029
- //#endregion
2030
- //#region src/verify.ts
2031
- async function verify(options) {
2032
- return verifyWorkflow(options);
2033
- }
2034
-
2035
2138
  //#endregion
2036
2139
  //#region src/index.ts
2037
2140
  async function createReleaseScripts(options) {
2038
2141
  const normalizedOptions = normalizeReleaseScriptsOptions(options);
2039
2142
  return {
2040
2143
  async verify() {
2041
- return verify(normalizedOptions);
2144
+ return verifyWorkflow(normalizedOptions);
2042
2145
  },
2043
2146
  async prepare() {
2044
- return release(normalizedOptions);
2147
+ return prepareWorkflow(normalizedOptions);
2045
2148
  },
2046
2149
  async publish() {
2047
- return publish(normalizedOptions);
2150
+ return publishWorkflow(normalizedOptions);
2048
2151
  },
2049
2152
  packages: {
2050
2153
  async list() {
2051
- return discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
2154
+ const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
2155
+ if (!result.ok) throw new Error(result.error.message);
2156
+ return result.value;
2052
2157
  },
2053
2158
  async get(packageName) {
2054
- return (await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions)).find((p) => p.name === packageName);
2159
+ const result = await discoverWorkspacePackages(normalizedOptions.workspaceRoot, normalizedOptions);
2160
+ if (!result.ok) throw new Error(result.error.message);
2161
+ return result.value.find((p) => p.name === packageName);
2055
2162
  }
2056
2163
  }
2057
2164
  };