@ucdjs/release-scripts 0.1.0-beta.5 → 0.1.0-beta.6

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.d.mts CHANGED
@@ -10,6 +10,45 @@ interface WorkspacePackage {
10
10
  //#endregion
11
11
  //#region src/types.d.ts
12
12
  type BumpKind = "none" | "patch" | "minor" | "major";
13
+ interface SharedOptions {
14
+ /**
15
+ * Repository identifier (e.g., "owner/repo")
16
+ */
17
+ repo: string;
18
+ /**
19
+ * Root directory of the workspace (defaults to process.cwd())
20
+ */
21
+ workspaceRoot?: string;
22
+ /**
23
+ * Specific packages to prepare for release.
24
+ * - true: discover all packages
25
+ * - FindWorkspacePackagesOptions: discover with filters
26
+ * - string[]: specific package names
27
+ */
28
+ packages?: true | FindWorkspacePackagesOptions | string[];
29
+ /**
30
+ * Whether to enable verbose logging
31
+ * @default false
32
+ */
33
+ verbose?: boolean;
34
+ /**
35
+ * GitHub token for authentication
36
+ */
37
+ githubToken: string;
38
+ /**
39
+ * Interactive prompt configuration
40
+ */
41
+ prompts?: {
42
+ /**
43
+ * Enable package selection prompt (defaults to true when not in CI)
44
+ */
45
+ packages?: boolean;
46
+ /**
47
+ * Enable version override prompt (defaults to true when not in CI)
48
+ */
49
+ versions?: boolean;
50
+ };
51
+ }
13
52
  interface PackageJson {
14
53
  name: string;
15
54
  version: string;
@@ -23,11 +62,11 @@ interface FindWorkspacePackagesOptions {
23
62
  /**
24
63
  * Package names to exclude
25
64
  */
26
- excluded?: string[];
65
+ exclude?: string[];
27
66
  /**
28
67
  * Only include these packages (if specified, all others are excluded)
29
68
  */
30
- included?: string[];
69
+ include?: string[];
31
70
  /**
32
71
  * Whether to exclude private packages (default: false)
33
72
  */
@@ -55,39 +94,17 @@ interface VersionUpdate {
55
94
  */
56
95
  hasDirectChanges: boolean;
57
96
  }
58
- interface ReleaseOptions {
59
- /**
60
- * Repository identifier (e.g., "owner/repo")
61
- */
62
- repo: string;
63
- /**
64
- * Root directory of the workspace (defaults to process.cwd())
65
- */
66
- workspaceRoot?: string;
67
- /**
68
- * Specific packages to prepare for release.
69
- * - true: discover all packages
70
- * - FindWorkspacePackagesOptions: discover with filters
71
- * - string[]: specific package names
72
- */
73
- packages?: true | FindWorkspacePackagesOptions | string[];
97
+ //#endregion
98
+ //#region src/publish.d.ts
99
+ interface PublishOptions extends SharedOptions {}
100
+ declare function publish(_options: PublishOptions): void;
101
+ //#endregion
102
+ //#region src/release.d.ts
103
+ interface ReleaseOptions extends SharedOptions {
74
104
  /**
75
105
  * Branch name for the release PR (defaults to "release/next")
76
106
  */
77
107
  releaseBranch?: string;
78
- /**
79
- * Interactive prompt configuration
80
- */
81
- prompts?: {
82
- /**
83
- * Enable package selection prompt (defaults to true when not in CI)
84
- */
85
- packages?: boolean;
86
- /**
87
- * Enable version override prompt (defaults to true when not in CI)
88
- */
89
- versions?: boolean;
90
- };
91
108
  /**
92
109
  * Whether to perform a dry run (no changes pushed or PR created)
93
110
  * @default false
@@ -99,9 +116,8 @@ interface ReleaseOptions {
99
116
  */
100
117
  safeguards?: boolean;
101
118
  /**
102
- * GitHub token for authentication
119
+ * Pull request configuration
103
120
  */
104
- githubToken: string;
105
121
  pullRequest?: {
106
122
  /**
107
123
  * Title for the release pull request
@@ -132,8 +148,6 @@ interface ReleaseResult {
132
148
  */
133
149
  created: boolean;
134
150
  }
135
- //#endregion
136
- //#region src/release.d.ts
137
151
  declare function release(options: ReleaseOptions): Promise<ReleaseResult | null>;
138
152
  //#endregion
139
- export { release };
153
+ export { type PublishOptions, type ReleaseOptions, type ReleaseResult, publish, release };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as Eta } from "./eta-Boh7yPZi.mjs";
2
- import process from "node:process";
3
2
  import { getCommits } from "commit-parser";
3
+ import process from "node:process";
4
4
  import farver from "farver";
5
5
  import { exec } from "tinyexec";
6
6
  import { dedent } from "@luxass/utils";
@@ -8,9 +8,34 @@ import { join } from "node:path";
8
8
  import { readFile, writeFile } from "node:fs/promises";
9
9
  import prompts from "prompts";
10
10
 
11
+ //#region src/publish.ts
12
+ function publish(_options) {}
13
+
14
+ //#endregion
11
15
  //#region src/utils.ts
12
- const globalOptions = { dryRun: false };
16
+ const globalOptions = {
17
+ dryRun: false,
18
+ verbose: false
19
+ };
13
20
  const isCI = typeof process.env.CI === "string" && process.env.CI !== "" && process.env.CI.toLowerCase() !== "false";
21
+ const logger = {
22
+ info: (...args) => {
23
+ console.info(farver.cyan("[info]:"), ...args);
24
+ },
25
+ debug: (...args) => {
26
+ console.debug(farver.gray("[debug]:"), ...args);
27
+ },
28
+ warn: (...args) => {
29
+ console.warn(farver.yellow("[warn]:"), ...args);
30
+ },
31
+ error: (...args) => {
32
+ console.error(farver.red("[error]:"), ...args);
33
+ },
34
+ log: (...args) => {
35
+ if (!globalOptions.verbose) return;
36
+ console.log(...args);
37
+ }
38
+ };
14
39
  async function run(bin, args, opts = {}) {
15
40
  return exec(bin, args, {
16
41
  throwOnError: true,
@@ -22,18 +47,49 @@ async function run(bin, args, opts = {}) {
22
47
  });
23
48
  }
24
49
  async function dryRun(bin, args, opts) {
25
- return console.log(farver.blue(`[dryrun] ${bin} ${args.join(" ")}`), opts || "");
50
+ return logger.log(farver.blue(`[dryrun] ${bin} ${args.join(" ")}`), opts || "");
26
51
  }
27
52
  const runIfNotDry = globalOptions.dryRun ? dryRun : run;
53
+ function exitWithError(message, hint) {
54
+ logger.error(farver.bold(message));
55
+ if (hint) console.error(farver.gray(` ${hint}`));
56
+ process.exit(1);
57
+ }
58
+ function normalizeSharedOptions(options) {
59
+ const { workspaceRoot = process.cwd(), githubToken = "", verbose = false, repo, packages = true, prompts: prompts$1 = {
60
+ packages: true,
61
+ versions: true
62
+ },...rest } = options;
63
+ globalOptions.verbose = verbose;
64
+ if (!githubToken.trim()) exitWithError("GitHub token is required", "Set GITHUB_TOKEN environment variable or pass it in options");
65
+ if (!repo || !repo.trim() || !repo.includes("/")) exitWithError("Repository (repo) is required", "Specify the repository in 'owner/repo' format (e.g., 'octocat/hello-world')");
66
+ const [owner, name] = options.repo.split("/");
67
+ if (!owner || !name) exitWithError(`Invalid repo format: "${options.repo}"`, "Expected format: \"owner/repo\" (e.g., \"octocat/hello-world\")");
68
+ return {
69
+ ...rest,
70
+ packages,
71
+ prompts: prompts$1,
72
+ workspaceRoot,
73
+ githubToken,
74
+ owner,
75
+ repo,
76
+ verbose
77
+ };
78
+ }
28
79
 
29
80
  //#endregion
30
81
  //#region src/commits.ts
31
82
  async function getLastPackageTag(packageName, workspaceRoot) {
32
- const { stdout } = await run("git", ["tag", "--list"], { nodeOptions: {
33
- cwd: workspaceRoot,
34
- stdio: "pipe"
35
- } });
36
- return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse().find((tag) => tag.startsWith(`${packageName}@`));
83
+ try {
84
+ const { stdout } = await run("git", ["tag", "--list"], { nodeOptions: {
85
+ cwd: workspaceRoot,
86
+ stdio: "pipe"
87
+ } });
88
+ return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse().find((tag) => tag.startsWith(`${packageName}@`));
89
+ } catch (err) {
90
+ logger.warn(`Failed to get tags for package ${packageName}: ${err.message}`);
91
+ return;
92
+ }
37
93
  }
38
94
  function determineHighestBump(commits) {
39
95
  if (commits.length === 0) return "none";
@@ -46,35 +102,42 @@ function determineHighestBump(commits) {
46
102
  }
47
103
  return highestBump;
48
104
  }
49
- async function getPackageCommits(pkg, workspaceRoot) {
105
+ /**
106
+ * Retrieves commits that affect a specific workspace package since its last tag.
107
+ *
108
+ * @param {string} workspaceRoot - The root directory of the workspace.
109
+ * @param {WorkspacePackage} pkg - The workspace package to analyze.
110
+ * @returns {Promise<GitCommit[]>} A promise that resolves to an array of GitCommit objects affecting the package.
111
+ */
112
+ async function getCommitsForWorkspacePackage(workspaceRoot, pkg) {
50
113
  const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
51
114
  const allCommits = getCommits({
52
115
  from: lastTag,
53
- to: "HEAD"
116
+ to: "HEAD",
117
+ cwd: workspaceRoot
118
+ });
119
+ logger.log(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
120
+ const touchedCommitHashes = getCommits({
121
+ from: lastTag,
122
+ to: "HEAD",
123
+ cwd: workspaceRoot,
124
+ folder: pkg.path
54
125
  });
55
- console.log(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
56
- const touchedCommitHashes = await getCommitsTouchingPackage(lastTag || "HEAD", "HEAD", pkg.path, workspaceRoot);
57
126
  const touchedSet = new Set(touchedCommitHashes);
58
- const packageCommits = allCommits.filter((commit) => touchedSet.has(commit.shortHash));
59
- console.log(`${packageCommits.length} commits affect ${pkg.name}`);
127
+ const packageCommits = allCommits.filter((commit) => touchedSet.has(commit));
128
+ logger.log(`${packageCommits.length} commits affect ${pkg.name}`);
60
129
  return packageCommits;
61
130
  }
62
- async function analyzePackageCommits(pkg, workspaceRoot) {
63
- return determineHighestBump(await getPackageCommits(pkg, workspaceRoot));
64
- }
65
- /**
66
- * Analyze commits for multiple packages to determine version bumps
67
- *
68
- * @param packages - Packages to analyze
69
- * @param workspaceRoot - Root directory of the workspace
70
- * @returns Map of package names to their bump types
71
- */
72
- async function analyzeCommits(packages, workspaceRoot) {
131
+ async function getWorkspacePackageCommits(workspaceRoot, packages) {
73
132
  const changedPackages = /* @__PURE__ */ new Map();
74
- for (const pkg of packages) {
75
- const bump = await analyzePackageCommits(pkg, workspaceRoot);
76
- if (bump !== "none") changedPackages.set(pkg.name, bump);
77
- }
133
+ const promises = packages.map(async (pkg) => {
134
+ return {
135
+ pkgName: pkg.name,
136
+ commits: await getCommitsForWorkspacePackage(workspaceRoot, pkg)
137
+ };
138
+ });
139
+ const results = await Promise.all(promises);
140
+ for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
78
141
  return changedPackages;
79
142
  }
80
143
  function determineBumpType(commit) {
@@ -95,24 +158,6 @@ function determineBumpType(commit) {
95
158
  default: return "none";
96
159
  }
97
160
  }
98
- async function getCommitsTouchingPackage(from, to, packagePath, workspaceRoot) {
99
- try {
100
- const { stdout } = await run("git", [
101
- "log",
102
- "--pretty=format:%h",
103
- from === "HEAD" ? "HEAD" : `${from}...${to}`,
104
- "--",
105
- packagePath
106
- ], { nodeOptions: {
107
- cwd: workspaceRoot,
108
- stdio: "pipe"
109
- } });
110
- return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
111
- } catch (error) {
112
- console.error(`Error getting commits touching package: ${error}`);
113
- return [];
114
- }
115
- }
116
161
 
117
162
  //#endregion
118
163
  //#region src/git.ts
@@ -129,7 +174,7 @@ async function isWorkingDirectoryClean(workspaceRoot) {
129
174
  } })).stdout.trim() !== "") return false;
130
175
  return true;
131
176
  } catch (err) {
132
- console.error("Error checking git status:", err);
177
+ logger.error("Error checking git status:", err);
133
178
  return false;
134
179
  }
135
180
  }
@@ -313,10 +358,10 @@ async function getExistingPullRequest({ owner, repo, branch, githubToken }) {
313
358
  draft: firstPullRequest.draft,
314
359
  html_url: firstPullRequest.html_url
315
360
  };
316
- console.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
361
+ logger.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
317
362
  return pullRequest;
318
363
  } catch (err) {
319
- console.error("Error fetching pull request:", err);
364
+ logger.error("Error fetching pull request:", err);
320
365
  return null;
321
366
  }
322
367
  }
@@ -346,7 +391,7 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
346
391
  const pr = await res.json();
347
392
  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");
348
393
  const action = isUpdate ? "Updated" : "Created";
349
- console.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
394
+ logger.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
350
395
  return {
351
396
  number: pr.number,
352
397
  title: pr.title,
@@ -355,7 +400,7 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
355
400
  html_url: pr.html_url
356
401
  };
357
402
  } catch (err) {
358
- console.error(`Error upserting pull request:`, err);
403
+ logger.error(`Error upserting pull request:`, err);
359
404
  throw err;
360
405
  }
361
406
  }
@@ -392,6 +437,65 @@ function generatePullRequestBody(updates, body) {
392
437
  })) });
393
438
  }
394
439
 
440
+ //#endregion
441
+ //#region src/prompts.ts
442
+ async function selectPackagePrompt(packages) {
443
+ const response = await prompts({
444
+ type: "multiselect",
445
+ name: "selectedPackages",
446
+ message: "Select packages to release",
447
+ choices: packages.map((pkg) => ({
448
+ title: `${pkg.name} (${farver.bold(pkg.version)})`,
449
+ value: pkg.name,
450
+ selected: true
451
+ })),
452
+ min: 1,
453
+ hint: "Space to select/deselect. Return to submit.",
454
+ instructions: false
455
+ });
456
+ if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
457
+ return response.selectedPackages;
458
+ }
459
+ async function promptVersionOverride(pkg, workspaceRoot, currentVersion, suggestedVersion, suggestedBumpType) {
460
+ const choices = [{
461
+ title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
462
+ value: "suggested"
463
+ }];
464
+ for (const bumpType of [
465
+ "patch",
466
+ "minor",
467
+ "major"
468
+ ]) if (bumpType !== suggestedBumpType) {
469
+ const version = getNextVersion(currentVersion, bumpType);
470
+ choices.push({
471
+ title: `${bumpType}: ${version}`,
472
+ value: bumpType
473
+ });
474
+ }
475
+ choices.push({
476
+ title: "Custom version",
477
+ value: "custom"
478
+ });
479
+ const response = await prompts([{
480
+ type: "select",
481
+ name: "choice",
482
+ message: `${pkg.name} (${currentVersion}):`,
483
+ choices,
484
+ initial: 0
485
+ }, {
486
+ type: (prev) => prev === "custom" ? "text" : null,
487
+ name: "customVersion",
488
+ message: "Enter custom version:",
489
+ initial: suggestedVersion,
490
+ validate: (value) => {
491
+ return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(value) || "Invalid semver version (e.g., 1.0.0)";
492
+ }
493
+ }]);
494
+ if (response.choice === "suggested") return suggestedVersion;
495
+ else if (response.choice === "custom") return response.customVersion;
496
+ else return getNextVersion(currentVersion, response.choice);
497
+ }
498
+
395
499
  //#endregion
396
500
  //#region src/version.ts
397
501
  function isValidSemver(version) {
@@ -400,11 +504,7 @@ function isValidSemver(version) {
400
504
  function validateSemver(version) {
401
505
  if (!isValidSemver(version)) throw new Error(`Invalid semver version: ${version}`);
402
506
  }
403
- /**
404
- * Calculate the new version based on current version and bump type
405
- * Pure function - no side effects, easily testable
406
- */
407
- function calculateNewVersion(currentVersion, bump) {
507
+ function getNextVersion(currentVersion, bump) {
408
508
  if (bump === "none") return currentVersion;
409
509
  validateSemver(currentVersion);
410
510
  const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
@@ -433,7 +533,7 @@ function calculateNewVersion(currentVersion, bump) {
433
533
  * Create a version update object
434
534
  */
435
535
  function createVersionUpdate(pkg, bump, hasDirectChanges) {
436
- const newVersion = calculateNewVersion(pkg.version, bump);
536
+ const newVersion = getNextVersion(pkg.version, bump);
437
537
  return {
438
538
  package: pkg,
439
539
  currentVersion: pkg.version,
@@ -443,8 +543,37 @@ function createVersionUpdate(pkg, bump, hasDirectChanges) {
443
543
  };
444
544
  }
445
545
  /**
446
- * Update a package.json file with new version and dependency versions
546
+ * Infer version updates from package commits with optional interactive overrides
547
+ *
548
+ * @param workspacePackages - All workspace packages
549
+ * @param packageCommits - Map of package names to their commits
550
+ * @param workspaceRoot - Root directory for prompts
551
+ * @param showPrompt - Whether to show prompts for version overrides
552
+ * @returns Version updates for packages with changes
447
553
  */
554
+ async function inferVersionUpdates(workspacePackages, packageCommits, workspaceRoot, showPrompt) {
555
+ const versionUpdates = [];
556
+ for (const [pkgName, commits] of packageCommits) {
557
+ if (commits.length === 0) continue;
558
+ const pkg = workspacePackages.find((p) => p.name === pkgName);
559
+ if (!pkg) continue;
560
+ const bump = determineHighestBump(commits);
561
+ if (bump === "none") {
562
+ logger.info(`No version bump needed for package ${pkg.name}`);
563
+ continue;
564
+ }
565
+ let newVersion = getNextVersion(pkg.version, bump);
566
+ if (!isCI && showPrompt) newVersion = await promptVersionOverride(pkg, workspaceRoot, pkg.version, newVersion, bump);
567
+ versionUpdates.push({
568
+ package: pkg,
569
+ currentVersion: pkg.version,
570
+ newVersion,
571
+ bumpType: bump,
572
+ hasDirectChanges: true
573
+ });
574
+ }
575
+ return versionUpdates;
576
+ }
448
577
  async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
449
578
  const packageJsonPath = join(pkg.path, "package.json");
450
579
  const content = await readFile(packageJsonPath, "utf-8");
@@ -569,148 +698,6 @@ async function updateAllPackageJsonFiles(updates) {
569
698
  }));
570
699
  }
571
700
 
572
- //#endregion
573
- //#region src/prompts.ts
574
- /**
575
- * Get commits for a package grouped by conventional commit type
576
- *
577
- * @param pkg - The workspace package
578
- * @param workspaceRoot - Root directory of the workspace
579
- * @param limit - Maximum number of commits to return (default: 10)
580
- * @returns Commits grouped by type
581
- */
582
- async function getCommitsForPackage(pkg, workspaceRoot, limit = 10) {
583
- const limitedCommits = (await getPackageCommits(pkg, workspaceRoot)).slice(0, limit);
584
- const grouped = {
585
- feat: [],
586
- fix: [],
587
- perf: [],
588
- chore: [],
589
- docs: [],
590
- style: [],
591
- refactor: [],
592
- test: [],
593
- build: [],
594
- ci: [],
595
- revert: [],
596
- other: []
597
- };
598
- for (const commit of limitedCommits) if (commit.type && commit.type in grouped) grouped[commit.type].push(commit);
599
- else grouped.other.push(commit);
600
- return grouped;
601
- }
602
- /**
603
- * Format grouped commits into a readable string
604
- */
605
- function formatCommitGroups(grouped) {
606
- const lines = [];
607
- const typeLabels = {
608
- feat: "Features",
609
- fix: "Bug Fixes",
610
- perf: "Performance",
611
- chore: "Chores",
612
- docs: "Documentation",
613
- style: "Styling",
614
- refactor: "Refactoring",
615
- test: "Tests",
616
- build: "Build",
617
- ci: "CI",
618
- revert: "Reverts",
619
- other: "Other"
620
- };
621
- for (const type of [
622
- "feat",
623
- "fix",
624
- "perf",
625
- "refactor",
626
- "test",
627
- "docs",
628
- "style",
629
- "build",
630
- "ci",
631
- "chore",
632
- "revert",
633
- "other"
634
- ]) {
635
- const commits = grouped[type];
636
- if (commits.length > 0) {
637
- lines.push(`\n${typeLabels[type]}:`);
638
- for (const commit of commits) {
639
- const scope = commit.scope ? `(${commit.scope})` : "";
640
- const breaking = commit.isBreaking ? " ⚠️ BREAKING" : "";
641
- lines.push(` • ${commit.type}${scope}: ${commit.message}${breaking}`);
642
- }
643
- }
644
- }
645
- return lines.join("\n");
646
- }
647
- async function selectPackagePrompt(packages) {
648
- const response = await prompts({
649
- type: "multiselect",
650
- name: "selectedPackages",
651
- message: "Select packages to release",
652
- choices: packages.map((pkg) => ({
653
- title: `${pkg.name} (${farver.bold(pkg.version)})`,
654
- value: pkg.name,
655
- selected: true
656
- })),
657
- min: 1,
658
- hint: "Space to select/deselect. Return to submit.",
659
- instructions: false
660
- });
661
- if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
662
- return response.selectedPackages;
663
- }
664
- async function promptVersionOverride(pkg, workspaceRoot, currentVersion, suggestedVersion, suggestedBumpType) {
665
- const commitSummary = formatCommitGroups(await getCommitsForPackage(pkg, workspaceRoot));
666
- if (commitSummary.trim()) console.log(`\nRecent changes in ${pkg.name}:${commitSummary}\n`);
667
- const choices = [{
668
- title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
669
- value: "suggested"
670
- }];
671
- for (const bumpType of [
672
- "patch",
673
- "minor",
674
- "major"
675
- ]) if (bumpType !== suggestedBumpType) {
676
- const version = calculateNewVersion(currentVersion, bumpType);
677
- choices.push({
678
- title: `${bumpType}: ${version}`,
679
- value: bumpType
680
- });
681
- }
682
- choices.push({
683
- title: "Custom version",
684
- value: "custom"
685
- });
686
- const response = await prompts([{
687
- type: "select",
688
- name: "choice",
689
- message: `${pkg.name} (${currentVersion}):`,
690
- choices,
691
- initial: 0
692
- }, {
693
- type: (prev) => prev === "custom" ? "text" : null,
694
- name: "customVersion",
695
- message: "Enter custom version:",
696
- initial: suggestedVersion,
697
- validate: (value) => {
698
- return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(value) || "Invalid semver version (e.g., 1.0.0)";
699
- }
700
- }]);
701
- if (response.choice === "suggested") return suggestedVersion;
702
- else if (response.choice === "custom") return response.customVersion;
703
- else return calculateNewVersion(currentVersion, response.choice);
704
- }
705
- async function promptVersionOverrides(packages, workspaceRoot) {
706
- const overrides = /* @__PURE__ */ new Map();
707
- for (const item of packages) {
708
- const newVersion = await promptVersionOverride(item.package, workspaceRoot, item.currentVersion, item.suggestedVersion, item.bumpType);
709
- overrides.set(item.package.name, newVersion);
710
- }
711
- return overrides;
712
- }
713
-
714
701
  //#endregion
715
702
  //#region src/workspace.ts
716
703
  async function discoverWorkspacePackages(workspaceRoot, options) {
@@ -720,29 +707,25 @@ async function discoverWorkspacePackages(workspaceRoot, options) {
720
707
  else if (Array.isArray(options.packages)) {
721
708
  workspaceOptions = {
722
709
  excludePrivate: false,
723
- included: options.packages
710
+ include: options.packages
724
711
  };
725
712
  explicitPackages = options.packages;
726
713
  } else {
727
714
  workspaceOptions = options.packages;
728
- if (options.packages.included) explicitPackages = options.packages.included;
715
+ if (options.packages.include) explicitPackages = options.packages.include;
729
716
  }
730
- const workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
717
+ let workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
731
718
  if (explicitPackages) {
732
719
  const foundNames = new Set(workspacePackages.map((p) => p.name));
733
720
  const missing = explicitPackages.filter((p) => !foundNames.has(p));
734
- if (missing.length > 0) throw new Error(`Packages not found in workspace: ${missing.join(", ")}`);
721
+ 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");
735
722
  }
736
- let packagesToAnalyze = workspacePackages;
737
723
  const isPackagePromptEnabled = options.prompts?.packages !== false;
738
724
  if (!isCI && isPackagePromptEnabled && !explicitPackages) {
739
725
  const selectedNames = await selectPackagePrompt(workspacePackages);
740
- packagesToAnalyze = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
726
+ workspacePackages = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
741
727
  }
742
- return {
743
- workspacePackages,
744
- packagesToAnalyze
745
- };
728
+ return workspacePackages;
746
729
  }
747
730
  async function findWorkspacePackages(workspaceRoot, options) {
748
731
  try {
@@ -769,131 +752,105 @@ async function findWorkspacePackages(workspaceRoot, options) {
769
752
  version: rawProject.version,
770
753
  path: rawProject.path,
771
754
  packageJson,
772
- workspaceDependencies: extractWorkspaceDependencies(rawProject.dependencies, allPackageNames),
773
- workspaceDevDependencies: extractWorkspaceDependencies(rawProject.devDependencies, allPackageNames)
755
+ workspaceDependencies: Object.keys(rawProject.dependencies || []).filter((dep) => {
756
+ return allPackageNames.has(dep);
757
+ }),
758
+ workspaceDevDependencies: Object.keys(rawProject.devDependencies || []).filter((dep) => {
759
+ return allPackageNames.has(dep);
760
+ })
774
761
  };
775
762
  });
776
763
  const packages = await Promise.all(promises);
777
- if (excludedPackages.size > 0) console.info(`${farver.cyan("[info]:")} Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
764
+ if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
778
765
  return packages.filter((pkg) => pkg !== null);
779
766
  } catch (err) {
780
- console.error("Error discovering workspace packages:", err);
767
+ logger.error("Error discovering workspace packages:", err);
781
768
  throw err;
782
769
  }
783
770
  }
784
771
  function shouldIncludePackage(pkg, options) {
785
772
  if (!options) return true;
786
773
  if (options.excludePrivate && pkg.private) return false;
787
- if (options.included && options.included.length > 0) {
788
- if (!options.included.includes(pkg.name)) return false;
774
+ if (options.include && options.include.length > 0) {
775
+ if (!options.include.includes(pkg.name)) return false;
789
776
  }
790
- if (options.excluded?.includes(pkg.name)) return false;
777
+ if (options.exclude?.includes(pkg.name)) return false;
791
778
  return true;
792
779
  }
793
- function extractWorkspaceDependencies(dependencies, workspacePackages) {
794
- if (!dependencies) return [];
795
- return Object.keys(dependencies).filter((dep) => {
796
- return workspacePackages.has(dep);
797
- });
798
- }
799
780
 
800
781
  //#endregion
801
782
  //#region src/release.ts
802
783
  async function release(options) {
803
- const { dryRun: dryRun$1 = false, safeguards = true, workspaceRoot = process.cwd(), releaseBranch = "release/next", githubToken } = options;
804
- globalOptions.dryRun = dryRun$1;
805
- if (githubToken.trim() === "" || githubToken == null) throw new Error("GitHub token is required");
806
- const [owner, repo] = options.repo.split("/");
807
- if (!owner || !repo) throw new Error(`Invalid repo format: ${options.repo}. Expected "owner/repo".`);
808
- if (safeguards && !await isWorkingDirectoryClean(workspaceRoot)) {
809
- console.error("Working directory is not clean. Please commit or stash your changes before proceeding.");
810
- return null;
811
- }
812
- const { workspacePackages, packagesToAnalyze } = await discoverWorkspacePackages(workspaceRoot, options);
813
- if (packagesToAnalyze.length === 0) {
814
- console.log("No packages found to analyze for release.");
784
+ const normalizedOptions = normalizeSharedOptions(options);
785
+ normalizedOptions.dryRun ??= false;
786
+ normalizedOptions.releaseBranch ??= "release/next";
787
+ normalizedOptions.safeguards ??= true;
788
+ globalOptions.dryRun = normalizedOptions.dryRun;
789
+ const workspaceRoot = normalizedOptions.workspaceRoot;
790
+ if (normalizedOptions.safeguards && !await isWorkingDirectoryClean(workspaceRoot)) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
791
+ const workspacePackages = await discoverWorkspacePackages(workspaceRoot, options);
792
+ if (workspacePackages.length === 0) {
793
+ logger.log("No packages found to analyze for release.");
815
794
  return null;
816
795
  }
817
- const changedPackages = await analyzeCommits(packagesToAnalyze, workspaceRoot);
818
- if (changedPackages.size === 0) throw new Error("No packages have changes requiring a release");
819
- let versionUpdates = [];
820
- for (const [pkgName, bump] of changedPackages) {
821
- const pkg = workspacePackages.find((p) => p.name === pkgName);
822
- if (pkg) versionUpdates.push(createVersionUpdate(pkg, bump, true));
823
- }
824
- const isVersionPromptEnabled = options.prompts?.versions !== false;
825
- if (!isCI && isVersionPromptEnabled) {
826
- const versionOverrides = await promptVersionOverrides(versionUpdates.map((u) => ({
827
- package: u.package,
828
- currentVersion: u.currentVersion,
829
- suggestedVersion: u.newVersion,
830
- bumpType: u.bumpType
831
- })), workspaceRoot);
832
- versionUpdates = versionUpdates.map((update) => {
833
- const overriddenVersion = versionOverrides.get(update.package.name);
834
- if (overriddenVersion && overriddenVersion !== update.newVersion) return {
835
- ...update,
836
- newVersion: overriddenVersion
837
- };
838
- return update;
839
- });
840
- }
796
+ const versionUpdates = await inferVersionUpdates(workspacePackages, await getWorkspacePackageCommits(workspaceRoot, workspacePackages), workspaceRoot, options.prompts?.versions !== false);
797
+ if (versionUpdates.length === 0) exitWithError("No packages have changes requiring a release", "Make sure you have commits since the last release");
841
798
  const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, versionUpdates);
842
799
  const currentBranch = await getCurrentBranch(workspaceRoot);
843
800
  const existingPullRequest = await getExistingPullRequest({
844
- owner,
845
- repo,
846
- branch: releaseBranch,
847
- githubToken
801
+ owner: normalizedOptions.owner,
802
+ repo: normalizedOptions.repo,
803
+ branch: normalizedOptions.releaseBranch,
804
+ githubToken: normalizedOptions.githubToken
848
805
  });
849
806
  const prExists = !!existingPullRequest;
850
- if (prExists) console.log("Existing pull request found:", existingPullRequest.html_url);
851
- else console.log("No existing pull request found, will create new one");
852
- const branchExists = await doesBranchExist(releaseBranch, workspaceRoot);
807
+ if (prExists) logger.log("Existing pull request found:", existingPullRequest.html_url);
808
+ else logger.log("No existing pull request found, will create new one");
809
+ const branchExists = await doesBranchExist(normalizedOptions.releaseBranch, workspaceRoot);
853
810
  if (!branchExists) {
854
- console.log("Creating release branch:", releaseBranch);
855
- await createBranch(releaseBranch, currentBranch, workspaceRoot);
811
+ logger.log("Creating release branch:", normalizedOptions.releaseBranch);
812
+ await createBranch(normalizedOptions.releaseBranch, currentBranch, workspaceRoot);
856
813
  }
857
- if (!await checkoutBranch(releaseBranch, workspaceRoot)) throw new Error(`Failed to checkout branch: ${releaseBranch}`);
814
+ if (!await checkoutBranch(normalizedOptions.releaseBranch, workspaceRoot)) throw new Error(`Failed to checkout branch: ${normalizedOptions.releaseBranch}`);
858
815
  if (branchExists) {
859
- console.log("Pulling latest changes from remote");
860
- if (!await pullLatestChanges(releaseBranch, workspaceRoot)) console.log("Warning: Failed to pull latest changes, continuing anyway");
816
+ logger.log("Pulling latest changes from remote");
817
+ if (!await pullLatestChanges(normalizedOptions.releaseBranch, workspaceRoot)) logger.log("Warning: Failed to pull latest changes, continuing anyway");
861
818
  }
862
- console.log("Rebasing release branch onto", currentBranch);
819
+ logger.log("Rebasing release branch onto", currentBranch);
863
820
  await rebaseBranch(currentBranch, workspaceRoot);
864
821
  await updateAllPackageJsonFiles(allUpdates);
865
822
  const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
866
- const isBranchAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
823
+ const isBranchAhead = await isBranchAheadOfRemote(normalizedOptions.releaseBranch, workspaceRoot);
867
824
  if (!hasCommitted && !isBranchAhead) {
868
- console.log("No changes to commit and branch is in sync with remote");
825
+ logger.log("No changes to commit and branch is in sync with remote");
869
826
  await checkoutBranch(currentBranch, workspaceRoot);
870
827
  if (prExists) {
871
- console.log("No updates needed, PR is already up to date");
828
+ logger.log("No updates needed, PR is already up to date");
872
829
  return {
873
830
  updates: allUpdates,
874
831
  prUrl: existingPullRequest.html_url,
875
832
  created: false
876
833
  };
877
834
  } else {
878
- console.error("No changes to commit, and no existing PR. Nothing to do.");
835
+ logger.error("No changes to commit, and no existing PR. Nothing to do.");
879
836
  return null;
880
837
  }
881
838
  }
882
- console.log("Pushing changes to remote");
883
- await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
839
+ logger.log("Pushing changes to remote");
840
+ await pushBranch(normalizedOptions.releaseBranch, workspaceRoot, { forceWithLease: true });
884
841
  const prTitle = existingPullRequest?.title || options.pullRequest?.title || "chore: update package versions";
885
842
  const prBody = generatePullRequestBody(allUpdates, options.pullRequest?.body);
886
843
  const pullRequest = await upsertPullRequest({
887
- owner,
888
- repo,
844
+ owner: normalizedOptions.owner,
845
+ repo: normalizedOptions.repo,
889
846
  pullNumber: existingPullRequest?.number,
890
847
  title: prTitle,
891
848
  body: prBody,
892
- head: releaseBranch,
849
+ head: normalizedOptions.releaseBranch,
893
850
  base: currentBranch,
894
- githubToken
851
+ githubToken: normalizedOptions.githubToken
895
852
  });
896
- console.log(prExists ? "Updated pull request:" : "Created pull request:", pullRequest?.html_url);
853
+ logger.log(prExists ? "Updated pull request:" : "Created pull request:", pullRequest?.html_url);
897
854
  await checkoutBranch(currentBranch, workspaceRoot);
898
855
  return {
899
856
  updates: allUpdates,
@@ -903,4 +860,4 @@ async function release(options) {
903
860
  }
904
861
 
905
862
  //#endregion
906
- export { release };
863
+ export { publish, release };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0-beta.6",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",