@reelout/core 0.1.1 → 0.2.0

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.
Files changed (125) hide show
  1. package/dist/analyzer.js +38 -0
  2. package/dist/analyzer.js.map +1 -0
  3. package/dist/builder.js +52 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/changelog.js +58 -0
  6. package/dist/changelog.js.map +1 -0
  7. package/dist/formatError-BXNdyRfu.js +23 -0
  8. package/dist/formatError-BXNdyRfu.js.map +1 -0
  9. package/dist/index.js +175 -1
  10. package/dist/index.js.map +1 -0
  11. package/dist/types-la_KkjCS.js +1 -0
  12. package/dist/types.js +2 -0
  13. package/dist/workflow-BoVmbvH6.js +2342 -0
  14. package/dist/workflow-BoVmbvH6.js.map +1 -0
  15. package/package.json +27 -3
  16. package/src/analyzer.ts +63 -0
  17. package/src/builder.ts +104 -0
  18. package/src/cache/TTLCache.ts +100 -0
  19. package/src/cache/index.ts +4 -0
  20. package/src/changelog.ts +97 -0
  21. package/src/config/const.ts +97 -0
  22. package/src/config/index.ts +11 -0
  23. package/src/config/isSemVerGitTag.ts +25 -0
  24. package/src/config/normalizeCommitFilter.ts +37 -0
  25. package/src/config/normalizeReleaseRule.ts +42 -0
  26. package/src/config/normalizeWorkspace.ts +34 -0
  27. package/src/config/resolvePlugin.ts +56 -0
  28. package/src/config/resolveReleaseTarget.ts +71 -0
  29. package/src/config/sanitizePrereleaseLabel.ts +18 -0
  30. package/src/git/branch/getCurrentGitBranch.ts +18 -0
  31. package/src/git/branch/index.ts +4 -0
  32. package/src/git/commit/createGitCommit.ts +29 -0
  33. package/src/git/commit/extractMentions.ts +34 -0
  34. package/src/git/commit/getCurrentGitCommit.ts +14 -0
  35. package/src/git/commit/getParsedCommit.ts +53 -0
  36. package/src/git/commit/index.ts +9 -0
  37. package/src/git/commit/parseCommit.ts +99 -0
  38. package/src/git/commit/parseGitPrettyFormat.ts +76 -0
  39. package/src/git/history/countGitHistory.ts +28 -0
  40. package/src/git/history/expandGitHistory.ts +108 -0
  41. package/src/git/history/index.ts +9 -0
  42. package/src/git/history/mergeGitHistory.ts +76 -0
  43. package/src/git/history/restoreFullGitHistory.ts +19 -0
  44. package/src/git/history/showGitHistory.ts +80 -0
  45. package/src/git/history/showParsedGitHistory.ts +30 -0
  46. package/src/git/index.ts +9 -0
  47. package/src/git/repository/ensureGitUserConfig.ts +31 -0
  48. package/src/git/repository/getGitConfigValue.ts +27 -0
  49. package/src/git/repository/getGitRemote.ts +20 -0
  50. package/src/git/repository/index.ts +10 -0
  51. package/src/git/repository/isGitShallowRepository.ts +21 -0
  52. package/src/git/repository/pushGitChanges.ts +25 -0
  53. package/src/git/repository/resolveGitRepository.ts +40 -0
  54. package/src/git/repository/withShallowLock.ts +62 -0
  55. package/src/git/revision/detectGitRelationship.ts +43 -0
  56. package/src/git/revision/index.ts +7 -0
  57. package/src/git/revision/isGitAncestor.ts +33 -0
  58. package/src/git/revision/readGitFile.ts +29 -0
  59. package/src/git/revision/resolveGitRevision.ts +23 -0
  60. package/src/git/tag/createGitTag.ts +25 -0
  61. package/src/git/tag/deleteGitTag.ts +17 -0
  62. package/src/git/tag/ensureGitTag.ts +63 -0
  63. package/src/git/tag/generateGitTagName.ts +43 -0
  64. package/src/git/tag/getCurrentGitTag.ts +23 -0
  65. package/src/git/tag/getGitTagMessage.ts +35 -0
  66. package/src/git/tag/getLatestGitTag.ts +22 -0
  67. package/src/git/tag/index.ts +13 -0
  68. package/src/git/tag/isGitTagConnected.ts +33 -0
  69. package/src/git/tag/isGitTagPointed.ts +38 -0
  70. package/src/git/tag/isGitTagPresent.ts +17 -0
  71. package/src/index.ts +8 -2
  72. package/src/types.ts +217 -0
  73. package/src/util/buildTrailerPattern.ts +20 -0
  74. package/src/util/compact.ts +14 -0
  75. package/src/util/execCommand.ts +130 -0
  76. package/src/util/execGit.ts +18 -0
  77. package/src/util/formatError.ts +21 -0
  78. package/src/util/formatFile.ts +41 -0
  79. package/src/util/index.ts +16 -0
  80. package/src/util/interpolate.ts +141 -0
  81. package/src/util/isCI.ts +23 -0
  82. package/src/util/isDefined.ts +12 -0
  83. package/src/util/isMatch.ts +43 -0
  84. package/src/util/readJson.ts +47 -0
  85. package/src/util/unique.ts +26 -0
  86. package/src/util/writeJson.ts +28 -0
  87. package/src/workflow/analysis/analyzePackageChanges.ts +54 -0
  88. package/src/workflow/analysis/index.ts +9 -0
  89. package/src/workflow/analysis/isCommitForPackage.ts +23 -0
  90. package/src/workflow/analysis/promotePrereleaseVersions.ts +50 -0
  91. package/src/workflow/analysis/propagateDependencyUpdates.ts +71 -0
  92. package/src/workflow/analysis/sliceUnreleased.ts +36 -0
  93. package/src/workflow/analysis/streamPackageCommits.ts +34 -0
  94. package/src/workflow/changelog/buildChangelog.ts +79 -0
  95. package/src/workflow/changelog/buildReleaseNotes.ts +50 -0
  96. package/src/workflow/changelog/extractLatestVersion.ts +22 -0
  97. package/src/workflow/changelog/extractPlainText.ts +38 -0
  98. package/src/workflow/changelog/extractPreviousVersion.ts +77 -0
  99. package/src/workflow/changelog/extractSemVer.ts +33 -0
  100. package/src/workflow/changelog/index.ts +12 -0
  101. package/src/workflow/changelog/iterateChangelogVersions.ts +70 -0
  102. package/src/workflow/changelog/prependChangelog.ts +59 -0
  103. package/src/workflow/changelog/writeChangelog.ts +27 -0
  104. package/src/workflow/committing/generateReleaseCommitMessage.ts +96 -0
  105. package/src/workflow/committing/index.ts +5 -0
  106. package/src/workflow/committing/isReleaseCommit.ts +52 -0
  107. package/src/workflow/discovery/buildDependencyGraph.ts +43 -0
  108. package/src/workflow/discovery/discoverPackageTag.ts +98 -0
  109. package/src/workflow/discovery/index.ts +9 -0
  110. package/src/workflow/discovery/resolvePackageStableTag.ts +88 -0
  111. package/src/workflow/discovery/resolvePackageTag.ts +45 -0
  112. package/src/workflow/discovery/sortPackages.ts +73 -0
  113. package/src/workflow/discovery/verifyPackageTag.ts +43 -0
  114. package/src/workflow/index.ts +10 -0
  115. package/src/workflow/report/generateReleaseReport.ts +76 -0
  116. package/src/workflow/report/index.ts +5 -0
  117. package/src/workflow/report/printReleaseReport.ts +97 -0
  118. package/src/workflow/runner/index.ts +4 -0
  119. package/src/workflow/runner/runWorkflow.ts +120 -0
  120. package/src/workflow/versioning/bumpVersion.ts +67 -0
  121. package/src/workflow/versioning/deriveReleaseType.ts +33 -0
  122. package/src/workflow/versioning/determineReleaseType.ts +35 -0
  123. package/src/workflow/versioning/index.ts +8 -0
  124. package/src/workflow/versioning/isStableVersion.ts +31 -0
  125. package/src/workflow/versioning/pickPriorityReleaseType.ts +27 -0
@@ -0,0 +1,2342 @@
1
+ import { colors } from "consola/utils";
2
+ import semver from "semver";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import prettier from "prettier";
6
+ import { minimatch } from "minimatch";
7
+ import fs, { readFile } from "node:fs/promises";
8
+ import { CommitParser } from "conventional-commits-parser";
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ import GitHost from "hosted-git-info";
11
+ import { fromMarkdown } from "mdast-util-from-markdown";
12
+ import assert from "node:assert";
13
+ import createPreset from "conventional-changelog-conventionalcommits";
14
+ import { writeChangelogString } from "conventional-changelog-writer";
15
+ import { toMarkdown } from "mdast-util-to-markdown";
16
+ //#region src/workflow/versioning/bumpVersion.ts
17
+ /**
18
+ * Bump a semantic version according to release type and prerelease settings.
19
+ *
20
+ * Increments the version using the provided release type. When a prerelease ID
21
+ * is provided, generates a prerelease version with that identifier. Throws an
22
+ * error when the version is invalid or the initial version is missing.
23
+ *
24
+ * @param pkg Package object containing current version
25
+ * @param releaseType Semantic version component to increment (`'major'`, `'minor'`, `'patch'`)
26
+ * @param prereleaseId Optional prerelease identifier (e.g., `'alpha'`, `'beta'`)
27
+ * @returns Next semantic version string, or `null` when `releaseType` is falsy
28
+ * @throws If the version is invalid or initial version is missing
29
+ */
30
+ function bumpVersion(pkg, releaseType, prereleaseId) {
31
+ if (!releaseType) return null;
32
+ if (!pkg.version) {
33
+ if (pkg.private) return null;
34
+ throw new Error("An initial version is required to compute the next version.");
35
+ }
36
+ const parsed = semver.parse(pkg.version);
37
+ if (!parsed) throw new Error(`The version "${colors.bold(pkg.version)}" is not a valid SemVer.`);
38
+ prereleaseId ||= void 0;
39
+ let type = releaseType;
40
+ if (prereleaseId) {
41
+ type = `pre${releaseType}`;
42
+ if (parsed.prerelease.length > 0) {
43
+ if (releaseType === "major" && parsed.minor === 0 && parsed.patch === 0 || releaseType === "minor" && parsed.patch === 0 || releaseType === "patch") type = "prerelease";
44
+ }
45
+ }
46
+ return semver.inc(parsed, type, { loose: void 0 }, prereleaseId, "0");
47
+ }
48
+ //#endregion
49
+ //#region src/workflow/versioning/determineReleaseType.ts
50
+ /**
51
+ * Determine the release type for a single commit.
52
+ *
53
+ * Automatically returns `'major'` if the commit is marked as a breaking change.
54
+ * Otherwise, routes the commit through the custom `classifier` and ensures the
55
+ * resolved value is a valid semantic versioning release type.
56
+ *
57
+ * @param classifier Function mapping commits to release types
58
+ * @param commit Parsed commit to evaluate
59
+ * @returns `ReleaseType` string, or `null` when no matching rule applies
60
+ */
61
+ function determineReleaseType(classifier, commit) {
62
+ if (commit.notes.length > 0) return "major";
63
+ const type = classifier(commit);
64
+ switch (type) {
65
+ case "major":
66
+ case "minor":
67
+ case "patch": return type;
68
+ }
69
+ return null;
70
+ }
71
+ //#endregion
72
+ //#region src/workflow/versioning/pickPriorityReleaseType.ts
73
+ /**
74
+ * Determine the highest-priority release type between two candidates.
75
+ *
76
+ * Compares release types by precedence order: `major` > `minor` > `patch`.
77
+ * Returns the higher-precedence type, or `null` if neither applies.
78
+ *
79
+ * @param curr Current accumulated release type
80
+ * @param next Candidate release type to consider
81
+ * @returns Highest-precedence `ReleaseType`, or `null` if neither applies
82
+ */
83
+ function pickPriorityReleaseType(curr, next) {
84
+ for (const type of [
85
+ "major",
86
+ "minor",
87
+ "patch"
88
+ ]) if (curr === type || next === type) return type;
89
+ return null;
90
+ }
91
+ //#endregion
92
+ //#region src/workflow/versioning/deriveReleaseType.ts
93
+ /**
94
+ * Derive the combined release type from a commit history collection.
95
+ *
96
+ * Processes commits sequentially, accumulating the highest-precedence release
97
+ * type found by evaluating each individual commit and comparing it against the
98
+ * current maximum via `pickPriorityReleaseType`.
99
+ *
100
+ * @param classifier Function mapping commits to release types
101
+ * @param history Array of parsed commits to evaluate
102
+ * @returns `ReleaseType` string, or `null` when no matching rule applies
103
+ */
104
+ function deriveReleaseType(classifier, history) {
105
+ let releaseType = null;
106
+ for (const commit of history) releaseType = pickPriorityReleaseType(releaseType, determineReleaseType(classifier, commit));
107
+ return releaseType;
108
+ }
109
+ //#endregion
110
+ //#region src/workflow/versioning/isStableVersion.ts
111
+ /**
112
+ * Determine whether a version string represents a stable SemVer release.
113
+ *
114
+ * A version is considered stable when:
115
+ * - It is a valid SemVer string.
116
+ * - Its major version is greater than 0 (not 0.x.x).
117
+ * - It contains no prerelease identifiers (no `-alpha`, `-beta`, `-rc`, etc.).
118
+ *
119
+ * Build metadata (the `+` suffix) does not affect stability.
120
+ *
121
+ * @param version Semantic version string to check
122
+ * @returns `true` if `version` is a stable SemVer; otherwise `false`
123
+ * @throws If `version` is not a valid SemVer string
124
+ */
125
+ function isStableVersion(version) {
126
+ const parsed = semver.parse(version);
127
+ if (!parsed) throw new Error(`The version "${colors.bold(version)}" is not a valid SemVer.`);
128
+ return parsed.major > 0 && parsed.prerelease.length === 0;
129
+ }
130
+ //#endregion
131
+ //#region src/config/const.ts
132
+ var ALL_FILES = "**/*";
133
+ var DEFAULT_CHANGELOG_FILE = "CHANGELOG.md";
134
+ var GIT_TRAILER_RELEASE_AGENT = "Release-Agent";
135
+ /**
136
+ * 1. Primitive Building Blocks
137
+ *
138
+ * Base regular expression patterns for wildcards, alphanumeric strings with hyphens,
139
+ * and standard non-negative integers.
140
+ */
141
+ var WILDCARD = "\\*";
142
+ var ALPHANUM_DASH = "[0-9a-zA-Z-]+";
143
+ var NUMERIC_IDENTIFIER = "0|[1-9]\\d*";
144
+ /**
145
+ * 2. Package Name & Prefix Constructs
146
+ *
147
+ * Matches package name prefixes (unscoped or scoped) or a simple `v` prefix.
148
+ *
149
+ * Capture Groups:
150
+ * - Group 1: Package name (e.g., `my-package` or `@scope/my-package`)
151
+ */
152
+ var PKG_NAME_PART = `(?:${WILDCARD}|${ALPHANUM_DASH})`;
153
+ var OPTIONAL_VERSION_PREFIX = `(?:v|${`(${PKG_NAME_PART}|@${PKG_NAME_PART}\\/${PKG_NAME_PART})`}@)`;
154
+ /**
155
+ * 3. Core SemVer Components
156
+ *
157
+ * Matches standard `MAJOR.MINOR.PATCH` versions or wildcard variants (e.g., `1.0.0`, `*.*.*`).
158
+ *
159
+ * Capture Groups:
160
+ * - Group 3: Major version
161
+ * - Group 4: Minor version
162
+ * - Group 5: Patch version
163
+ */
164
+ var CAPTURED_VERSION_DIGIT = `(${WILDCARD}|${NUMERIC_IDENTIFIER})`;
165
+ var SEMVER_CORE = `${CAPTURED_VERSION_DIGIT}\\.${CAPTURED_VERSION_DIGIT}\\.${CAPTURED_VERSION_DIGIT}`;
166
+ var PRERELEASE_IDENTIFIER = `(?:${WILDCARD}|${NUMERIC_IDENTIFIER}|(\\d*[a-zA-Z-][0-9a-zA-Z-]*))`;
167
+ var FULL_SEMVER_PATTERN = `${OPTIONAL_VERSION_PREFIX}?${`(${SEMVER_CORE}${`(?:-(${PRERELEASE_IDENTIFIER}(?:\\.${PRERELEASE_IDENTIFIER})*))`}?${`(?:\\+(${ALPHANUM_DASH}(?:\\.${ALPHANUM_DASH})*))`}?)`}`;
168
+ /**
169
+ * Regular expression to match and capture Semantic Versioning (SemVer) strings,
170
+ * including optional package prefixes (e.g., `@scope/pkg@`) and `v` prefixes.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * SEMVER_REGEX.test('1.0.0') // true
175
+ * SEMVER_REGEX.test('v1.2.3-beta.1+sha.1234') // true
176
+ * SEMVER_REGEX.test('@scope/package@1.0.0') // true
177
+ * ```
178
+ *
179
+ * @see {@link https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string SemVer Specification}
180
+ */
181
+ var SEMVER_REGEX = new RegExp(FULL_SEMVER_PATTERN);
182
+ var RELEASE_LIFECYCLE_HOOK_LIST = [
183
+ "configure",
184
+ "discover",
185
+ "analyze",
186
+ "build",
187
+ "write",
188
+ "commit",
189
+ "publish",
190
+ "finalize"
191
+ ];
192
+ //#endregion
193
+ //#region src/config/isSemVerGitTag.ts
194
+ /**
195
+ * Determine whether a tag string is a valid SemVer-style Git tag.
196
+ *
197
+ * Anchors `SEMVER_REGEX.source` for full-string matching and applies the
198
+ * repository's Git-tag acceptance rules.
199
+ *
200
+ * @param tag Tag string to validate
201
+ * @returns `true` if `tag` is accepted as a SemVer-style Git tag; otherwise `false`
202
+ */
203
+ function isSemVerGitTag(tag) {
204
+ const match = new RegExp(`^${SEMVER_REGEX.source}$`).exec(tag);
205
+ if (!match) return false;
206
+ const isScopedPackage = Boolean(match[1]);
207
+ const hasVersionPrefix = tag.startsWith("v");
208
+ return isScopedPackage || hasVersionPrefix;
209
+ }
210
+ //#endregion
211
+ //#region src/util/buildTrailerPattern.ts
212
+ /**
213
+ * Build a regular expression to match trailer lines using custom markers.
214
+ *
215
+ * Combines the provided marker strings with a standard alphanumeric pattern
216
+ * to construct a regular expression that matches trailer definitions.
217
+ *
218
+ * @param markers Optional array of custom trailer marker strings
219
+ * @returns A regular expression matching trailer lines
220
+ */
221
+ function buildTrailerPattern(markers = []) {
222
+ return new RegExp(`^(${markers.map(RegExp.escape).concat("[A-Za-z0-9_-]+").join("|")})[ \\t]*:[ \\t]*(.*)$`);
223
+ }
224
+ //#endregion
225
+ //#region src/util/isDefined.ts
226
+ /**
227
+ * Type guard that asserts a value is neither `null` nor `undefined`.
228
+ *
229
+ * @param value Value to test
230
+ * @returns `true` when `value` is not `null` or `undefined`
231
+ */
232
+ function isDefined(value) {
233
+ return value != null;
234
+ }
235
+ //#endregion
236
+ //#region src/util/compact.ts
237
+ /**
238
+ * Remove `null` and `undefined` values from an array.
239
+ *
240
+ * @param array The array to compact
241
+ * @returns A new array that does not contain `null` or `undefined` values
242
+ */
243
+ function compact(array) {
244
+ return array.filter(isDefined);
245
+ }
246
+ //#endregion
247
+ //#region src/util/execCommand.ts
248
+ (/* @__PURE__ */ new Date("2026-01-01T00:00:00Z")).valueOf();
249
+ /**
250
+ * Execute a command asynchronously and return its standard output.
251
+ *
252
+ * @param cwd Working directory for the command
253
+ * @param command Binary to execute
254
+ * @param args Command-line arguments
255
+ * @returns Promise resolving to the stdout string
256
+ */
257
+ function execCommand(cwd, command, args) {
258
+ const env = { ...process.env };
259
+ let childProcess;
260
+ const promise = new Promise((resolve, reject) => {
261
+ childProcess = spawn(command, args, {
262
+ cwd,
263
+ env
264
+ });
265
+ childProcess.stdout.setEncoding("utf-8");
266
+ childProcess.stderr.setEncoding("utf-8");
267
+ let stdout = "";
268
+ let stderr = "";
269
+ childProcess.stdout.on("data", (data) => {
270
+ stdout += data;
271
+ });
272
+ childProcess.stderr.on("data", (data) => {
273
+ stderr += data;
274
+ });
275
+ childProcess.on("error", (err) => {
276
+ reject(err);
277
+ });
278
+ childProcess.on("close", (code, signal) => {
279
+ if (code === 0) return resolve(stdout);
280
+ const errorMessage = `Command ${quoteCommandForMessage([command, ...args].join(" "))} failed (${code !== null ? `exit code ${code}` : signal ? `signal ${signal}` : "unknown status"})`;
281
+ const errorDiagnostics = (stderr || stdout).trim();
282
+ const err = new Error([errorMessage, errorDiagnostics].filter(Boolean).join(errorDiagnostics.includes("\n") ? "\n" : ": "), { cause: {
283
+ code,
284
+ signal,
285
+ diagnostics: errorDiagnostics
286
+ } });
287
+ Object.defineProperty(err, "code", { value: code });
288
+ reject(err);
289
+ });
290
+ });
291
+ promise.child = childProcess;
292
+ return promise;
293
+ }
294
+ function quoteCommandForMessage(command) {
295
+ if (!command.includes("'")) return `'${command}'`;
296
+ if (!command.includes("\"")) return `"${command}"`;
297
+ return JSON.stringify(command);
298
+ }
299
+ //#endregion
300
+ //#region src/util/execGit.ts
301
+ /**
302
+ * Run a git command in a working directory and return its standard output.
303
+ *
304
+ * @param cwd Working directory for the git command
305
+ * @param args Git CLI arguments
306
+ * @returns Promise resolving to the stdout string
307
+ */
308
+ function execGit(cwd, args) {
309
+ return execCommand(cwd, "git", args);
310
+ }
311
+ //#endregion
312
+ //#region src/util/formatFile.ts
313
+ /**
314
+ * Format `content` for `filePath` using Prettier when possible.
315
+ *
316
+ * Resolves project/editorconfig settings and formats the provided content.
317
+ * Returns the original content on error or when no parser is inferred.
318
+ *
319
+ * @param filePath Path used to infer parser and config
320
+ * @param content Source content to format
321
+ * @returns Formatted content or original content on failure
322
+ */
323
+ async function formatFile(filePath, content) {
324
+ try {
325
+ const info = await prettier.getFileInfo(filePath, { resolveConfig: true });
326
+ const config = await prettier.resolveConfig(filePath, { editorconfig: true });
327
+ if (info.inferredParser == null) return content;
328
+ return await prettier.format(content, {
329
+ ...config,
330
+ parser: info.inferredParser,
331
+ filepath: filePath
332
+ });
333
+ } catch {
334
+ return content;
335
+ }
336
+ }
337
+ //#endregion
338
+ //#region src/util/interpolate.ts
339
+ var BACKSLASH = "\\";
340
+ var LEFT_BRACE = "{";
341
+ var RIGHT_BRACE = "}";
342
+ var DOT = ".";
343
+ var ESCAPE_CHARS = [
344
+ BACKSLASH,
345
+ LEFT_BRACE,
346
+ RIGHT_BRACE
347
+ ];
348
+ var BANNED_KEYS = [
349
+ "__proto__",
350
+ "constructor",
351
+ "prototype"
352
+ ];
353
+ /**
354
+ * Interpolate placeholders in `template` using values from `data`.
355
+ *
356
+ * - Placeholders use `{path.to.value}` (dot notation).
357
+ * - Backslash escapes are supported for `{`, `}`, `\`, and `.` inside tokens.
358
+ * - Tokens that reference banned keys (`__proto__`, `constructor`, `prototype`)
359
+ * are left unexpanded to avoid prototype pollution.
360
+ * - Only own properties are followed when resolving paths; if any step is
361
+ * missing, the original token text is preserved.
362
+ * - Resolved non-object, non-null values are stringified and inserted.
363
+ * - Unclosed tokens and a trailing backslash are preserved verbatim.
364
+ *
365
+ * @param template Template string containing placeholders like `{user.name}`
366
+ * @param data Object used to resolve placeholder paths
367
+ * @returns The template with placeholders replaced by corresponding values
368
+ */
369
+ function interpolate(template, data) {
370
+ let result = "";
371
+ let i = 0;
372
+ let char;
373
+ let nextChar;
374
+ while (char = template[i]) if (char === BACKSLASH) {
375
+ nextChar = template[i + 1];
376
+ if (nextChar !== void 0 && ESCAPE_CHARS.includes(nextChar)) {
377
+ result += nextChar;
378
+ i += 2;
379
+ } else {
380
+ result += char;
381
+ i += 1;
382
+ }
383
+ } else if (char === LEFT_BRACE) {
384
+ const keys = [];
385
+ let key = "";
386
+ let token = "";
387
+ let closed = false;
388
+ i += 1;
389
+ while (char = template[i]) {
390
+ if (char === RIGHT_BRACE) {
391
+ closed = true;
392
+ keys.push(key);
393
+ i += 1;
394
+ break;
395
+ }
396
+ if (char === BACKSLASH) {
397
+ nextChar = template[i + 1];
398
+ if (nextChar !== void 0 && [...ESCAPE_CHARS, DOT].includes(nextChar)) {
399
+ key += nextChar;
400
+ token += nextChar;
401
+ i += 2;
402
+ } else {
403
+ key += char;
404
+ token += char;
405
+ i += 1;
406
+ }
407
+ } else if (char === DOT) {
408
+ keys.push(key);
409
+ key = "";
410
+ token += char;
411
+ i += 1;
412
+ } else {
413
+ key += char;
414
+ token += char;
415
+ i += 1;
416
+ }
417
+ }
418
+ if (closed) {
419
+ let value = data;
420
+ for (const key of keys) {
421
+ const isSafeKey = !BANNED_KEYS.includes(key);
422
+ if (value !== null && typeof value === "object" && Object.hasOwn(value, key) && isSafeKey) value = value[key];
423
+ else {
424
+ value = void 0;
425
+ break;
426
+ }
427
+ }
428
+ if (value == null || typeof value === "object") result += LEFT_BRACE + token + RIGHT_BRACE;
429
+ else result += String(value);
430
+ } else result += LEFT_BRACE + token;
431
+ } else {
432
+ result += char;
433
+ i += 1;
434
+ }
435
+ return result;
436
+ }
437
+ //#endregion
438
+ //#region src/util/isMatch.ts
439
+ /**
440
+ * Determine whether a target string matches glob patterns.
441
+ *
442
+ * Matches positive and negative glob patterns using dotfile support. Automatically
443
+ * expands a single negative pattern by prepending a global wildcard match.
444
+ *
445
+ * @param target Target string to evaluate against glob patterns
446
+ * @param patterns Single glob pattern or list of patterns to evaluate
447
+ * @returns True if target matches positive patterns and satisfies negative patterns
448
+ */
449
+ function isMatch(target, patterns) {
450
+ if (typeof patterns === "string") if (patterns.startsWith("!")) patterns = ["**/*", patterns];
451
+ else patterns = [patterns];
452
+ const positives = [];
453
+ const negatives = [];
454
+ for (const pattern of patterns) if (pattern.startsWith("!")) negatives.push(pattern);
455
+ else positives.push(pattern);
456
+ return positives.some((pattern) => minimatch(target, pattern, { dot: true })) && negatives.every((pattern) => minimatch(target, pattern, { dot: true }));
457
+ }
458
+ //#endregion
459
+ //#region src/util/readJson.ts
460
+ /**
461
+ * Read and parse a JSON file.
462
+ *
463
+ * Throws a descriptive error when the file cannot be read or parsed, or when
464
+ * the parsed value is not an object.
465
+ *
466
+ * @param filePath Path to the JSON file
467
+ * @returns Parsed JSON object
468
+ * @throws If the file cannot be read, parsed, or is not an object
469
+ */
470
+ async function readJson(filePath) {
471
+ let content;
472
+ try {
473
+ content = await fs.readFile(filePath, "utf-8");
474
+ } catch (err) {
475
+ throw new Error(`JSON file at "${colors.bold(filePath)}" could not be read.`, { cause: err });
476
+ }
477
+ let json;
478
+ try {
479
+ json = JSON.parse(content);
480
+ } catch (err) {
481
+ throw new Error(`JSON file at "${colors.bold(filePath)}" could not be parsed.`, { cause: err });
482
+ }
483
+ if (typeof json !== "object" || json === null) throw new Error(`JSON file at "${colors.bold(filePath)}" does not contain a valid object.`);
484
+ return json;
485
+ }
486
+ //#endregion
487
+ //#region src/util/writeJson.ts
488
+ /**
489
+ * Write a JSON `payload` to `filePath`, formatting the content first.
490
+ *
491
+ * Ensures the directory exists and writes the formatted JSON.
492
+ *
493
+ * @param filePath Path to write the JSON file
494
+ * @param payload JSON-serializable object to write
495
+ * @returns Promise that resolves when the file is written
496
+ */
497
+ async function writeJson(filePath, payload) {
498
+ let content = JSON.stringify(payload, null, 2);
499
+ content = await formatFile(filePath, content);
500
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
501
+ await fs.writeFile(filePath, content);
502
+ }
503
+ //#endregion
504
+ //#region src/config/sanitizePrereleaseLabel.ts
505
+ /**
506
+ * Sanitize a string into a safe, semver-compliant prerelease label.
507
+ *
508
+ * Replaces sequential non-alphanumeric characters or consecutive hyphens
509
+ * with a single hyphen, and strips leading or trailing hyphens.
510
+ *
511
+ * @param label Raw string to sanitize
512
+ * @returns Safe string for a prerelease label
513
+ */
514
+ function sanitizePrereleaseLabel(label) {
515
+ return label.replace(/[^a-zA-Z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
516
+ }
517
+ //#endregion
518
+ //#region src/config/resolveReleaseTarget.ts
519
+ /**
520
+ * Resolve prerelease label and release channel for a ref.
521
+ *
522
+ * Resolution priority for each key:
523
+ * 1. Explicit `options` (string values are used verbatim; `false` disables the key).
524
+ * 2. First matching `rule` (rules are checked in order).
525
+ * 3. Fallback: derive and sanitize a label from `ref.name`.
526
+ *
527
+ * Resolved string values are interpolated with `data` before being returned.
528
+ *
529
+ * @param ref Reference context containing `type` and `name`
530
+ * @param rules Ordered list of release rules to match against
531
+ * @param options User overrides for `prereleaseLabel` and `releaseChannel`
532
+ * @param data Values used to interpolate resolved template strings
533
+ * @returns Partial release rule with resolved `prereleaseLabel` and/or `releaseChannel`
534
+ */
535
+ function resolveReleaseTarget(ref, rules, options, data) {
536
+ const matchedRule = rules.find((rule) => rule.type === ref.type && isMatch(ref.name, rule.pattern));
537
+ const resolve = (key) => {
538
+ const value1 = options[key];
539
+ if (value1 === false) return;
540
+ if (value1 && typeof value1 === "string") return value1;
541
+ const value2 = matchedRule?.[key] || value1;
542
+ if (!value2) return;
543
+ if (typeof value2 === "string") return value2;
544
+ return sanitizePrereleaseLabel(ref.name);
545
+ };
546
+ const prereleaseLabel = resolve("prereleaseLabel");
547
+ const releaseChannel = resolve("releaseChannel");
548
+ return {
549
+ prereleaseLabel: prereleaseLabel && interpolate(prereleaseLabel, data),
550
+ releaseChannel: releaseChannel && interpolate(releaseChannel, data)
551
+ };
552
+ }
553
+ //#endregion
554
+ //#region src/workflow/analysis/isCommitForPackage.ts
555
+ /**
556
+ * Test whether a commit modifies files in a package.
557
+ *
558
+ * @param pkg Package with an explicit file pattern or base directory
559
+ * @param commit Parsed commit containing changed file paths
560
+ * @returns `true` if any changed file matches the package pattern
561
+ */
562
+ function isCommitForPackage(pkg, commit) {
563
+ return commit.files.some((file) => {
564
+ const { dir } = pkg;
565
+ return isMatch(file, pkg.files || path.join(dir, "**/*"));
566
+ });
567
+ }
568
+ //#endregion
569
+ //#region src/workflow/analysis/analyzePackageChanges.ts
570
+ /**
571
+ * Analyze commit history and compute release types for each package plan.
572
+ *
573
+ * Iterates through commits, associates them with affected packages, and
574
+ * calculates each package's release type using the provided classifier.
575
+ *
576
+ * @param classifier Function mapping commits to release types
577
+ * @param plans Array of release plans to update
578
+ * @param history Commit history as iterable or async iterable
579
+ * @returns Updated array of plans with computed `releaseType`
580
+ */
581
+ async function analyzePackageChanges(classifier, plans, history) {
582
+ for await (const commit of history) plans = plans.map((plan) => {
583
+ if (!isCommitForPackage(plan.package, commit)) return plan;
584
+ return {
585
+ ...plan,
586
+ history: plan.history.concat(commit)
587
+ };
588
+ });
589
+ return plans.map((plan) => {
590
+ let { release } = plan;
591
+ const releaseType = pickPriorityReleaseType(release?.type, deriveReleaseType(classifier, plan.history));
592
+ if (releaseType) release = {
593
+ ...release,
594
+ type: releaseType
595
+ };
596
+ return {
597
+ ...plan,
598
+ release
599
+ };
600
+ });
601
+ }
602
+ //#endregion
603
+ //#region src/workflow/analysis/promotePrereleaseVersions.ts
604
+ /**
605
+ * Promote prerelease versions when they have no explicit release type.
606
+ *
607
+ * Analyzes prerelease and zero-major versions to determine an appropriate
608
+ * release type for graduation:
609
+ * - Prerelease versions are promoted based on their highest incremented component
610
+ * (patch, minor, or major)
611
+ * - Zero-major versions (0.y.z) are promoted to major
612
+ * - Stable versions (1.0.0+) remain unchanged
613
+ * - Plans with explicit releases are preserved as-is
614
+ *
615
+ * @param plans Release plans to analyze
616
+ * @returns Plans with inferred release types for prerelease versions
617
+ */
618
+ async function promotePrereleaseVersions(plans) {
619
+ return plans.map((plan) => {
620
+ if (plan.release) return plan;
621
+ if (!plan.package.tag) return plan;
622
+ const parsed = semver.parse(plan.package.version);
623
+ if (!parsed) return plan;
624
+ let type;
625
+ if (parsed.prerelease.length) if (parsed.patch !== 0) type = "patch";
626
+ else if (parsed.minor !== 0) type = "minor";
627
+ else if (parsed.major !== 0) type = "major";
628
+ else type = "major";
629
+ else if (parsed.major === 0) type = "major";
630
+ if (type) return {
631
+ ...plan,
632
+ release: { type }
633
+ };
634
+ return plan;
635
+ });
636
+ }
637
+ //#endregion
638
+ //#region src/git/branch/getCurrentGitBranch.ts
639
+ /**
640
+ * Get the current Git branch name.
641
+ *
642
+ * Uses `execGit` to run `git symbolic-ref -q --short HEAD` in the provided
643
+ * working directory and trims the result. Returns an empty string when the
644
+ * repository is in a detached HEAD state.
645
+ *
646
+ * @param cwd The working directory to run the Git command in
647
+ * @returns The current branch name as a string
648
+ */
649
+ async function getCurrentGitBranch(cwd) {
650
+ return (await execGit(cwd, [
651
+ "symbolic-ref",
652
+ "-q",
653
+ "--short",
654
+ "HEAD"
655
+ ])).trim();
656
+ }
657
+ //#endregion
658
+ //#region src/git/commit/createGitCommit.ts
659
+ /**
660
+ * Stage all changes and create a git commit with the specified message.
661
+ *
662
+ * Validates the commit message and working directory status before staging
663
+ * all changes and executing the commit command.
664
+ *
665
+ * @param cwd The target directory path for executing git commands
666
+ * @param message The message to associate with the commit
667
+ * @returns A promise that resolves when the operation completes
668
+ */
669
+ async function createGitCommit(cwd, message) {
670
+ if (!message) return;
671
+ await execGit(cwd, ["add", "."]);
672
+ if (!(await execGit(cwd, ["status", "--porcelain"])).trim()) return;
673
+ await execGit(cwd, [
674
+ "commit",
675
+ "--message",
676
+ message
677
+ ]);
678
+ }
679
+ //#endregion
680
+ //#region src/git/commit/extractMentions.ts
681
+ var ALPHANUMERIC = "[a-zA-Z0-9]";
682
+ var SEPARATOR = "[-_]";
683
+ var USERNAME = `${ALPHANUMERIC}+(?:${SEPARATOR}+${ALPHANUMERIC}+)*`;
684
+ var EMAIL_LOCAL = "[a-zA-Z0-9._%+-]";
685
+ var DOMAIN_SUFFIX = "[a-zA-Z0-9.-]*\\.[a-zA-Z]{2,}";
686
+ var SCOPED_PACKAGE_SPEC_SUFFIX = `/${USERNAME}@[0-9]+\\.[0-9]+\\.[0-9]+`;
687
+ /**
688
+ * Extract unique mention handles from the input text string.
689
+ *
690
+ * Evaluates `text` against boundary rules to isolate valid handles while
691
+ * excluding email addresses, domain names, and scoped package specs, then
692
+ * de-duplicates the resulting handles.
693
+ *
694
+ * @param text The input string to parse for mentions
695
+ * @returns An array of unique mention handles
696
+ */
697
+ function extractMentions(text) {
698
+ if (!text) return [];
699
+ const regex = new RegExp(`(?<!${EMAIL_LOCAL})@(${USERNAME})\\b(?!${SEPARATOR}|${DOMAIN_SUFFIX}|${SCOPED_PACKAGE_SPEC_SUFFIX})`, "g");
700
+ const matches = text.matchAll(regex);
701
+ const mentions = new Set(matches.map((match) => match[1]));
702
+ return Array.from(mentions);
703
+ }
704
+ //#endregion
705
+ //#region src/git/revision/resolveGitRevision.ts
706
+ /**
707
+ * Resolve a Git revision or commit-ish expression to its full commit hash.
708
+ *
709
+ * Runs `git rev-parse --verify <revision>^{commit}` under the hood. Peels annotated
710
+ * tags, branches, short SHAs, and relative expressions down to a verified commit object.
711
+ *
712
+ * @param cwd The repository working directory
713
+ * @param revision The Git revision or expression to resolve
714
+ * @returns A promise that resolves to the full, trimmed commit SHA-1 string
715
+ */
716
+ async function resolveGitRevision(cwd, revision) {
717
+ return (await execGit(cwd, [
718
+ "rev-parse",
719
+ "--verify",
720
+ `${revision}^{commit}`
721
+ ])).trim();
722
+ }
723
+ //#endregion
724
+ //#region src/git/commit/getCurrentGitCommit.ts
725
+ /**
726
+ * Resolve the current commit hash for HEAD in the repository.
727
+ *
728
+ * @param cwd The repository working directory
729
+ * @returns A promise that resolves to the full, trimmed commit SHA-1 string
730
+ */
731
+ function getCurrentGitCommit(cwd) {
732
+ return resolveGitRevision(cwd, "HEAD");
733
+ }
734
+ //#endregion
735
+ //#region src/cache/TTLCache.ts
736
+ /**
737
+ * A simple time-to-live cache that automatically evicts entries after a TTL.
738
+ *
739
+ * - Entries are removed automatically when their TTL expires.
740
+ * - Reading an entry resets its TTL to the original value.
741
+ * - Setting an existing key clears its previous timer and resets the TTL.
742
+ *
743
+ * @template K Type of cache keys
744
+ * @template V Type of cache values
745
+ */
746
+ var TTLCache = class {
747
+ constructor() {
748
+ this.cache = /* @__PURE__ */ new Map();
749
+ }
750
+ /**
751
+ * Store a value under `key` for `ttl` milliseconds.
752
+ *
753
+ * If the key already exists its previous timer is cleared and the TTL is reset.
754
+ *
755
+ * @param key Cache key
756
+ * @param value Value to store
757
+ * @param ttl Time to live in milliseconds (default: 60_000)
758
+ */
759
+ set(key, value, ttl = 6e4) {
760
+ const ref = this.cache.get(key);
761
+ if (ref) clearTimeout(ref.timer);
762
+ const timer = setTimeout(() => {
763
+ this.cache.delete(key);
764
+ }, ttl).unref();
765
+ this.cache.set(key, {
766
+ value,
767
+ timer,
768
+ ttl
769
+ });
770
+ }
771
+ /**
772
+ * Retrieve the value for `key`, or `undefined` if not present.
773
+ *
774
+ * Reading an entry resets its TTL to the original `ttl` provided when it was set.
775
+ *
776
+ * @param key Cache key
777
+ * @returns The stored value or `undefined` if not present
778
+ */
779
+ get(key) {
780
+ const ref = this.cache.get(key);
781
+ if (!ref) return void 0;
782
+ this.set(key, ref.value, ref.ttl);
783
+ return ref.value;
784
+ }
785
+ /**
786
+ * Check whether `key` exists in the cache.
787
+ *
788
+ * @param key Cache key
789
+ * @returns `true` if present; otherwise `false`
790
+ */
791
+ has(key) {
792
+ return this.cache.has(key);
793
+ }
794
+ /**
795
+ * Remove `key` from the cache and clear its eviction timer.
796
+ *
797
+ * @param key Cache key
798
+ * @returns `true` if the key was present and removed; otherwise `false`
799
+ */
800
+ delete(key) {
801
+ const ref = this.cache.get(key);
802
+ if (!ref) return false;
803
+ clearTimeout(ref.timer);
804
+ return this.cache.delete(key);
805
+ }
806
+ /**
807
+ * Clear all entries and their timers.
808
+ */
809
+ clear() {
810
+ for (const { timer } of this.cache.values()) clearTimeout(timer);
811
+ this.cache.clear();
812
+ }
813
+ };
814
+ //#endregion
815
+ //#region src/git/commit/parseCommit.ts
816
+ var BREAKING_HEADER_REGEX = /^([\w-]+)(?:\(([^)]+)\))?!: (.*)$/;
817
+ var BREAKING_MARKERS = ["BREAKING CHANGE", "BREAKING-CHANGE"];
818
+ /**
819
+ * Parse a git log entry into a structured commit object.
820
+ *
821
+ * Combines the commit subject and body, extracts conventional commit metadata
822
+ * using custom trailer markers and breaking header patterns, and normalizes
823
+ * dates to ISO format strings.
824
+ *
825
+ * @param entry The raw git log entry to parse
826
+ * @param markers Optional array of custom trailer marker strings
827
+ * @returns A structured commit object containing parsed details, notes, and mentions
828
+ */
829
+ async function parseCommit(entry, markers = []) {
830
+ const message = [entry.subject, entry.body].filter(Boolean).join("\n\n");
831
+ const commit = new CommitParser({
832
+ breakingHeaderPattern: BREAKING_HEADER_REGEX,
833
+ noteKeywords: [],
834
+ notesPattern: () => buildTrailerPattern(BREAKING_MARKERS.concat(markers))
835
+ }).parse(message);
836
+ const notes = [];
837
+ const trailers = [];
838
+ for (const note of commit.notes) {
839
+ if (BREAKING_MARKERS.includes(note.title.toUpperCase())) notes.push(note);
840
+ trailers.push({
841
+ key: note.title,
842
+ value: note.text
843
+ });
844
+ }
845
+ if (notes.length === 0 && BREAKING_HEADER_REGEX.test(commit.header)) {
846
+ const note = {
847
+ title: "BREAKING CHANGE",
848
+ text: commit.subject
849
+ };
850
+ notes.push(note);
851
+ trailers.push({
852
+ key: note.title,
853
+ value: note.text
854
+ });
855
+ }
856
+ const mentions = extractMentions(message);
857
+ return {
858
+ ...commit,
859
+ hash: entry.hash,
860
+ parent: entry.parent,
861
+ authorDate: new Date(entry.authorDate).toISOString(),
862
+ committerDate: new Date(entry.committerDate).toISOString(),
863
+ type: commit.type?.toLowerCase() || null,
864
+ scope: commit.scope || null,
865
+ subject: commit.subject || null,
866
+ header: commit.header,
867
+ body: commit.body,
868
+ footer: commit.footer,
869
+ notes,
870
+ mentions,
871
+ trailers,
872
+ files: entry.files
873
+ };
874
+ }
875
+ //#endregion
876
+ //#region src/git/commit/parseGitPrettyFormat.ts
877
+ /**
878
+ * Field separator used in the git pretty format to produce a single-line
879
+ *, machine-parseable record.
880
+ */
881
+ var GIT_PRETTY_FIELD_SEPARATOR = "";
882
+ /**
883
+ * Pretty format string passed to `git log --pretty=format:`. Produces seven
884
+ * fields separated by `GIT_PRETTY_FIELD_SEPARATOR`:
885
+ * hash, parents, author date, committer date, subject, body, files.
886
+ */
887
+ var GIT_PRETTY_FORMAT = [
888
+ "%H",
889
+ "%P",
890
+ "%aI",
891
+ "%cI",
892
+ "%s",
893
+ "%b",
894
+ ""
895
+ ].join("");
896
+ /**
897
+ * Parse a single line produced by `git log --pretty=format:${GIT_PRETTY_FORMAT}`.
898
+ *
899
+ * The input is expected to contain seven fields separated by
900
+ * `GIT_PRETTY_FIELD_SEPARATOR`:
901
+ * 1. `hash` (commit hash)
902
+ * 2. `parent` (space-separated parent hashes)
903
+ * 3. `authorDate` (ISO 8601)
904
+ * 4. `committerDate` (ISO 8601)
905
+ * 5. `subject`
906
+ * 6. `body`
907
+ * 7. `files` (newline-separated list)
908
+ *
909
+ * The function trims fields, converts parent hashes into an array, normalizes
910
+ * dates to ISO strings, and splits `files` into an array. Missing optional
911
+ * fields are normalized to sensible defaults.
912
+ *
913
+ * @param raw Raw pretty-format string produced by git
914
+ * @returns Parsed `GitLogEntry` with `hash`, `parent`, `authorDate`, `committerDate`, `subject`, `body`, and `files`
915
+ * @throws If required fields (hash, authorDate, committerDate, subject) are missing or malformed
916
+ */
917
+ function parseGitPrettyFormat(raw) {
918
+ let [hash, parentHashes, authorDate, committerDate, subject, body, files] = raw.split("");
919
+ hash = hash?.trim();
920
+ parentHashes = parentHashes?.trim();
921
+ authorDate = authorDate?.trim();
922
+ committerDate = committerDate?.trim();
923
+ subject = subject?.trim();
924
+ body = body?.trim();
925
+ files = files?.trim();
926
+ if (!hash || !authorDate || !committerDate || typeof subject !== "string") throw new Error(`The input "${colors.bold(raw)}" is not in the expected pretty format.`);
927
+ return {
928
+ hash,
929
+ parent: parentHashes ? parentHashes.split(/\s+/) : [],
930
+ authorDate: new Date(authorDate).toISOString(),
931
+ committerDate: new Date(committerDate).toISOString(),
932
+ subject,
933
+ body: body ?? "",
934
+ files: files ? files.split("\n") : []
935
+ };
936
+ }
937
+ //#endregion
938
+ //#region src/git/commit/getParsedCommit.ts
939
+ var cache = new TTLCache();
940
+ /**
941
+ * Retrieve and parse a commit by its reference.
942
+ *
943
+ * Fetches the raw git log entry for the specified reference, checks the cache
944
+ * for an existing entry, and parses the log data if a cache miss occurs.
945
+ *
946
+ * @param cwd The current working directory path
947
+ * @param ref The git reference to fetch
948
+ * @param markers Optional array of custom trailer marker strings
949
+ * @returns A structured commit object or null if the operation fails
950
+ */
951
+ async function getParsedCommit(cwd, ref, markers = []) {
952
+ try {
953
+ const entry = parseGitPrettyFormat(await execGit(cwd, [
954
+ "log",
955
+ "-1",
956
+ ref,
957
+ "--name-only",
958
+ `--pretty=format:${GIT_PRETTY_FORMAT}`
959
+ ]));
960
+ const key = `${entry.hash}:${entry.parent}:${markers}`;
961
+ const cached = cache.get(key);
962
+ if (cached) return cached;
963
+ const parsed = await parseCommit(entry, markers);
964
+ cache.set(key, parsed);
965
+ return parsed;
966
+ } catch {
967
+ return null;
968
+ }
969
+ }
970
+ //#endregion
971
+ //#region src/git/history/countGitHistory.ts
972
+ /**
973
+ * Count mainline commits reachable from a given reference.
974
+ *
975
+ * Uses `git rev-list --count --first-parent` to calculate the length of the
976
+ * primary history trunk, excluding commits introduced by side-branch merges.
977
+ *
978
+ * @param cwd Repository working directory
979
+ * @param ref Commit reference to count from (defaults to `HEAD`)
980
+ * @returns Number of commits along the primary branch line up to `ref`
981
+ */
982
+ async function countGitHistory(cwd, ref = "HEAD") {
983
+ const stdout = await execGit(cwd, [
984
+ "rev-list",
985
+ "--count",
986
+ "--first-parent",
987
+ ref
988
+ ]);
989
+ return Number.parseInt(stdout.trim(), 10);
990
+ }
991
+ //#endregion
992
+ //#region src/git/repository/isGitShallowRepository.ts
993
+ /**
994
+ * Determine whether the repository at `cwd` is a shallow clone.
995
+ *
996
+ * Runs `git rev-parse --is-shallow-repository` to detect shallow clones.
997
+ * Useful before operations that require full history.
998
+ *
999
+ * @param cwd Repository working directory
1000
+ * @returns `true` if shallow, `false` if full clone, or `null` on error
1001
+ */
1002
+ async function isGitShallowRepository(cwd) {
1003
+ return (await execGit(cwd, ["rev-parse", "--is-shallow-repository"])).trim() === "true";
1004
+ }
1005
+ //#endregion
1006
+ //#region src/git/repository/withShallowLock.ts
1007
+ var lockQueue = /* @__PURE__ */ new Map();
1008
+ var context = new AsyncLocalStorage();
1009
+ /**
1010
+ * Serialize access to a shallow Git repository while running a callback.
1011
+ *
1012
+ * If `cwd` is a shallow clone, callers from different async contexts are
1013
+ * executed one at a time; nested calls within the same async context run
1014
+ * immediately without waiting. If the repository becomes non-shallow before
1015
+ * a queued turn runs, downstream waiters are released early.
1016
+ *
1017
+ * @param cwd Repository working directory path string
1018
+ * @param callback Function to execute while the lock is held
1019
+ * @returns Promise resolving to the result of `callback`
1020
+ */
1021
+ async function withShallowLock(cwd, callback) {
1022
+ if (!await isGitShallowRepository(cwd)) return callback();
1023
+ const active = context.getStore();
1024
+ if (active?.has(cwd)) return callback();
1025
+ const { promise, resolve } = Promise.withResolvers();
1026
+ const previous = lockQueue.get(cwd) ?? Promise.resolve();
1027
+ const queued = previous.then(() => promise);
1028
+ lockQueue.set(cwd, queued);
1029
+ await previous;
1030
+ if (!await isGitShallowRepository(cwd)) resolve();
1031
+ try {
1032
+ return await context.run(new Set(active).add(cwd), callback);
1033
+ } finally {
1034
+ resolve();
1035
+ if (lockQueue.get(cwd) === queued) lockQueue.delete(cwd);
1036
+ }
1037
+ }
1038
+ //#endregion
1039
+ //#region src/git/revision/isGitAncestor.ts
1040
+ /**
1041
+ * Determine whether `ancestor` is an ancestor of `descendant`.
1042
+ *
1043
+ * Runs `git merge-base --is-ancestor <ancestor> <descendant>` in the repository at `cwd`.
1044
+ * Resolves to `true` if `ancestor` is an ancestor, `false` if not.
1045
+ * Rethrows errors for invalid refs or other failures.
1046
+ *
1047
+ * @param cwd Repository working directory
1048
+ * @param ancestor Candidate ancestor ref
1049
+ * @param descendant Candidate descendant ref
1050
+ * @returns Resolves to `true` if `ancestor` is an ancestor of `descendant`, `false` otherwise
1051
+ * @throws If a ref is invalid or the Git command fails
1052
+ */
1053
+ function isGitAncestor(cwd, ancestor, descendant) {
1054
+ return execGit(cwd, [
1055
+ "merge-base",
1056
+ "--is-ancestor",
1057
+ ancestor,
1058
+ descendant
1059
+ ]).then(() => true).catch((err) => {
1060
+ if (err?.code === 1) return false;
1061
+ throw err;
1062
+ });
1063
+ }
1064
+ //#endregion
1065
+ //#region src/git/history/expandGitHistory.ts
1066
+ /**
1067
+ * Deepen a shallow clone so additional commits become available.
1068
+ *
1069
+ * Attempts a single `--shallow-since` fetch to reach `until` (if provided),
1070
+ * otherwise deepens by `pageSize`, accounting for existing depth when `from`
1071
+ * is supplied. Operation is serialized to avoid concurrent deepens.
1072
+ *
1073
+ * @param cwd Repository working directory
1074
+ * @param options Options: { pageSize?, from?, until? }
1075
+ * @returns true if history was deepened or already sufficient, otherwise false
1076
+ */
1077
+ async function expandGitHistory(cwd, options = {}) {
1078
+ if (!await isGitShallowRepository(cwd)) return false;
1079
+ return withShallowLock(cwd, async () => {
1080
+ const { until } = options;
1081
+ let { from } = options;
1082
+ let { pageSize = 50 } = options;
1083
+ if (until) {
1084
+ if (await isGitAncestor(cwd, until, "HEAD")) return true;
1085
+ const commitDate = (await execGit(cwd, [
1086
+ "show",
1087
+ "-s",
1088
+ "--format=%cI",
1089
+ `${until}^{commit}`
1090
+ ])).trim();
1091
+ try {
1092
+ await execGit(cwd, [
1093
+ "fetch",
1094
+ `--shallow-since=${commitDate}`,
1095
+ "--update-shallow",
1096
+ "--filter=blob:none"
1097
+ ]);
1098
+ return true;
1099
+ } catch {}
1100
+ }
1101
+ if (from) {
1102
+ from = await resolveGitRevision(cwd, from);
1103
+ const depth = await countGitHistory(cwd, from);
1104
+ if (depth > pageSize) return true;
1105
+ pageSize -= depth - 1;
1106
+ }
1107
+ try {
1108
+ await execGit(cwd, [
1109
+ "fetch",
1110
+ `--deepen=${pageSize}`,
1111
+ "--update-shallow",
1112
+ "--filter=blob:none",
1113
+ ...from && !await isGitAncestor(cwd, from, "HEAD") ? [
1114
+ "origin",
1115
+ from,
1116
+ "HEAD"
1117
+ ] : []
1118
+ ]);
1119
+ return true;
1120
+ } catch {
1121
+ return false;
1122
+ }
1123
+ });
1124
+ }
1125
+ //#endregion
1126
+ //#region src/git/revision/detectGitRelationship.ts
1127
+ var GIT_REL_FLAGS = {
1128
+ NONE: 0,
1129
+ IS_ANCESTOR: 1,
1130
+ IS_DESCENDANT: 2,
1131
+ IS_EQUAL: 3
1132
+ };
1133
+ /**
1134
+ * Determine the relationship between two git refs.
1135
+ *
1136
+ * Runs `git merge-base --is-ancestor` in both directions and returns a
1137
+ * numeric bitmask using `GIT_REL_FLAGS`. A successful check (exit code 0)
1138
+ * indicates an ancestor relationship; non-zero exit is treated as "not ancestor".
1139
+ *
1140
+ * @param cwd Repository working directory
1141
+ * @param refA First ref (candidate ancestor)
1142
+ * @param refB Second ref (candidate descendant)
1143
+ * @returns Bitmask of type `GitRelFlags`
1144
+ */
1145
+ async function detectGitRelationship(cwd, refA, refB) {
1146
+ let flags = GIT_REL_FLAGS.NONE;
1147
+ const [aIsAncestor, bIsAncestor] = await Promise.all([isGitAncestor(cwd, refA, refB), isGitAncestor(cwd, refB, refA)]);
1148
+ if (aIsAncestor) flags |= GIT_REL_FLAGS.IS_ANCESTOR;
1149
+ if (bIsAncestor) flags |= GIT_REL_FLAGS.IS_DESCENDANT;
1150
+ return flags;
1151
+ }
1152
+ //#endregion
1153
+ //#region src/git/history/mergeGitHistory.ts
1154
+ /**
1155
+ * Merge two newest-first commit lists into a single deduplicated newest-first list.
1156
+ *
1157
+ * Compares commits by hash, uses git relationship (ancestor/descendant) when needed,
1158
+ * and falls back to committerDate for unrelated commits.
1159
+ *
1160
+ * @param cwd Repository working directory used for relationship checks
1161
+ * @param a Commits from the first source (newest-first)
1162
+ * @param b Commits from the second source (newest-first)
1163
+ * @returns Merged commits in newest-first order with duplicates removed
1164
+ */
1165
+ async function mergeGitHistory(cwd, a, b) {
1166
+ const merged = [];
1167
+ const a2 = [...a];
1168
+ const b2 = [...b];
1169
+ let i = a2.shift();
1170
+ let j = b2.shift();
1171
+ while (i && j) switch (i.hash === j.hash ? GIT_REL_FLAGS.IS_EQUAL : await detectGitRelationship(cwd, i.hash, j.hash)) {
1172
+ case GIT_REL_FLAGS.IS_EQUAL:
1173
+ merged.push(i);
1174
+ i = a2.shift();
1175
+ j = b2.shift();
1176
+ break;
1177
+ case GIT_REL_FLAGS.IS_ANCESTOR:
1178
+ merged.push(j);
1179
+ j = b2.shift();
1180
+ break;
1181
+ case GIT_REL_FLAGS.IS_DESCENDANT:
1182
+ merged.push(i);
1183
+ i = a2.shift();
1184
+ break;
1185
+ case GIT_REL_FLAGS.NONE: if (i.committerDate < j.committerDate) {
1186
+ merged.push(j);
1187
+ j = b2.shift();
1188
+ } else {
1189
+ merged.push(i);
1190
+ i = a2.shift();
1191
+ }
1192
+ }
1193
+ if (i) merged.push(i);
1194
+ if (j) merged.push(j);
1195
+ return merged.concat(a2).concat(b2);
1196
+ }
1197
+ //#endregion
1198
+ //#region src/git/history/restoreFullGitHistory.ts
1199
+ /**
1200
+ * Convert a shallow clone into a full clone by fetching additional history.
1201
+ *
1202
+ * Runs `git fetch --unshallow`. Returns `true` when the operation succeeds
1203
+ * and `false` on error.
1204
+ *
1205
+ * @param cwd Repository working directory
1206
+ * @returns `true` when the operation succeeded, otherwise `false`
1207
+ */
1208
+ function restoreFullGitHistory(cwd) {
1209
+ return execGit(cwd, ["fetch", "--unshallow"]).then(() => true).catch(() => false);
1210
+ }
1211
+ //#endregion
1212
+ //#region src/git/history/showGitHistory.ts
1213
+ /**
1214
+ * Yield commit SHAs from repository history in paginated order.
1215
+ *
1216
+ * Ensures commits are available via `expandGitHistory`, resolves `until` to a
1217
+ * commit id, and pages from `from` toward `until`, yielding each commit SHA.
1218
+ * If a git command fails the generator ends silently.
1219
+ *
1220
+ * @param cwd Repository working directory
1221
+ * @param options Supports `from`, `until`, `pageSize`, and `filePath`
1222
+ * @yields Commit SHA strings in paginated order
1223
+ */
1224
+ async function* showGitHistory(cwd, options = {}) {
1225
+ try {
1226
+ const { filePath, pageSize = 50 } = options;
1227
+ let from = options.from || "HEAD";
1228
+ let until = options.until || void 0;
1229
+ if (until) until = await resolveGitRevision(cwd, until);
1230
+ while (true) {
1231
+ await expandGitHistory(cwd, {
1232
+ from,
1233
+ until,
1234
+ pageSize
1235
+ });
1236
+ const args = [
1237
+ "rev-list",
1238
+ `--max-count=${pageSize}`,
1239
+ from
1240
+ ];
1241
+ if (until) args.push(`^${until}`);
1242
+ if (filePath) args.push("--", ...(Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean));
1243
+ const list = (await execGit(cwd, args)).trim().split("\n").filter(Boolean);
1244
+ for (const item of list.slice(0, -1)) yield item;
1245
+ const tail = list.at(-1);
1246
+ if (!tail) return;
1247
+ if (tail === from) {
1248
+ yield tail;
1249
+ from = `${tail}^`;
1250
+ from = await resolveGitRevision(cwd, from);
1251
+ } else from = tail;
1252
+ }
1253
+ } catch {}
1254
+ }
1255
+ //#endregion
1256
+ //#region src/git/history/showParsedGitHistory.ts
1257
+ /**
1258
+ * Yield parsed commits sequentially from repository history.
1259
+ *
1260
+ * Fetches commits using `showGitHistory` and parses each SHA with
1261
+ * `getParsedCommit`. Yields successfully parsed `Commit` objects on demand and
1262
+ * omits unparsable commits.
1263
+ *
1264
+ * @param cwd Repository working directory
1265
+ * @param options Filter and pagination options passed to `showGitHistory`
1266
+ * @yields Parsed `Commit` objects in historical order
1267
+ */
1268
+ async function* showParsedGitHistory(cwd, options) {
1269
+ for await (const commit of showGitHistory(cwd, options)) {
1270
+ const parsed = await getParsedCommit(cwd, commit);
1271
+ if (parsed) yield parsed;
1272
+ }
1273
+ }
1274
+ //#endregion
1275
+ //#region src/git/repository/getGitConfigValue.ts
1276
+ /**
1277
+ * Get a local git configuration value for `key` in the repository at `cwd`.
1278
+ *
1279
+ * Returns the config value when present; otherwise `undefined`.
1280
+ *
1281
+ * @param cwd Repository working directory
1282
+ * @param key Git config key to read
1283
+ * @returns The config value or `undefined` if not set
1284
+ */
1285
+ async function getGitConfigValue(cwd, key) {
1286
+ try {
1287
+ const value = (await execGit(cwd, [
1288
+ "config",
1289
+ "--local",
1290
+ "--get",
1291
+ key
1292
+ ])).trim();
1293
+ return value ? value : void 0;
1294
+ } catch {
1295
+ return;
1296
+ }
1297
+ }
1298
+ //#endregion
1299
+ //#region src/git/repository/ensureGitUserConfig.ts
1300
+ /**
1301
+ * Ensure git `user.name` and `user.email` are configured for the repository at `cwd`.
1302
+ *
1303
+ * No-op when the values are already present.
1304
+ *
1305
+ * @param cwd Repository working directory
1306
+ * @param user Object containing `name` and `email` values to set when missing
1307
+ * @returns Promise that resolves when configuration is ensured
1308
+ */
1309
+ async function ensureGitUserConfig(cwd, user) {
1310
+ if (await getGitConfigValue(cwd, "user.name") && await getGitConfigValue(cwd, "user.email")) return;
1311
+ await execGit(cwd, [
1312
+ "config",
1313
+ "user.name",
1314
+ user.name
1315
+ ]);
1316
+ await execGit(cwd, [
1317
+ "config",
1318
+ "user.email",
1319
+ user.email
1320
+ ]);
1321
+ }
1322
+ //#endregion
1323
+ //#region src/git/repository/getGitRemote.ts
1324
+ /**
1325
+ * Get the URL for the named remote (defaults to `origin`).
1326
+ *
1327
+ * @param cwd Repository working directory
1328
+ * @param remoteName Remote name (defaults to `'origin'`)
1329
+ * @returns Remote URL string or `null` when unavailable
1330
+ */
1331
+ function getGitRemote(cwd, remoteName = "origin") {
1332
+ return execGit(cwd, [
1333
+ "remote",
1334
+ "get-url",
1335
+ remoteName
1336
+ ]).then((stdout) => stdout.trim()).catch(() => null);
1337
+ }
1338
+ //#endregion
1339
+ //#region src/git/repository/pushGitChanges.ts
1340
+ /**
1341
+ * Push a branch and optional tags to the `origin` remote using an atomic push.
1342
+ *
1343
+ * @param cwd Repository working directory
1344
+ * @param branch Branch name to push
1345
+ * @param additionalTags Optional list of tag names to push alongside the branch
1346
+ * @returns Promise that resolves when the push completes
1347
+ */
1348
+ async function pushGitChanges(cwd, branch, ...additionalTags) {
1349
+ await execGit(cwd, [
1350
+ "push",
1351
+ "origin",
1352
+ ...compact([branch, ...additionalTags]),
1353
+ "--atomic"
1354
+ ]);
1355
+ }
1356
+ //#endregion
1357
+ //#region src/git/repository/resolveGitRepository.ts
1358
+ /**
1359
+ * Resolve Git repository host, owner, and project metadata from the `origin` remote URL.
1360
+ *
1361
+ * @param cwd Repository working directory
1362
+ * @returns Promise that resolves to a `GitRepository` object, or `null` when unavailable
1363
+ */
1364
+ async function resolveGitRepository(cwd) {
1365
+ const origin = await getGitRemote(cwd);
1366
+ if (!origin) return null;
1367
+ const info = GitHost.fromUrl(origin);
1368
+ if (info && typeof info.type === "string" && typeof info.user === "string" && typeof info.project === "string") return {
1369
+ host: info.type,
1370
+ owner: info.user,
1371
+ repo: info.project,
1372
+ url: info.browse()
1373
+ };
1374
+ return null;
1375
+ }
1376
+ //#endregion
1377
+ //#region src/git/revision/readGitFile.ts
1378
+ /**
1379
+ * Read a file's contents from a specific Git revision.
1380
+ *
1381
+ * Runs `git show <ref>:<filePath>` and returns the command output as a
1382
+ * string. If the file does not exist at the given revision or the Git
1383
+ * command fails for any reason, the function returns `null`.
1384
+ *
1385
+ * The returned string is the raw file contents as emitted by Git; callers
1386
+ * that need binary-safe handling should decode or process the bytes
1387
+ * accordingly. This function intentionally swallows Git errors and signals
1388
+ * absence via a `null` result rather than throwing.
1389
+ *
1390
+ * @param cwd Repository working directory
1391
+ * @param ref Commit-ish reference (commit SHA, tag, or branch) to read from
1392
+ * @param filePath Path to the file relative to the repository root
1393
+ * @returns File contents as a string, or `null` if the file is unavailable at the ref
1394
+ */
1395
+ function readGitFile(cwd, ref, filePath) {
1396
+ return execGit(cwd, ["show", `${ref}:${filePath}`]).catch(() => null);
1397
+ }
1398
+ //#endregion
1399
+ //#region src/git/tag/createGitTag.ts
1400
+ /**
1401
+ * Create a git tag in the repository at `cwd`.
1402
+ *
1403
+ * Creates an annotated tag when `message` is provided.
1404
+ *
1405
+ * @param cwd Repository working directory
1406
+ * @param tag Tag name to create
1407
+ * @param message Optional tag message (creates an annotated tag when present)
1408
+ * @returns Promise that resolves when the tag is created
1409
+ */
1410
+ async function createGitTag(cwd, tag, message) {
1411
+ await execGit(cwd, ["tag", ...message ? [
1412
+ "-a",
1413
+ tag,
1414
+ "-m",
1415
+ message,
1416
+ "--cleanup=whitespace"
1417
+ ] : [tag]]);
1418
+ }
1419
+ //#endregion
1420
+ //#region src/git/tag/deleteGitTag.ts
1421
+ /**
1422
+ * Delete a local git tag in the repository at `cwd`.
1423
+ *
1424
+ * @param cwd Repository working directory
1425
+ * @param tag Tag name to delete
1426
+ * @returns `true` when deletion succeeded, otherwise `false`
1427
+ */
1428
+ function deleteGitTag(cwd, tag) {
1429
+ return execGit(cwd, [
1430
+ "tag",
1431
+ "-d",
1432
+ tag
1433
+ ]).then(() => true).catch(() => false);
1434
+ }
1435
+ //#endregion
1436
+ //#region src/git/tag/isGitTagPresent.ts
1437
+ /**
1438
+ * Determine whether a specific git tag exists locally in the repository.
1439
+ *
1440
+ * @param cwd Working directory for the git command
1441
+ * @param tag Name of the tag to evaluate
1442
+ * @returns True if the tag reference exists locally, false otherwise
1443
+ */
1444
+ function isGitTagPresent(cwd, tag) {
1445
+ return resolveGitRevision(cwd, `refs/tags/${tag}`).then(() => true).catch(() => false);
1446
+ }
1447
+ //#endregion
1448
+ //#region src/git/tag/ensureGitTag.ts
1449
+ var MIN_HISTORY_DEPTH = 2;
1450
+ var ORIGIN_NOT_REPO = `fatal: 'origin' does not appear to be a git repository`;
1451
+ var REMOTE_REF_NOT_FOUND = `fatal: couldn't find remote ref`;
1452
+ /**
1453
+ * Ensure a tag exists locally, fetching it shallowly from `origin` if needed.
1454
+ *
1455
+ * Acquires a shallow-fetch lock, verifies local presence and history depth,
1456
+ * attempts a shallow fetch of `refs/tags/<tag>` when required, and removes a
1457
+ * local tag if the remote reports the tag is missing.
1458
+ *
1459
+ * @param cwd Working directory
1460
+ * @param tag Tag name (for example `v1.2.0`)
1461
+ * @returns `true` if the tag is present or was fetched; otherwise `false`
1462
+ */
1463
+ function ensureGitTag(cwd, tag) {
1464
+ return withShallowLock(cwd, async () => {
1465
+ const tagRef = `refs/tags/${tag}`;
1466
+ if (await isGitTagPresent(cwd, tag) && await countGitHistory(cwd, tagRef) >= MIN_HISTORY_DEPTH) return true;
1467
+ try {
1468
+ await execGit(cwd, [
1469
+ "fetch",
1470
+ "origin",
1471
+ `${tagRef}:${tagRef}`,
1472
+ `--depth=${MIN_HISTORY_DEPTH}`,
1473
+ "--update-shallow",
1474
+ "--force"
1475
+ ]);
1476
+ return true;
1477
+ } catch (err) {
1478
+ const errorMessage = err?.message;
1479
+ if (errorMessage?.includes(ORIGIN_NOT_REPO)) return await isGitTagPresent(cwd, tag);
1480
+ if (errorMessage?.includes(REMOTE_REF_NOT_FOUND)) {
1481
+ if (await isGitTagPresent(cwd, tag)) await deleteGitTag(cwd, tag);
1482
+ }
1483
+ }
1484
+ return false;
1485
+ });
1486
+ }
1487
+ //#endregion
1488
+ //#region src/git/tag/generateGitTagName.ts
1489
+ function generateGitTagName(entry, name, version) {
1490
+ if (!version) return null;
1491
+ if (entry) return `v${version}`;
1492
+ if (!name) throw new Error("Package name is required to build a tag name.");
1493
+ return `${name}@${version}`;
1494
+ }
1495
+ //#endregion
1496
+ //#region src/git/tag/getCurrentGitTag.ts
1497
+ /**
1498
+ * Get the tag name that exactly matches the current `HEAD`, if any.
1499
+ *
1500
+ * @param cwd Repository working directory
1501
+ * @returns Tag name or `null` when no exact tag matches HEAD
1502
+ */
1503
+ async function getCurrentGitTag(cwd) {
1504
+ try {
1505
+ const tag = (await execGit(cwd, [
1506
+ "describe",
1507
+ "--tags",
1508
+ "--exact-match"
1509
+ ])).trim();
1510
+ /* istanbul ignore next */
1511
+ return tag ? tag : null;
1512
+ } catch {
1513
+ return null;
1514
+ }
1515
+ }
1516
+ //#endregion
1517
+ //#region src/git/tag/getGitTagMessage.ts
1518
+ /**
1519
+ * Get the message of an annotated git tag in the repository at `cwd`.
1520
+ *
1521
+ * @param cwd Repository working directory
1522
+ * @param tag Tag name to query
1523
+ * @returns The tag message string when found, otherwise `undefined`
1524
+ */
1525
+ async function getGitTagMessage(cwd, tag) {
1526
+ try {
1527
+ const [objectType, message] = (await execGit(cwd, [
1528
+ "tag",
1529
+ "-l",
1530
+ "--format=%(objecttype)%00%(contents)",
1531
+ tag
1532
+ ])).split("\0");
1533
+ if (objectType?.trim() !== "tag") return;
1534
+ return message?.trim() || void 0;
1535
+ } catch {
1536
+ return;
1537
+ }
1538
+ }
1539
+ //#endregion
1540
+ //#region src/git/tag/getLatestGitTag.ts
1541
+ /**
1542
+ * Get the most recent tag reachable from `HEAD`.
1543
+ *
1544
+ * @param cwd Repository working directory
1545
+ * @returns Latest tag string or `null` when none is found
1546
+ */
1547
+ async function getLatestGitTag(cwd) {
1548
+ try {
1549
+ const tag = (await execGit(cwd, [
1550
+ "describe",
1551
+ "--tags",
1552
+ "--abbrev=0"
1553
+ ])).trim();
1554
+ /* istanbul ignore next */
1555
+ return tag ? tag : null;
1556
+ } catch {
1557
+ return null;
1558
+ }
1559
+ }
1560
+ //#endregion
1561
+ //#region src/git/tag/isGitTagConnected.ts
1562
+ /**
1563
+ * Determine whether a specific git tag commit is reachable from the current HEAD.
1564
+ *
1565
+ * Uses `git merge-base --is-ancestor refs/tags/<tag>^{commit} HEAD` to verify
1566
+ * that the tag's commit is an ancestor of the current reference. Any non-zero
1567
+ * exit status or execution error is treated as "not reachable".
1568
+ *
1569
+ * @param cwd Working directory where the git command will run
1570
+ * @param tag Tag name to evaluate (for example, "v1.2.0")
1571
+ * @returns True if the tag commit is reachable from HEAD, false otherwise
1572
+ */
1573
+ async function isGitTagConnected(cwd, tag) {
1574
+ try {
1575
+ await execGit(cwd, [
1576
+ "merge-base",
1577
+ "--is-ancestor",
1578
+ `refs/tags/${tag}^{commit}`,
1579
+ "HEAD"
1580
+ ]);
1581
+ return true;
1582
+ } catch {
1583
+ return false;
1584
+ }
1585
+ }
1586
+ //#endregion
1587
+ //#region src/git/tag/isGitTagPointed.ts
1588
+ /**
1589
+ * Check whether a given tag points to the provided commit.
1590
+ *
1591
+ * @param cwd Repository working directory
1592
+ * @param tag Tag name to resolve
1593
+ * @param commit Commit hash to compare against
1594
+ * @returns `true` when the tag points at `commit`, otherwise `false`
1595
+ */
1596
+ async function isGitTagPointed(cwd, tag, commit) {
1597
+ try {
1598
+ await execGit(cwd, [
1599
+ "merge-base",
1600
+ "--is-ancestor",
1601
+ `refs/tags/${tag}^{commit}`,
1602
+ commit
1603
+ ]);
1604
+ await execGit(cwd, [
1605
+ "merge-base",
1606
+ "--is-ancestor",
1607
+ commit,
1608
+ `refs/tags/${tag}^{commit}`
1609
+ ]);
1610
+ return true;
1611
+ } catch {
1612
+ return false;
1613
+ }
1614
+ }
1615
+ //#endregion
1616
+ //#region src/workflow/discovery/buildDependencyGraph.ts
1617
+ /**
1618
+ * Build a dependency graph mapping package names to dependent release plans.
1619
+ *
1620
+ * Each key is a package name and the value is a set of `ReleasePlan`s that
1621
+ * list that package as a dependency.
1622
+ *
1623
+ * @param plans Array of release plans to analyze
1624
+ * @returns Dependency graph mapping package name -> set of dependent plans
1625
+ */
1626
+ function buildDependencyGraph(plans) {
1627
+ const graph = /* @__PURE__ */ new Map();
1628
+ const packageNames = new Set(plans.map((plan) => plan.package.name).filter((name) => !!name));
1629
+ for (const plan of plans) for (const depName of plan.package.dependencies) {
1630
+ let set = graph.get(depName);
1631
+ if (!set && packageNames.has(depName)) graph.set(depName, set = /* @__PURE__ */ new Set());
1632
+ if (set) set.add(plan);
1633
+ }
1634
+ return graph;
1635
+ }
1636
+ //#endregion
1637
+ //#region src/workflow/changelog/extractPlainText.ts
1638
+ /**
1639
+ * Extract trimmed plain text from a Heading, Paragraph, or PhrasingContent node.
1640
+ *
1641
+ * Performs a depth-first traversal, concatenating string `value` tokens in
1642
+ * document order. Non-string values are ignored. Returns `null` for empty or
1643
+ * missing input.
1644
+ *
1645
+ * @param node Node to extract text from
1646
+ * @returns The trimmed plain-text string, or null if empty
1647
+ */
1648
+ function extractPlainText(node) {
1649
+ if (!node) return null;
1650
+ const queue = [node];
1651
+ let result = "";
1652
+ while (node = queue.shift()) {
1653
+ if ("value" in node && typeof node.value === "string") result += node.value;
1654
+ /* istanbul ignore else */
1655
+ if ("children" in node && Array.isArray(node.children)) queue.unshift(...node.children);
1656
+ }
1657
+ return result.trim() || null;
1658
+ }
1659
+ //#endregion
1660
+ //#region src/workflow/changelog/extractSemVer.ts
1661
+ var ANCHORED_SEMVER_REGEX = new RegExp(`^${SEMVER_REGEX.source}$`);
1662
+ var BRACKETED_SEMVER_REGEX = new RegExp(`\\[${SEMVER_REGEX.source}\\]`);
1663
+ /**
1664
+ * Extract SemVer core strings from a line.
1665
+ *
1666
+ * Splits the line on runs of spaces or tabs and on `..`/`...`. For each token,
1667
+ * first attempts an anchored SemVer match (must match the whole token), then
1668
+ * a bracketed match that finds a SemVer inside `[...]`. Returns the captured
1669
+ * SemVer core (capture group 2) for each successful match; falsy results are
1670
+ * filtered out.
1671
+ *
1672
+ * @param line Line to scan for SemVer cores
1673
+ * @returns Array of extracted SemVer core strings; empty if none
1674
+ */
1675
+ function extractSemVer(line) {
1676
+ return compact(line.split(/[ \t]+|\.{2,3}/).map((token) => {
1677
+ return (ANCHORED_SEMVER_REGEX.exec(token) ?? BRACKETED_SEMVER_REGEX.exec(token))?.at(2);
1678
+ }));
1679
+ }
1680
+ //#endregion
1681
+ //#region src/workflow/changelog/iterateChangelogVersions.ts
1682
+ var SENTENCE_TERMINAL_REGEX = /\p{Sentence_Terminal}$/u;
1683
+ /**
1684
+ * Iterate over Semantic Version strings extracted from a Markdown changelog.
1685
+ *
1686
+ * Parses `changelogContent` into an AST and evaluates nodes sequentially using
1687
+ * custom extractors. Evaluates heading nodes first before checking short
1688
+ * paragraph nodes. Stops execution after the first strategy yields valid
1689
+ * versions.
1690
+ *
1691
+ * @param changelogContent Raw Markdown text of the changelog
1692
+ * @yields Valid SemVer strings found in headings or candidate paragraphs
1693
+ */
1694
+ function* iterateChangelogVersions(changelogContent) {
1695
+ const root = fromMarkdown(changelogContent);
1696
+ const nodeExtractors = [(node) => {
1697
+ return node.type === "heading" ? extractPlainText(node) : null;
1698
+ }, (node) => {
1699
+ if (node.type !== "paragraph") return null;
1700
+ const paragraphText = extractPlainText(node);
1701
+ if (!paragraphText || paragraphText.length > 48 || SENTENCE_TERMINAL_REGEX.test(paragraphText)) return null;
1702
+ return paragraphText;
1703
+ }];
1704
+ for (const extractText of nodeExtractors) {
1705
+ let isVersionFound = false;
1706
+ for (const node of root.children) {
1707
+ const extractedText = extractText(node);
1708
+ const candidateVersion = extractedText ? semver.sort(extractSemVer(extractedText)).pop() : null;
1709
+ if (candidateVersion && semver.valid(candidateVersion)) {
1710
+ isVersionFound = true;
1711
+ yield candidateVersion;
1712
+ }
1713
+ }
1714
+ if (isVersionFound) return;
1715
+ }
1716
+ }
1717
+ //#endregion
1718
+ //#region src/workflow/changelog/extractLatestVersion.ts
1719
+ /**
1720
+ * Find the highest semantic version from the first version-bearing element.
1721
+ *
1722
+ * Pulls the first available version string from the parsed markdown iterator.
1723
+ * Short-circuits execution immediately upon locating a match to avoid passing
1724
+ * through the remaining document structure.
1725
+ *
1726
+ * @param changelogContent The raw markdown string representing the changelog
1727
+ * @returns The highest valid semantic version string found or null
1728
+ */
1729
+ function extractLatestVersion(changelogContent) {
1730
+ for (const version of iterateChangelogVersions(changelogContent)) return version;
1731
+ return null;
1732
+ }
1733
+ //#endregion
1734
+ //#region src/workflow/discovery/verifyPackageTag.ts
1735
+ /**
1736
+ * Verify a Git tag against package specifications and history connection.
1737
+ *
1738
+ * Ensures the target tag exists locally before parsing its commit data. Rejects
1739
+ * tags that do not target a commit matching the given `pkg`. Expands the history
1740
+ * graph up to the tag reference before verifying structural connectivity.
1741
+ *
1742
+ * @param cwd Repository working directory
1743
+ * @param pkg Package descriptor containing version and location context
1744
+ * @param tagName The name of the Git tag to verify
1745
+ * @returns A promise resolving to true if the tag is valid and connected
1746
+ */
1747
+ async function verifyPackageTag(cwd, pkg, tagName) {
1748
+ await ensureGitTag(cwd, tagName);
1749
+ const tagRef = `refs/tags/${tagName}`;
1750
+ const tagCommit = await getParsedCommit(cwd, tagRef);
1751
+ if (!tagCommit || !isCommitForPackage(pkg, tagCommit)) return false;
1752
+ await expandGitHistory(cwd, { until: tagRef });
1753
+ return isGitTagConnected(cwd, tagName);
1754
+ }
1755
+ //#endregion
1756
+ //#region src/workflow/discovery/discoverPackageTag.ts
1757
+ /**
1758
+ * Discover and validate the Git tag for a package.
1759
+ *
1760
+ * Returns the existing tag if already present on the `pkg` object. Resolves
1761
+ * the local changelog file path, extracts the current version, and generates
1762
+ * the expected Git tag name. Verifies the tag against the repository and
1763
+ * ensures its historical changelog version matches the current local version.
1764
+ *
1765
+ * @param cwd The current working directory
1766
+ * @param pkg The package configuration object
1767
+ * @returns The validated `Tag` object or null if validation fails
1768
+ */
1769
+ async function discoverPackageTag(cwd, pkg) {
1770
+ if (pkg.tag) return pkg.tag;
1771
+ const changelogFile = pkg.changelogFile || "CHANGELOG.md";
1772
+ const changelogFilePath = path.resolve(cwd, pkg.dir, changelogFile);
1773
+ let localChangelogContent;
1774
+ try {
1775
+ localChangelogContent = await readFile(changelogFilePath, "utf-8");
1776
+ } catch {}
1777
+ if (!localChangelogContent) return null;
1778
+ const currentVersion = extractLatestVersion(localChangelogContent);
1779
+ if (!currentVersion) return null;
1780
+ let expectedTagName;
1781
+ try {
1782
+ expectedTagName = generateGitTagName(pkg.entry, pkg.name, currentVersion);
1783
+ } catch {}
1784
+ if (!expectedTagName) return null;
1785
+ if (!await verifyPackageTag(cwd, pkg, expectedTagName)) return null;
1786
+ const historicalChangelogContent = await readGitFile(cwd, await resolveGitRevision(cwd, `refs/tags/${expectedTagName}`), path.relative(cwd, changelogFilePath));
1787
+ if (!historicalChangelogContent) return null;
1788
+ const historicalChangelogVersion = extractLatestVersion(historicalChangelogContent);
1789
+ if (!historicalChangelogVersion || historicalChangelogVersion !== currentVersion) return null;
1790
+ return {
1791
+ name: expectedTagName,
1792
+ message: await getGitTagMessage(cwd, expectedTagName)
1793
+ };
1794
+ }
1795
+ //#endregion
1796
+ //#region src/workflow/discovery/resolvePackageStableTag.ts
1797
+ /**
1798
+ * Resolve the latest stable Git tag for a package.
1799
+ *
1800
+ * Reads changelog release versions for `pkg` and verifies strict descending
1801
+ * semver order. Ignores prerelease versions unless transitioning across major
1802
+ * or prerelease boundaries, and verifies that the matching Git tag exists.
1803
+ *
1804
+ * @param cwd Absolute path to the root working directory
1805
+ * @param pkg Package configuration object
1806
+ * @returns Verified tag metadata object or `null` if no stable tag is resolved
1807
+ * @throws If a version is invalid, versions are not in strict descending
1808
+ * order, or the corresponding Git tag is missing
1809
+ */
1810
+ async function resolvePackageStableTag(cwd, pkg) {
1811
+ const changelogFile = pkg.changelogFile || "CHANGELOG.md";
1812
+ const changelogFilePath = path.resolve(cwd, pkg.dir, changelogFile);
1813
+ let changelogContent;
1814
+ try {
1815
+ changelogContent = await readFile(changelogFilePath, "utf-8");
1816
+ } catch (error) {
1817
+ /* istanbul ignore else */
1818
+ if (error?.code === "ENOENT") return null;
1819
+ /* istanbul ignore next */
1820
+ throw error;
1821
+ }
1822
+ let precedingVersion = null;
1823
+ for (const version of iterateChangelogVersions(changelogContent)) {
1824
+ const currentVersion = semver.parse(version);
1825
+ assert(currentVersion, `Version "${version}" is not a valid semantic version.`);
1826
+ if (precedingVersion && semver.gte(currentVersion, precedingVersion)) throw new Error(`Version "${version}" must be strictly lower than preceding version "${precedingVersion.raw}".`);
1827
+ if (!semver.prerelease(currentVersion) && (currentVersion.major >= 1 || precedingVersion && semver.prerelease(precedingVersion))) {
1828
+ const tagName = generateGitTagName(pkg.entry, pkg.name, version);
1829
+ if (!await verifyPackageTag(cwd, pkg, tagName)) throw new Error(`Git tag "${tagName}" is not associated with package "${pkg.name ?? ""}" in directory "${pkg.dir}".`);
1830
+ return {
1831
+ name: tagName,
1832
+ message: await getGitTagMessage(cwd, tagName)
1833
+ };
1834
+ }
1835
+ precedingVersion = currentVersion;
1836
+ }
1837
+ return null;
1838
+ }
1839
+ //#endregion
1840
+ //#region src/workflow/discovery/resolvePackageTag.ts
1841
+ /**
1842
+ * Resolve the release tag for a single package version.
1843
+ *
1844
+ * Returns the cached `pkg.tag` immediately if it already exists. Generates an
1845
+ * expected tag name from the package metadata and verifies its integrity using
1846
+ * `verifyPackageTag`.
1847
+ *
1848
+ * @param cwd Repository working directory
1849
+ * @param pkg Package descriptor containing version and location context
1850
+ * @returns A promise resolving to the tag metadata or null when not found
1851
+ */
1852
+ async function resolvePackageTag(cwd, pkg) {
1853
+ if (pkg.tag) return pkg.tag;
1854
+ let expectedTagName;
1855
+ try {
1856
+ expectedTagName = generateGitTagName(pkg.entry, pkg.name, pkg.version);
1857
+ } catch {}
1858
+ if (!expectedTagName) return null;
1859
+ if (await verifyPackageTag(cwd, pkg, expectedTagName)) return {
1860
+ name: expectedTagName,
1861
+ message: await getGitTagMessage(cwd, expectedTagName)
1862
+ };
1863
+ return null;
1864
+ }
1865
+ //#endregion
1866
+ //#region src/workflow/analysis/propagateDependencyUpdates.ts
1867
+ /**
1868
+ * Propagate release updates through package dependencies.
1869
+ *
1870
+ * Marks packages that depend on a releasing package for a `patch` release
1871
+ * when they do not already have a release type.
1872
+ *
1873
+ * @param plans Array of release plans to update
1874
+ * @returns Updated array of release plans
1875
+ */
1876
+ async function propagateDependencyUpdates(cwd, plans) {
1877
+ const graph = buildDependencyGraph(plans);
1878
+ const scheduled = /* @__PURE__ */ new Map();
1879
+ const stack = [];
1880
+ for (const plan of plans) if (plan.release?.type) {
1881
+ const visited = /* @__PURE__ */ new Set([plan]);
1882
+ stack.push(plan);
1883
+ let curr;
1884
+ while (curr = stack.pop()) if (curr.package.name) {
1885
+ for (const dependent of graph.get(curr.package.name) ?? []) if (!visited.has(dependent)) {
1886
+ visited.add(dependent);
1887
+ stack.push(dependent);
1888
+ scheduled.set(dependent, await mergeGitHistory(cwd, scheduled.get(dependent) ?? dependent.history, plan.history));
1889
+ }
1890
+ }
1891
+ }
1892
+ return plans.map((plan) => {
1893
+ const history = scheduled.get(plan);
1894
+ if (history) {
1895
+ plan = {
1896
+ ...plan,
1897
+ history
1898
+ };
1899
+ if (!plan.release?.type) plan.release = { type: "patch" };
1900
+ }
1901
+ return plan;
1902
+ });
1903
+ }
1904
+ //#endregion
1905
+ //#region src/workflow/committing/isReleaseCommit.ts
1906
+ /**
1907
+ * Determine whether a commit represents a release or matches a release marker.
1908
+ *
1909
+ * Resolves string commit identifiers to parsed commit objects, checks if any
1910
+ * associated trailer matches the marker predicate, and falls back to verifying
1911
+ * exact tag matches for the `commit` hash.
1912
+ *
1913
+ * @param cwd The working directory path for Git commands
1914
+ * @param commit The commit object, hash string, or null/undefined
1915
+ * @param marker The trailer key string or a predicate function to evaluate
1916
+ * @returns True if the commit is a release commit, otherwise false
1917
+ */
1918
+ async function isReleaseCommit(cwd, commit, marker = GIT_TRAILER_RELEASE_AGENT) {
1919
+ if (typeof commit === "string") commit = await getParsedCommit(cwd, commit, typeof marker === "string" ? [marker] : []);
1920
+ if (!commit) return false;
1921
+ const predicate = typeof marker === "string" ? (trailer) => trailer.key === marker && !!trailer.value : marker;
1922
+ if (commit.trailers.some(predicate)) return true;
1923
+ return execGit(cwd, [
1924
+ "describe",
1925
+ "--tags",
1926
+ "--exact-match",
1927
+ commit.hash
1928
+ ]).then(() => true).catch(() => false);
1929
+ }
1930
+ //#endregion
1931
+ //#region src/workflow/analysis/sliceUnreleased.ts
1932
+ /**
1933
+ * Yield parsed commits from the workspace until a release commit is found.
1934
+ *
1935
+ * Iterates through parsed commit history and yields unreleased commits in
1936
+ * order. Stops processing upon reaching the first release commit.
1937
+ *
1938
+ * @param cwd Path to the working directory
1939
+ * @param options Options for configuring Git history retrieval
1940
+ * @yields Parsed `Commit` objects up to the most recent release commit
1941
+ */
1942
+ async function* sliceUnreleased(cwd, options) {
1943
+ for await (const commit of showParsedGitHistory(cwd, options)) {
1944
+ if (await isReleaseCommit(cwd, commit, options?.marker)) break;
1945
+ yield commit;
1946
+ }
1947
+ }
1948
+ //#endregion
1949
+ //#region src/workflow/analysis/streamPackageCommits.ts
1950
+ /**
1951
+ * Yield commits from git history associated with a package.
1952
+ *
1953
+ * Filters git commit history to stream only entries relevant to the target
1954
+ * `pkg` directory.
1955
+ *
1956
+ * @param cwd Path to the working directory for git commands
1957
+ * @param pkg Package configuration containing the target directory
1958
+ * @param options Options for parsing and filtering git history
1959
+ * @yields Commits belonging to the specified package
1960
+ */
1961
+ async function* streamPackageCommits(cwd, pkg, options = {}) {
1962
+ options = {
1963
+ ...options,
1964
+ filePath: options.filePath || pkg.dir
1965
+ };
1966
+ for await (const commit of showParsedGitHistory(cwd, options)) if (isCommitForPackage(pkg, commit)) yield commit;
1967
+ }
1968
+ //#endregion
1969
+ //#region src/workflow/changelog/buildChangelog.ts
1970
+ var preset = await createPreset();
1971
+ var headerPartial = (context) => {
1972
+ return context.version ? preset.writer.headerPartial?.(context) : void 0;
1973
+ };
1974
+ /**
1975
+ * Build the changelog string for a release.
1976
+ *
1977
+ * Compiles commit history and release context into a formatted changelog using
1978
+ * conventional commits and writer configurations. Resolves file paths and handles
1979
+ * tag comparisons when repository URLs are available.
1980
+ *
1981
+ * @param context The release context containing repository and working directory details
1982
+ * @param history The iterable or async iterable collection of commits to process
1983
+ * @param options The build configuration options for the changelog
1984
+ * @returns The formatted changelog content string
1985
+ */
1986
+ async function buildChangelog(context, history, options = {}) {
1987
+ const repository = context.repository?.url || void 0;
1988
+ const version = options.version || void 0;
1989
+ const previousTag = options.previousTag || void 0;
1990
+ const currentTag = options.currentTag || void 0;
1991
+ return formatFile(path.join(context.cwd, options.packageDir || ".", options.changelogFile || "CHANGELOG.md"), await writeChangelogString(history, {
1992
+ date: context.today,
1993
+ version,
1994
+ previousTag,
1995
+ currentTag,
1996
+ linkCompare: !!repository && !!previousTag && !!currentTag,
1997
+ linkReferences: !!repository,
1998
+ repoUrl: repository,
1999
+ commit: "commit",
2000
+ issue: "issues",
2001
+ headerPartial
2002
+ }, preset.writer));
2003
+ }
2004
+ //#endregion
2005
+ //#region src/workflow/changelog/buildReleaseNotes.ts
2006
+ /**
2007
+ * Build release notes from a release context and commit history.
2008
+ *
2009
+ * Generates changelog content from commits, parses it into an abstract syntax
2010
+ * tree, shifts heading depths, and formats the resulting markdown file.
2011
+ *
2012
+ * @param context The release context containing repository and configuration details
2013
+ * @param history The iterable or async iterable collection of commits to process
2014
+ * @returns The formatted release notes as a markdown string
2015
+ */
2016
+ async function buildReleaseNotes(context, history) {
2017
+ const changelogContent = await buildChangelog(context, history);
2018
+ const root = fromMarkdown(changelogContent);
2019
+ root.children = root.children.flatMap((node) => {
2020
+ if (node.type === "heading") {
2021
+ /* istanbul ignore next */
2022
+ if (node.depth <= 2) return [];
2023
+ node = {
2024
+ ...node,
2025
+ depth: node.depth - 1
2026
+ };
2027
+ }
2028
+ return node;
2029
+ });
2030
+ return formatFile(path.join(context.cwd, DEFAULT_CHANGELOG_FILE), toMarkdown(root));
2031
+ }
2032
+ //#endregion
2033
+ //#region src/workflow/changelog/extractPreviousVersion.ts
2034
+ /**
2035
+ * Find the latest stable version that precedes the current version.
2036
+ *
2037
+ * Parses markdown from top to bottom to extract semantic versions from headings
2038
+ * and paragraphs. Enforces that discovered versions appear in strict
2039
+ * descending order. Returns the first version that falls outside the current
2040
+ * version's major, minor, and patch boundaries if the current version is
2041
+ * `v1.0.0` or higher, or the first non-matching version for `v0.x.x` releases.
2042
+ *
2043
+ * @param currentVersion The version to evaluate against
2044
+ * @param changelogContent The raw markdown content to scan for versions
2045
+ * @returns The resolved stable version string or null when none is found
2046
+ * @throws If `currentVersion` is invalid or already stable, or if versions in the changelog are not listed in strict descending order
2047
+ */
2048
+ async function extractPreviousVersion(currentVersion, changelogContent) {
2049
+ const parsedCurrent = semver.parse(currentVersion);
2050
+ if (!currentVersion || !parsedCurrent) throw new Error(`The current version "${colors.bold(String(currentVersion))}" is not a valid semantic version.`);
2051
+ const currentMajor = parsedCurrent.major;
2052
+ if (currentMajor >= 1 && parsedCurrent.prerelease.length === 0) throw new Error(`The current version "${colors.bold(currentVersion)}" is already a stable release.`);
2053
+ const baseVersion = `${parsedCurrent.major}.${parsedCurrent.minor}.${parsedCurrent.patch}`;
2054
+ let priorVersion = null;
2055
+ for (const version of iterateChangelogVersions(changelogContent)) {
2056
+ const parsedVersion = semver.parse(version);
2057
+ /* istanbul ignore else */
2058
+ if (parsedVersion) if (!priorVersion) {
2059
+ if (version !== currentVersion) return version;
2060
+ priorVersion = parsedVersion;
2061
+ } else {
2062
+ if (semver.gte(parsedVersion, priorVersion)) throw new Error(`Each newer release must have a higher version, but "${colors.bold(priorVersion.version)}" appears before "${colors.bold(version)}".`);
2063
+ priorVersion = parsedVersion;
2064
+ const { major, minor, patch } = parsedVersion;
2065
+ if (currentMajor >= 1 && `${major}.${minor}.${patch}` !== baseVersion) return version;
2066
+ }
2067
+ }
2068
+ return null;
2069
+ }
2070
+ //#endregion
2071
+ //#region src/workflow/changelog/prependChangelog.ts
2072
+ var defaultChangelogTitle = "Changelog";
2073
+ /**
2074
+ * Prepend changelog content to a markdown file, creating it if necessary.
2075
+ *
2076
+ * Reads the existing file if present, prepends the new content before the main heading,
2077
+ * formats and writes the result back.
2078
+ *
2079
+ * @param filePath Path to the changelog file
2080
+ * @param revision Changelog content to prepend
2081
+ * @returns Promise that resolves when the changelog is written
2082
+ */
2083
+ async function prependChangelog(filePath, revision) {
2084
+ let content = "";
2085
+ try {
2086
+ content = await fs.readFile(filePath, "utf-8");
2087
+ } catch (err) {
2088
+ /* istanbul ignore next */
2089
+ if (err.code !== "ENOENT") throw err;
2090
+ }
2091
+ const root = fromMarkdown(content);
2092
+ let index = root.children.findIndex((node) => node.type === "heading" && node.depth === 1);
2093
+ if (index === -1) {
2094
+ index = 0;
2095
+ root.children.unshift({
2096
+ type: "heading",
2097
+ depth: 1,
2098
+ children: [{
2099
+ type: "text",
2100
+ value: defaultChangelogTitle
2101
+ }]
2102
+ });
2103
+ }
2104
+ root.children.splice(index + 1, 0, ...fromMarkdown(revision).children);
2105
+ content = toMarkdown(root);
2106
+ content = await formatFile(filePath, content);
2107
+ await fs.writeFile(filePath, content);
2108
+ }
2109
+ //#endregion
2110
+ //#region src/workflow/changelog/writeChangelog.ts
2111
+ /**
2112
+ * Write changelog content to the file system based on the release plan.
2113
+ *
2114
+ * @param cwd Repository working directory
2115
+ * @param plan Release plan containing package and changelog content
2116
+ * @returns Promise that resolves when the changelog file has been written
2117
+ */
2118
+ async function writeChangelog(cwd, plan) {
2119
+ if (plan.release?.changelog) {
2120
+ const changelogFile = plan.package.changelogFile || "CHANGELOG.md";
2121
+ await prependChangelog(path.join(cwd, plan.package.dir, changelogFile), plan.release.changelog);
2122
+ }
2123
+ }
2124
+ //#endregion
2125
+ //#region package.json
2126
+ var version = "0.1.1";
2127
+ //#endregion
2128
+ //#region src/workflow/committing/generateReleaseCommitMessage.ts
2129
+ /**
2130
+ * Generate a formatted release commit message based on release plans and
2131
+ * custom trailers.
2132
+ *
2133
+ * Compiles package version updates and included commit logs into a standardized
2134
+ * conventional commit message body, appending configured release agent and
2135
+ * custom trailers. Returns null if no valid release subject can be derived.
2136
+ *
2137
+ * @param plans Readonly array of release plans containing package changes and commit history
2138
+ * @param trailers Optional record of custom git trailers to append to the commit
2139
+ * @returns The formatted release commit message string or null if the subject is empty
2140
+ */
2141
+ async function generateReleaseCommitMessage(plans, trailers = {}) {
2142
+ const standalone = plans.length === 1;
2143
+ trailers = {
2144
+ [GIT_TRAILER_RELEASE_AGENT]: `ReelOut/${/* istanbul ignore next */ version}`,
2145
+ ...trailers
2146
+ };
2147
+ plans = plans.filter((plan) => plan.release?.tag?.name).toSorted((a, b) => {
2148
+ if (a.package.entry) return -1;
2149
+ if (b.package.entry) return 1;
2150
+ /* istanbul ignore next */
2151
+ return (a.package.name ?? "").localeCompare(b.package.name ?? "");
2152
+ });
2153
+ let subject;
2154
+ if (plans.length === 1) subject = standalone ? plans[0]?.release?.version : plans[0]?.release?.tag?.name;
2155
+ else if (plans.length >= 2 && plans.length <= 3) subject = plans.map((plan) => plan.release?.tag?.name).join(", ");
2156
+ else if (plans.length > 3) subject = `${plans.length} packages (${plans.slice(0, 2).map((plan) => plan.release?.tag?.name).join(", ")}, +${plans.length - 2} more)`;
2157
+ if (!subject) return null;
2158
+ const packages = plans.map((plan) => {
2159
+ return `- ${plan.package.name || plan.package.dir}: ${plan.package.version} → ${plan.release?.version}`;
2160
+ });
2161
+ const commits = Array.from(new Map(plans.flatMap((plan) => plan.history).map((commit) => [commit.hash, commit])).values()).toSorted((a, b) => {
2162
+ return new Date(a.committerDate).valueOf() - new Date(b.committerDate).valueOf();
2163
+ }).map(({ header }) => `- ${header}`);
2164
+ return [
2165
+ `chore: release ${subject}`,
2166
+ packages.length >= 2 ? ["## Version Updates", ...packages].join("\n") : void 0,
2167
+ commits.length > 0 ? ["## Changes Included", ...commits].join("\n") : void 0,
2168
+ Object.entries(trailers).map(([key, value]) => key && value != null ? `${key}: ${value}` : "").filter(Boolean).join("\n")
2169
+ ].filter(Boolean).join("\n\n");
2170
+ }
2171
+ //#endregion
2172
+ //#region src/workflow/report/generateReleaseReport.ts
2173
+ /**
2174
+ * Generate a release report from the current workflow state.
2175
+ *
2176
+ * Extracts relevant information from the context and organizes it into a report
2177
+ * containing workflow phase, status, plugin metadata, packages, and release plans.
2178
+ *
2179
+ * @param context Release context with workflow state and plans
2180
+ * @param plugin Name of the current plugin executing
2181
+ * @param hook Current workflow phase
2182
+ * @param status Workflow completion status (`'success'` or `'failure'`)
2183
+ * @param reason Error or failure reason (used only when status is `'failure'`)
2184
+ * @returns Release report object with structured workflow results
2185
+ */
2186
+ function generateReleaseReport(context, plugin, hook, status, reason) {
2187
+ return {
2188
+ phase: plugin && hook ? {
2189
+ plugin,
2190
+ hook
2191
+ } : void 0,
2192
+ status,
2193
+ reason: Error.isError(reason) ? reason.message : typeof reason === "string" ? reason : null,
2194
+ prereleaseLabel: context.prereleaseLabel,
2195
+ releaseChannel: context.releaseChannel,
2196
+ plugins: context.plugins.map((plugin) => ({
2197
+ name: plugin.name,
2198
+ version: plugin.version,
2199
+ hooks: Object.keys(plugin).filter((key) => key !== "name" && key !== "version")
2200
+ })),
2201
+ packages: context.packages.map((pkg) => ({
2202
+ dir: pkg.dir,
2203
+ name: pkg.name,
2204
+ version: pkg.version
2205
+ })),
2206
+ plans: compact(context.plans.map((plan) => {
2207
+ if (!plan.release?.version) return null;
2208
+ return {
2209
+ package: {
2210
+ entry: plan.package.entry,
2211
+ dir: plan.package.dir,
2212
+ name: plan.package.name,
2213
+ version: plan.package.version,
2214
+ private: !!plan.package.private
2215
+ },
2216
+ release: {
2217
+ type: plan.release.type,
2218
+ version: plan.release.version,
2219
+ tag: plan.release.tag?.name,
2220
+ changelog: plan.release.changelog
2221
+ }
2222
+ };
2223
+ }))
2224
+ };
2225
+ }
2226
+ //#endregion
2227
+ //#region src/workflow/report/printReleaseReport.ts
2228
+ /**
2229
+ * Print a formatted release report to the logger.
2230
+ *
2231
+ * Logs the workflow status and details about released packages. On success,
2232
+ * displays released package names and version changes. On failure, displays
2233
+ * the error reason.
2234
+ *
2235
+ * @param context Release context for logging
2236
+ * @param report Release report to display
2237
+ * @returns void
2238
+ */
2239
+ function printReleaseReport(context, report) {
2240
+ const statusLabel = {
2241
+ success: colors.green("SUCCESS"),
2242
+ failure: colors.red("FAILURE")
2243
+ }[report.status];
2244
+ context.logger.info(`Status: ${statusLabel}`);
2245
+ if (report.status === "failure") return context.logger.error(["Release complete:", colors.red(report.reason || "An unexpected error occurred during the release process.")].join(" "));
2246
+ if (report.plans.length === 0) return context.logger.info(["Release complete:", colors.yellow("No packages are scheduled for release.")].join(" "));
2247
+ const header = [
2248
+ "Release complete:",
2249
+ colors.bold(colors.green(`${report.plans.length} released`)),
2250
+ colors.gray(`(${report.packages.length})`)
2251
+ ].join(" ");
2252
+ const list = report.plans.toSorted((a, b) => (a.package.name || "").localeCompare(b.package.name || "")).map((plan) => {
2253
+ let dir = path.normalize(path.join(plan.package.dir, "."));
2254
+ dir = dir === "." ? dir : `./${dir}`;
2255
+ const name = plan.package.name ? colors.bgBlue(` ${plan.package.name} `) : !plan.package.entry && colors.gray(` ${dir} `);
2256
+ let version = [plan.release.version, plan.package.version].filter(Boolean).join(" ← ");
2257
+ version = ` ${version} `;
2258
+ const type = plan.release.type && colors.gray(` ${plan.release.type} `);
2259
+ return [[
2260
+ name,
2261
+ plan.release.type === "major" ? colors.bgRed(version) : colors.bgGreen(version),
2262
+ type
2263
+ ].filter(Boolean).join(""), plan.release.changelog?.replace(/^(#+ .+)$/gm, (_, header) => colors.cyan(header)).trim()].filter(Boolean).join("\n\n");
2264
+ }).join("\n\n");
2265
+ context.logger.info(`${header}\n\n${list}\n`);
2266
+ }
2267
+ //#endregion
2268
+ //#region src/workflow/runner/runWorkflow.ts
2269
+ /**
2270
+ * Execute the plugin-driven release workflow using the provided `ReleaseContext`.
2271
+ *
2272
+ * The workflow runs plugin stages in order: `configure`, `discover`,
2273
+ * `analyze`, `build`, `write`, `commit`, and `publish`. After a successful
2274
+ * run the function sets `context.status` to `'success'`. The workflow is
2275
+ * finalized in all cases; on success a release report is printed.
2276
+ *
2277
+ * The returned array contains all computed release plans, including plans
2278
+ * without releases. Side effects (file writes, commits, tags, publishing)
2279
+ * are performed by plugins and may be skipped when `context.dryRun` is true.
2280
+ *
2281
+ * @returns Promise that resolves when the workflow is complete.
2282
+ */
2283
+ async function runWorkflow(createContext) {
2284
+ const context = await createContext();
2285
+ const stack = [];
2286
+ let plugin;
2287
+ let hook;
2288
+ let status = "success";
2289
+ let reason = null;
2290
+ try {
2291
+ const ordered = RELEASE_LIFECYCLE_HOOK_LIST.flatMap((hookName) => {
2292
+ return compact(context.plugins.map((plugin) => {
2293
+ let hook = plugin[hookName];
2294
+ if (!hook) return void 0;
2295
+ hook = typeof hook === "function" ? { handler: hook } : hook;
2296
+ const { enforce, handler } = hook;
2297
+ return {
2298
+ plugin,
2299
+ hook: hookName,
2300
+ handler,
2301
+ weight: enforce === "pre" ? -1 : enforce === "post" ? 1 : 0
2302
+ };
2303
+ })).sort((a, b) => a.weight - b.weight);
2304
+ });
2305
+ let handler;
2306
+ for ({plugin, hook, handler} of ordered) {
2307
+ const label = `${colors.gray(`[${hook}]`)} ${plugin.name}`;
2308
+ let startedLogging = false;
2309
+ let timer;
2310
+ timer = setTimeout(() => {
2311
+ startedLogging = true;
2312
+ context.logger.start(label);
2313
+ }, 10);
2314
+ const startTime = performance.now();
2315
+ try {
2316
+ const rollback = await handler(context);
2317
+ if (typeof rollback === "function") stack.push(rollback);
2318
+ } finally {
2319
+ clearTimeout(timer);
2320
+ }
2321
+ const duration = (performance.now() - startTime) / 1e3;
2322
+ /* istanbul ignore next */
2323
+ if (startedLogging) {
2324
+ const time = (duration >= 5 ? colors.red : colors.cyan)(`${duration.toFixed(2)}s`);
2325
+ context.logger.success(`${label} ${time}`);
2326
+ }
2327
+ }
2328
+ } catch (err) {
2329
+ status = "failure";
2330
+ reason = err;
2331
+ for (const rollback of stack.toReversed()) try {
2332
+ await rollback(reason);
2333
+ } catch {}
2334
+ throw err;
2335
+ } finally {
2336
+ printReleaseReport(context, generateReleaseReport(context, plugin?.name, hook, status, reason));
2337
+ }
2338
+ }
2339
+ //#endregion
2340
+ export { GIT_PRETTY_FORMAT as $, ensureGitTag as A, deriveReleaseType as At, showParsedGitHistory as B, buildDependencyGraph as C, isSemVerGitTag as Ct, getGitTagMessage as D, RELEASE_LIFECYCLE_HOOK_LIST as Dt, getLatestGitTag as E, GIT_TRAILER_RELEASE_AGENT as Et, resolveGitRepository as F, detectGitRelationship as G, restoreFullGitHistory as H, pushGitChanges as I, withShallowLock as J, expandGitHistory as K, getGitRemote as L, deleteGitTag as M, determineReleaseType as Mt, createGitTag as N, bumpVersion as Nt, getCurrentGitTag as O, SEMVER_REGEX as Ot, readGitFile as P, GIT_PRETTY_FIELD_SEPARATOR as Q, ensureGitUserConfig as R, extractPlainText as S, buildTrailerPattern as St, isGitTagConnected as T, DEFAULT_CHANGELOG_FILE as Tt, mergeGitHistory as U, showGitHistory as V, GIT_REL_FLAGS as W, countGitHistory as X, isGitShallowRepository as Y, getParsedCommit as Z, discoverPackageTag as _, formatFile as _t, version as a, extractMentions as at, iterateChangelogVersions as b, compact as bt, extractPreviousVersion as c, promotePrereleaseVersions as ct, streamPackageCommits as d, resolveReleaseTarget as dt, parseGitPrettyFormat as et, sliceUnreleased as f, sanitizePrereleaseLabel as ft, resolvePackageStableTag as g, interpolate as gt, resolvePackageTag as h, isMatch as ht, generateReleaseCommitMessage as i, resolveGitRevision as it, isGitTagPresent as j, pickPriorityReleaseType as jt, generateGitTagName as k, isStableVersion as kt, buildReleaseNotes as l, analyzePackageChanges as lt, propagateDependencyUpdates as m, readJson as mt, printReleaseReport as n, TTLCache as nt, writeChangelog as o, createGitCommit as ot, isReleaseCommit as p, writeJson as pt, isGitAncestor as q, generateReleaseReport as r, getCurrentGitCommit as rt, prependChangelog as s, getCurrentGitBranch as st, runWorkflow as t, parseCommit as tt, buildChangelog as u, isCommitForPackage as ut, verifyPackageTag as v, execGit as vt, isGitTagPointed as w, ALL_FILES as wt, extractSemVer as x, isDefined as xt, extractLatestVersion as y, execCommand as yt, getGitConfigValue as z };
2341
+
2342
+ //# sourceMappingURL=workflow-BoVmbvH6.js.map