@qualcomm-ui/changesets-cli 1.2.0 → 2.0.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.
package/dist/cli.js ADDED
@@ -0,0 +1,1003 @@
1
+ import { program } from "@commander-js/extra-typings";
2
+ import { stdin, stdout } from "node:process";
3
+ import { createInterface } from "node:readline/promises";
4
+ import * as core from "@actions/core";
5
+ import { getPackages, getPackagesSync } from "@manypkg/get-packages";
6
+ import { readFile, readdir, writeFile } from "node:fs/promises";
7
+ import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
+ import dayjs from "dayjs";
9
+ import advancedFormat from "dayjs/plugin/advancedFormat.js";
10
+ import { execFileSync, execSync } from "node:child_process";
11
+ import { Octokit } from "@octokit/rest";
12
+ import { tmpdir } from "node:os";
13
+ import readChangeset from "@changesets/read";
14
+ import writeChangeset from "@changesets/write";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { Node, Project, ts } from "ts-morph";
17
+ //#region src/publishable-packages.ts
18
+ var DEFAULT_CONFIG_LOCATION = join(".changeset", "config.json");
19
+ /**
20
+ * Retrieves all packages in the monorepo that should be published.
21
+ * Filters out packages marked as private and ignored packages.
22
+ *
23
+ * @param configPath - Path to the changesets config file, relative to cwd
24
+ * @param cwd - Working directory (defaults to process.cwd())
25
+ * @returns Array of package objects that are eligible for publishing
26
+ */
27
+ async function getPublishablePackages(configPath, cwd = process.cwd()) {
28
+ const { packages } = await getPackages(cwd);
29
+ const changesetConfig = JSON.parse(await readFile(resolve(cwd, configPath ?? DEFAULT_CONFIG_LOCATION), "utf-8"));
30
+ const ignoredPackages = new Set(changesetConfig.ignored ?? []);
31
+ return packages.filter((pkg) => {
32
+ if (ignoredPackages.has(pkg.packageJson.name)) return false;
33
+ if (!pkg.packageJson.version) return false;
34
+ return !pkg.packageJson.private;
35
+ });
36
+ }
37
+ //#endregion
38
+ //#region src/check-versions.ts
39
+ async function getPublishedVersion(packageName) {
40
+ const response = await fetch(`https://registry.npmjs.org/${packageName}`);
41
+ if (response.status === 404) return null;
42
+ if (!response.ok) throw new Error(`Failed to fetch ${packageName}: ${response.status}`);
43
+ return (await response.json())["dist-tags"]?.latest || null;
44
+ }
45
+ function compareVersions(current, published) {
46
+ const [cMajor, cMinor, cPatch] = current.split(".").map(Number);
47
+ const [pMajor, pMinor, pPatch] = published.split(".").map(Number);
48
+ if (cMajor !== pMajor) return cMajor - pMajor;
49
+ if (cMinor !== pMinor) return cMinor - pMinor;
50
+ return cPatch - pPatch;
51
+ }
52
+ async function checkVersions(options) {
53
+ const packages = await getPublishablePackages(options?.configPath);
54
+ const newer = (await Promise.all(packages.map(async (pkg) => {
55
+ const { packageJson } = pkg;
56
+ const { name, version } = packageJson;
57
+ const published = await getPublishedVersion(name);
58
+ if (!published) return {
59
+ name,
60
+ unpublished: true,
61
+ version
62
+ };
63
+ const comparison = compareVersions(version, published);
64
+ return {
65
+ current: version,
66
+ isNewer: comparison > 0,
67
+ isOlder: comparison < 0,
68
+ isSame: comparison === 0,
69
+ name,
70
+ published
71
+ };
72
+ }))).filter((r) => "isNewer" in r && r.isNewer === true);
73
+ if (newer.length > 0) {
74
+ console.log("The following packages will be published:");
75
+ newer.forEach((r) => console.log(` ${r.name}: ${r.published} -> ${r.current}`));
76
+ core.setOutput("should-publish", true);
77
+ } else {
78
+ console.log("No packages need publishing.");
79
+ core.setOutput("should-publish", false);
80
+ }
81
+ }
82
+ //#endregion
83
+ //#region src/consolidate-changelogs.ts
84
+ dayjs.extend(advancedFormat);
85
+ var DATE_FORMAT = "MMM Do, YYYY";
86
+ function getChangedChangelogs() {
87
+ try {
88
+ return execSync("git diff --name-only HEAD", { encoding: "utf-8" }).trim().split("\n").filter(Boolean).filter((file) => file.endsWith("CHANGELOG.md"));
89
+ } catch {
90
+ console.error("Failed to get changed files from git diff");
91
+ process.exit(1);
92
+ }
93
+ }
94
+ async function consolidateChangelog(changelogPath) {
95
+ const lines = (await readFile(changelogPath, "utf-8")).split("\n");
96
+ const firstReleaseIndex = lines.findIndex((line) => line.startsWith("## "));
97
+ if (firstReleaseIndex === -1) return;
98
+ const secondReleaseIndex = lines.findIndex((line, i) => i > firstReleaseIndex && line.startsWith("## "));
99
+ const endIndex = secondReleaseIndex === -1 ? lines.length : secondReleaseIndex;
100
+ const before = lines.slice(0, firstReleaseIndex);
101
+ const releaseLines = lines.slice(firstReleaseIndex, endIndex);
102
+ const after = lines.slice(endIndex);
103
+ const sections = /* @__PURE__ */ new Map();
104
+ let versionLine = "";
105
+ let dateLine = "";
106
+ let currentSection = "";
107
+ for (const line of releaseLines) {
108
+ if (line.startsWith("## ")) {
109
+ const dateMatch = line.match(/\((\d{4}\/\d{2}\/\d{2})\)/);
110
+ if (dateMatch) {
111
+ versionLine = line.replace(` (${dateMatch[1]})`, "");
112
+ dateLine = dayjs(dateMatch[1]).format(DATE_FORMAT);
113
+ } else {
114
+ versionLine = line;
115
+ dateLine = dayjs().format(DATE_FORMAT);
116
+ }
117
+ continue;
118
+ }
119
+ if (line.startsWith("### Patch Changes") || line.startsWith("### Minor Changes") || line.startsWith("### Major Changes")) continue;
120
+ if (line.startsWith("### ")) {
121
+ currentSection = line;
122
+ if (!sections.has(currentSection)) sections.set(currentSection, []);
123
+ continue;
124
+ }
125
+ if (currentSection && line.trim()) sections.get(currentSection)?.push(line.trim());
126
+ }
127
+ const output = [];
128
+ output.push(versionLine);
129
+ output.push("");
130
+ output.push(dateLine);
131
+ output.push("");
132
+ for (const [section, items] of sections) {
133
+ if (items.length === 0) continue;
134
+ output.push(section);
135
+ output.push("");
136
+ output.push(...items);
137
+ output.push("");
138
+ }
139
+ await writeFile(changelogPath, [
140
+ ...before,
141
+ ...output,
142
+ ...after
143
+ ].join("\n"));
144
+ }
145
+ async function consolidateChangelogs() {
146
+ const changedChangelogs = getChangedChangelogs();
147
+ if (changedChangelogs.length === 0) {
148
+ console.log("No changelogs changed");
149
+ return;
150
+ }
151
+ console.log(`Consolidating ${changedChangelogs.length} changelog(s)...`);
152
+ for (const changelogPath of changedChangelogs) {
153
+ console.log(` - ${changelogPath}`);
154
+ await consolidateChangelog(changelogPath);
155
+ }
156
+ console.log("Done");
157
+ }
158
+ //#endregion
159
+ //#region src/create-github-releases.ts
160
+ /**
161
+ * Parses a changelog file to extract the latest version entry.
162
+ *
163
+ * @param path - Path to the CHANGELOG.md file
164
+ * @returns Object containing version, date, and body of the latest entry, or null if parsing fails
165
+ */
166
+ async function parseChangelog(path) {
167
+ const lines = (await readFile(path, "utf-8")).split("\n");
168
+ const firstVersionIndex = lines.findIndex((l) => l.startsWith("## "));
169
+ if (firstVersionIndex === -1) {
170
+ console.log(` No version header found`);
171
+ return null;
172
+ }
173
+ const headerLine = lines[firstVersionIndex];
174
+ const match = headerLine.match(/^## ([\d.]+)/);
175
+ if (!match) {
176
+ console.log(` Invalid version format: ${headerLine}`);
177
+ return null;
178
+ }
179
+ const [, version, date] = match;
180
+ const endIndex = lines.findIndex((l, i) => i > firstVersionIndex && l.startsWith("## "));
181
+ return {
182
+ body: lines.slice(firstVersionIndex + 1, endIndex === -1 ? void 0 : endIndex).join("\n").trim(),
183
+ date,
184
+ version
185
+ };
186
+ }
187
+ function getRepoFromGitRemote() {
188
+ const remoteUrl = execSync("git remote get-url origin").toString().trim();
189
+ const match = remoteUrl.match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
190
+ if (!match) throw new Error(`Could not parse GitHub owner/repo from remote URL: ${remoteUrl}, use --repo to specify explicitly.`);
191
+ return {
192
+ owner: match[1],
193
+ repo: match[2]
194
+ };
195
+ }
196
+ async function createGitHubReleases(options) {
197
+ const octokit = new Octokit({ auth: options.token });
198
+ const repoOpts = options.repo ? {
199
+ owner: options.repo.split("/")[0],
200
+ repo: options.repo.split("/")[1]
201
+ } : getRepoFromGitRemote();
202
+ const packages = await getPublishablePackages(options.configPath);
203
+ for (const pkg of packages) {
204
+ const changelog = await parseChangelog(join(pkg.dir, "CHANGELOG.md")).catch(() => null);
205
+ if (!changelog) {
206
+ console.warn("no changelog found, skipping package:", pkg.packageJson.name);
207
+ continue;
208
+ }
209
+ if (changelog.version !== pkg.packageJson.version) {
210
+ console.log(`Skipping ${pkg.packageJson.name}: changelog ${changelog.version} !== package.json ${pkg.packageJson.version}`);
211
+ continue;
212
+ }
213
+ const tag = `${pkg.packageJson.name}@${changelog.version}`;
214
+ const release = await octokit.repos.getReleaseByTag({
215
+ ...repoOpts,
216
+ tag
217
+ }).catch(() => null);
218
+ if (release) {
219
+ console.log(`Release \x1b[93m${release.data.name}\x1b[0m already exists, skipping`);
220
+ continue;
221
+ }
222
+ console.log(`Creating release: \x1b[96m${tag}\x1b[0m`);
223
+ await octokit.repos.createRelease({
224
+ ...repoOpts,
225
+ body: changelog.body,
226
+ name: tag,
227
+ tag_name: tag
228
+ });
229
+ }
230
+ }
231
+ //#endregion
232
+ //#region src/generate-release-notes.ts
233
+ var RELEASE_NOTES_FILENAME = "release-notes.md";
234
+ function extractLatestEntry(content, fallbackName) {
235
+ const lines = content.split("\n");
236
+ const packageName = (lines[0]?.match(/^# (.+) Changelog$/))?.[1] ?? fallbackName;
237
+ const firstVersionIndex = lines.findIndex((l) => l.startsWith("## "));
238
+ if (firstVersionIndex === -1) return null;
239
+ const versionMatch = lines[firstVersionIndex].match(/^## ([\d.]+)/);
240
+ if (!versionMatch) return null;
241
+ const version = versionMatch[1];
242
+ const endIndex = lines.findIndex((l, i) => i > firstVersionIndex && l.startsWith("## "));
243
+ const bodyLines = lines.slice(firstVersionIndex + 1, endIndex === -1 ? void 0 : endIndex);
244
+ const date = bodyLines.find((l) => /^[A-Z][a-z]{2} \d/.test(l.trim()))?.trim() ?? "";
245
+ return {
246
+ date,
247
+ packageName,
248
+ sections: bodyLines.filter((l) => l.trim() !== date).join("\n").trim(),
249
+ version
250
+ };
251
+ }
252
+ function isDepsOnlyEntry(sections) {
253
+ const lines = sections.split("\n").filter((l) => l.trim());
254
+ const hasOnlyChoresHeader = lines.filter((l) => l.startsWith("### ")).every((l) => l === "### Miscellaneous Chores");
255
+ const bulletLines = lines.filter((l) => l.startsWith("- "));
256
+ const allDepsBullets = bulletLines.every((l) => l.includes("**deps:** update dependencies"));
257
+ return hasOnlyChoresHeader && allDepsBullets && bulletLines.length > 0;
258
+ }
259
+ function getReleaseNotesPath() {
260
+ return join(tmpdir(), RELEASE_NOTES_FILENAME);
261
+ }
262
+ async function generateReleaseNotes() {
263
+ const changedChangelogs = getChangedChangelogs();
264
+ if (changedChangelogs.length === 0) {
265
+ console.log("No changed changelogs, skipping release notes generation");
266
+ return "";
267
+ }
268
+ const entries = [];
269
+ for (const changelogPath of changedChangelogs) {
270
+ const entry = extractLatestEntry(await readFile(changelogPath, "utf-8"), changelogPath);
271
+ if (entry) entries.push(entry);
272
+ }
273
+ if (entries.length === 0) {
274
+ console.log("No parseable changelog entries found");
275
+ return "";
276
+ }
277
+ const substantive = entries.filter((e) => !isDepsOnlyEntry(e.sections));
278
+ const depsOnly = entries.filter((e) => isDepsOnlyEntry(e.sections));
279
+ const lines = ["# Release Notes", ""];
280
+ for (const entry of substantive) lines.push(`## ${entry.packageName} — ${entry.version} (${entry.date})`, "", entry.sections, "");
281
+ if (depsOnly.length > 0) {
282
+ lines.push("---", "", "<details>", "<summary>Dependency-only updates</summary>", "");
283
+ for (const entry of depsOnly) lines.push(`- ${entry.packageName} — ${entry.version}`);
284
+ lines.push("", "</details>", "");
285
+ }
286
+ const markdown = lines.join("\n");
287
+ const outPath = getReleaseNotesPath();
288
+ await writeFile(outPath, markdown);
289
+ console.log(`Release notes written to ${outPath}`);
290
+ console.log(` ${substantive.length} package(s) with changes, ${depsOnly.length} dependency-only update(s)`);
291
+ return outPath;
292
+ }
293
+ //#endregion
294
+ //#region src/utils.ts
295
+ var defaultCommitTypes = [
296
+ {
297
+ section: "Features",
298
+ type: "feat"
299
+ },
300
+ {
301
+ section: "Features",
302
+ type: "feature"
303
+ },
304
+ {
305
+ section: "Bug Fixes",
306
+ type: "fix"
307
+ },
308
+ {
309
+ section: "Performance Improvements",
310
+ type: "perf"
311
+ },
312
+ {
313
+ section: "Reverts",
314
+ type: "revert"
315
+ },
316
+ {
317
+ section: "Documentation",
318
+ type: "docs"
319
+ },
320
+ {
321
+ section: "Styles",
322
+ type: "style"
323
+ },
324
+ {
325
+ section: "Styles",
326
+ type: "styles"
327
+ },
328
+ {
329
+ section: "Miscellaneous Chores",
330
+ type: "chore"
331
+ },
332
+ {
333
+ section: "Code Refactoring",
334
+ type: "refactor"
335
+ },
336
+ {
337
+ section: "Tests",
338
+ type: "test"
339
+ },
340
+ {
341
+ section: "Build System",
342
+ type: "build"
343
+ },
344
+ {
345
+ section: "Continuous Integration",
346
+ type: "ci"
347
+ }
348
+ ];
349
+ /**
350
+ * Normalizes conventional commit format by removing whitespace between type and
351
+ * scope. Transforms "fix (scope): message" to "fix(scope): message"
352
+ * @param commit - The commit message to normalize
353
+ * @returns Normalized commit message
354
+ */
355
+ function normalizeConventionalCommit(commit) {
356
+ const normalized = commit.replace(/^(\w+)\s+(\(.*?\))/, "$1$2");
357
+ if (normalized.startsWith("- ")) return normalized.substring(2);
358
+ return normalized;
359
+ }
360
+ /**
361
+ * Checks if a commit message follows conventional commit format
362
+ * @param commit - The commit message to check
363
+ * @returns True if the commit follows conventional commit format
364
+ */
365
+ function isConventionalCommit(commit) {
366
+ const normalized = normalizeConventionalCommit(commit);
367
+ return defaultCommitTypes.some((commitType) => normalized.match(new RegExp(`^(?:-\\s)?${commitType.type}\\s*(?:\(.*\))?!?:`)));
368
+ }
369
+ /**
370
+ * Checks if a commit message indicates a breaking change
371
+ * @param commit - The commit message to check
372
+ * @returns True if the commit contains a breaking change indicator
373
+ */
374
+ function isBreakingChange(commit) {
375
+ const normalized = normalizeConventionalCommit(commit);
376
+ return normalized.includes("BREAKING CHANGE:") || defaultCommitTypes.some((commitType) => normalized.match(new RegExp(`^${commitType.type}\\s*(?:\(.*\))?!:`)));
377
+ }
378
+ /**
379
+ * Filters commits to only conventional commits and maps them to changelog format
380
+ * @param commits - Array of commits to translate
381
+ * @returns Array of conventional commit messages with their associated commit hashes
382
+ */
383
+ function translateCommitsToConventionalCommitMessages(commits) {
384
+ return commits.filter((commit) => isConventionalCommit(commit.commitMessage)).map((commit) => ({
385
+ changelogMessage: normalizeConventionalCommit(commit.commitMessage),
386
+ commitHashes: [commit.commitHash]
387
+ }));
388
+ }
389
+ /**
390
+ * Gets the list of files changed between two git refs
391
+ * @param opts - Object containing from and to git refs
392
+ * @returns Array of file paths that changed
393
+ */
394
+ function getFilesChangedSince(opts) {
395
+ return execSync(`git diff --name-only ${opts.from}~1...${opts.to}`).toString().trim().split("\n");
396
+ }
397
+ /**
398
+ * Gets the absolute path to the git repository root
399
+ * @returns Absolute path to repository root
400
+ */
401
+ function getRepoRoot() {
402
+ return execSync("git rev-parse --show-toplevel").toString().trim().replace(/\n|\r/g, "");
403
+ }
404
+ /**
405
+ * Gets the full commit message for a given commit hash
406
+ * @param commitHash - The git commit hash
407
+ * @returns The full commit message
408
+ */
409
+ function getCommitMessage(commitHash) {
410
+ return execSync(`git log -1 --pretty=%B ${commitHash}`).toString().trim();
411
+ }
412
+ /**
413
+ * Extracts all conventional commit messages from a multi-line commit message
414
+ * @param commitMessage - The commit message to parse
415
+ * @returns Array of conventional commit messages found
416
+ */
417
+ function extractConventionalCommits(commitMessage) {
418
+ const lines = commitMessage.split("\n");
419
+ const conventionalCommits = [];
420
+ for (const line of lines) {
421
+ const trimmed = line.trim();
422
+ if (trimmed && isConventionalCommit(trimmed)) conventionalCommits.push(normalizeConventionalCommit(trimmed));
423
+ }
424
+ return conventionalCommits;
425
+ }
426
+ /**
427
+ * Converts conventional commit messages to changesets based on affected packages
428
+ * @param conventionalMessagesToCommits - Array of conventional messages with their commit hashes
429
+ * @param options - Configuration options including ignored files and packages
430
+ * @returns Array of changesets for affected packages
431
+ */
432
+ function conventionalMessagesWithCommitsToChangesets(conventionalMessagesToCommits, options) {
433
+ const { ignoredFiles = [], includeCommitLinks, packages } = options;
434
+ const repoRoot = getRepoRoot();
435
+ return conventionalMessagesToCommits.flatMap((entry) => {
436
+ const filesChanged = getFilesChangedSince({
437
+ from: entry.commitHashes[0],
438
+ to: entry.commitHashes[entry.commitHashes.length - 1]
439
+ }).filter((file) => {
440
+ return ignoredFiles.every((ignoredPattern) => !file.match(ignoredPattern));
441
+ });
442
+ const packagesChanged = packages.filter((pkg) => {
443
+ const pkgPath = pkg.dir.replace(/\\/g, "/").replace(`${repoRoot}/`, "");
444
+ return filesChanged.some((file) => file.startsWith(`${pkgPath}/`));
445
+ });
446
+ if (packagesChanged.length === 0) return [];
447
+ return entry.commitHashes.flatMap((hash) => {
448
+ return extractConventionalCommits(getCommitMessage(hash)).map((msg) => ({
449
+ hash,
450
+ message: msg
451
+ }));
452
+ }).filter((item, index, self) => index === self.findIndex((other) => other.message === item.message)).flatMap(({ hash, message: conventionalCommit }) => {
453
+ const changeType = isBreakingChange(conventionalCommit) ? "major" : conventionalCommit.startsWith("feat") ? "minor" : "patch";
454
+ return {
455
+ packagesChanged,
456
+ releases: packagesChanged.map((pkg) => ({
457
+ name: pkg.packageJson.name,
458
+ type: changeType
459
+ })),
460
+ summary: includeCommitLinks ? `${conventionalCommit}\n\ncommit: ${hash.slice(0, 7)}` : conventionalCommit
461
+ };
462
+ });
463
+ }).filter(Boolean);
464
+ }
465
+ /**
466
+ * Fetches the latest changes from a remote branch
467
+ * @param branch - The branch name to fetch
468
+ */
469
+ function gitFetch(branch) {
470
+ execSync(`git fetch origin ${branch}`);
471
+ }
472
+ /**
473
+ * Gets all commit hashes since a specific commit
474
+ * @param sha - The commit SHA to compare against
475
+ * @returns Array of commit hashes since the given commit
476
+ */
477
+ function getCommitsSinceCommit(sha) {
478
+ return execSync(`git rev-list ${sha}..HEAD`).toString().split("\n").filter(Boolean).reverse();
479
+ }
480
+ /**
481
+ * Gets all commit hashes since a reference branch or tag
482
+ * @param branch - The branch to compare against
483
+ * @returns Array of commit hashes since the reference point
484
+ */
485
+ function getCommitsSinceBranch(branch) {
486
+ gitFetch(branch);
487
+ return execSync(`git rev-list ${`origin/${branch}`}..HEAD`).toString().split("\n").filter(Boolean).reverse();
488
+ }
489
+ /**
490
+ * Compares two changesets for equality
491
+ * @param a - First changeset
492
+ * @param b - Second changeset
493
+ * @returns True if changesets are equal
494
+ */
495
+ function compareChangeSet(a, b) {
496
+ return a.summary.replace(/\n$/, "") === b.summary && JSON.stringify(a.releases) === JSON.stringify(b.releases);
497
+ }
498
+ /**
499
+ * Returns changesets in array a that are not in array b
500
+ * @param a - Array of changesets to filter
501
+ * @param b - Array of changesets to compare against
502
+ * @returns Changesets that exist in a but not in b
503
+ */
504
+ function difference(a, b) {
505
+ return a.filter((changeA) => !b.some((changeB) => compareChangeSet(changeA, changeB)));
506
+ }
507
+ //#endregion
508
+ //#region src/main.ts
509
+ var CHANGESET_CONFIG_LOCATION$1 = join(".changeset", "config.json");
510
+ function getCommitsWithMessages(commitHashes) {
511
+ return commitHashes.map((commitHash) => {
512
+ return {
513
+ commitHash,
514
+ commitMessage: execSync(`git log -n 1 --pretty=format:%B ${commitHash}`).toString()
515
+ };
516
+ });
517
+ }
518
+ async function conventionalCommitChangeset(options, cwd = process.cwd()) {
519
+ const configLocation = options.configPath ?? CHANGESET_CONFIG_LOCATION$1;
520
+ const changesetConfig = JSON.parse(readFileSync(join(cwd, configLocation)).toString());
521
+ const ignored = changesetConfig.ignore ?? [];
522
+ const packages = getPackagesSync(cwd).packages.filter((pkg) => Boolean(pkg.packageJson.version) && !ignored.includes(pkg.packageJson.name));
523
+ const { baseBranch = "main" } = changesetConfig;
524
+ const { commitSha, includeCommitLinks } = options;
525
+ const changesets = conventionalMessagesWithCommitsToChangesets(translateCommitsToConventionalCommitMessages(getCommitsWithMessages(commitSha ? getCommitsSinceCommit(commitSha) : getCommitsSinceBranch(baseBranch))), {
526
+ ignoredFiles: ignored,
527
+ includeCommitLinks,
528
+ packages
529
+ });
530
+ const currentChangesets = await readChangeset(cwd);
531
+ const newChangesets = currentChangesets.length === 0 ? changesets : difference(changesets, currentChangesets);
532
+ await Promise.all(newChangesets.map((changeset) => writeChangeset(changeset, cwd)));
533
+ }
534
+ //#endregion
535
+ //#region src/update-jsdoc-since-tags.ts
536
+ var NEXT_RELEASE = "next-release";
537
+ var NEXT_RELEASE_TAG_PATTERN = /@since\s+next-release\b/;
538
+ var SOURCE_EXTENSIONS = new Set([
539
+ ".cjs",
540
+ ".cts",
541
+ ".js",
542
+ ".jsx",
543
+ ".mjs",
544
+ ".mts",
545
+ ".ts",
546
+ ".tsx"
547
+ ]);
548
+ var EXCLUDED_DIRECTORIES = new Set([
549
+ ".turbo",
550
+ "__tests__",
551
+ "build",
552
+ "coverage",
553
+ "dist",
554
+ "lib",
555
+ "node_modules"
556
+ ]);
557
+ var TEST_FILE_PATTERN = /\.(spec|test)\.[cm]?[jt]sx?$/;
558
+ function pluralize(count, singular, plural) {
559
+ return count === 1 ? singular : plural;
560
+ }
561
+ function formatJsDocSinceUpdateStartMessage() {
562
+ return "Updating JSDoc @since tags...";
563
+ }
564
+ function formatJsDocSincePackageUpdateProgress({ name, version }) {
565
+ return `Processing package ${name} ${version}...`;
566
+ }
567
+ function normalizeRelativePath(cwd, filePath) {
568
+ return relative(cwd, filePath).split(sep).join("/");
569
+ }
570
+ function hasNextReleaseSinceTag(content) {
571
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, content);
572
+ let token = scanner.scan();
573
+ while (token !== ts.SyntaxKind.EndOfFileToken) {
574
+ if (token === ts.SyntaxKind.MultiLineCommentTrivia) {
575
+ const text = scanner.getTokenText();
576
+ if (text.startsWith("/**") && NEXT_RELEASE_TAG_PATTERN.test(text)) return true;
577
+ }
578
+ token = scanner.scan();
579
+ }
580
+ return false;
581
+ }
582
+ function getNodeName(node) {
583
+ if (Node.isClassDeclaration(node) || Node.isEnumDeclaration(node) || Node.isFunctionDeclaration(node) || Node.isInterfaceDeclaration(node) || Node.isMethodDeclaration(node) || Node.isMethodSignature(node) || Node.isPropertyDeclaration(node) || Node.isPropertySignature(node) || Node.isTypeAliasDeclaration(node)) return node.getName();
584
+ if (Node.isVariableStatement(node)) return node.getDeclarationList().getDeclarations().map((declaration) => declaration.getName()).join(", ");
585
+ }
586
+ function getQualifiedEntityName(node) {
587
+ const name = getNodeName(node);
588
+ if (!name) return "unknown";
589
+ const parent = node.getParent();
590
+ const parentName = parent ? getNodeName(parent) : void 0;
591
+ if (parentName) return `${parentName}.${name}`;
592
+ return name;
593
+ }
594
+ function getVersionedPackages(packages) {
595
+ return packages.flatMap((pkg) => {
596
+ if (!pkg.version) return [];
597
+ return [{
598
+ name: pkg.name,
599
+ root: pkg.root,
600
+ version: pkg.version
601
+ }];
602
+ });
603
+ }
604
+ function isSourceFile(filePath) {
605
+ return SOURCE_EXTENSIONS.has(extname(filePath)) && !TEST_FILE_PATTERN.test(filePath);
606
+ }
607
+ async function getSourceFiles(dir) {
608
+ const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
609
+ const files = [];
610
+ for (const entry of entries) {
611
+ const entryPath = join(dir, entry.name);
612
+ if (entry.isDirectory()) {
613
+ if (!EXCLUDED_DIRECTORIES.has(entry.name)) files.push(...await getSourceFiles(entryPath));
614
+ continue;
615
+ }
616
+ if (entry.isFile() && isSourceFile(entry.name)) files.push(entryPath);
617
+ }
618
+ return files;
619
+ }
620
+ async function updateSourceFile(filePath, version) {
621
+ const sourceFile = new Project({
622
+ compilerOptions: {
623
+ allowJs: true,
624
+ checkJs: false
625
+ },
626
+ manipulationSettings: {
627
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false,
628
+ useTrailingCommas: true
629
+ },
630
+ skipAddingFilesFromTsConfig: true
631
+ }).addSourceFileAtPath(filePath);
632
+ let tagCount = 0;
633
+ sourceFile.forEachDescendant((node) => {
634
+ if (!Node.isJSDocable(node)) return;
635
+ for (const jsDoc of node.getJsDocs()) for (const tag of jsDoc.getTags()) if (tag.getTagName() === "since" && tag.getCommentText()?.trim() === NEXT_RELEASE) {
636
+ tag.set({
637
+ tagName: "since",
638
+ text: version
639
+ });
640
+ tagCount += 1;
641
+ }
642
+ });
643
+ if (tagCount > 0) await sourceFile.save();
644
+ return tagCount;
645
+ }
646
+ async function findUnresolvedFiles(cwd, sourceFiles) {
647
+ const unresolvedFiles = [];
648
+ for (const filePath of sourceFiles) if (hasNextReleaseSinceTag(await readFile(filePath, "utf-8"))) unresolvedFiles.push(normalizeRelativePath(cwd, filePath));
649
+ return unresolvedFiles;
650
+ }
651
+ function collectSourceFileSinceTagLocations(cwd, sourceFile) {
652
+ const filePath = normalizeRelativePath(cwd, sourceFile.getFilePath());
653
+ const locations = [];
654
+ sourceFile.forEachDescendant((node) => {
655
+ if (!Node.isJSDocable(node)) return;
656
+ for (const jsDoc of node.getJsDocs()) for (const tag of jsDoc.getTags()) if (tag.getTagName() === "since" && tag.getCommentText()?.trim() === NEXT_RELEASE) locations.push({
657
+ entityName: getQualifiedEntityName(node),
658
+ filePath
659
+ });
660
+ });
661
+ return locations;
662
+ }
663
+ function createProject() {
664
+ return new Project({
665
+ compilerOptions: {
666
+ allowJs: true,
667
+ checkJs: false
668
+ },
669
+ manipulationSettings: {
670
+ insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false,
671
+ useTrailingCommas: true
672
+ },
673
+ skipAddingFilesFromTsConfig: true
674
+ });
675
+ }
676
+ function getFileSinceTagLocations(cwd, filePath) {
677
+ return collectSourceFileSinceTagLocations(cwd, createProject().addSourceFileAtPath(filePath));
678
+ }
679
+ async function checkJsDocSinceTags({ cwd = process.cwd(), packages }) {
680
+ const unresolvedTags = [];
681
+ for (const pkg of packages) {
682
+ const sourceFiles = await getSourceFiles(pkg.root);
683
+ for (const filePath of sourceFiles) unresolvedTags.push(...getFileSinceTagLocations(cwd, filePath));
684
+ }
685
+ return { unresolvedTags };
686
+ }
687
+ async function updatePackages(cwd, packages, onProgress) {
688
+ const updatedPackages = [];
689
+ const unresolvedFiles = [];
690
+ for (const pkg of packages) {
691
+ onProgress?.(formatJsDocSincePackageUpdateProgress(pkg));
692
+ const sourceFiles = await getSourceFiles(pkg.root);
693
+ let fileCount = 0;
694
+ let tagCount = 0;
695
+ for (const filePath of sourceFiles) {
696
+ const updatedTags = await updateSourceFile(filePath, pkg.version);
697
+ if (updatedTags > 0) {
698
+ fileCount += 1;
699
+ tagCount += updatedTags;
700
+ }
701
+ }
702
+ if (tagCount > 0) updatedPackages.push({
703
+ fileCount,
704
+ name: pkg.name,
705
+ tagCount,
706
+ version: pkg.version
707
+ });
708
+ unresolvedFiles.push(...await findUnresolvedFiles(cwd, sourceFiles));
709
+ }
710
+ return {
711
+ unresolvedFiles,
712
+ updatedPackages
713
+ };
714
+ }
715
+ async function updateJsDocSinceTagsForPackages({ cwd = process.cwd(), onProgress, packages }) {
716
+ return updatePackages(cwd, getVersionedPackages(packages), onProgress);
717
+ }
718
+ function formatJsDocSinceUpdateResult(result) {
719
+ const lines = [];
720
+ if (result.updatedPackages.length > 0) {
721
+ lines.push("Updated JSDoc @since tags:");
722
+ for (const pkg of result.updatedPackages) lines.push(` ${pkg.name} ${pkg.version}: ${pkg.tagCount} ${pluralize(pkg.tagCount, "tag", "tags")} in ${pkg.fileCount} ${pluralize(pkg.fileCount, "file", "files")}`);
723
+ }
724
+ if (result.unresolvedFiles.length > 0) {
725
+ if (lines.length > 0) lines.push("");
726
+ lines.push("Warning: unresolved @since next-release tags remain after JSDoc version update:");
727
+ lines.push(...result.unresolvedFiles.map((filePath) => ` ${filePath}`));
728
+ lines.push("");
729
+ lines.push("These tags were not rewritten automatically. This is non-blocking, but the updater may need to support another JSDoc shape.");
730
+ }
731
+ return lines;
732
+ }
733
+ function formatJsDocSinceCheckResult(result) {
734
+ if (result.unresolvedTags.length === 0) return ["No @since next-release tags found."];
735
+ return ["Found @since next-release tags:", ...result.unresolvedTags.map(({ entityName, filePath }) => ` ${filePath}: ${entityName}`)];
736
+ }
737
+ //#endregion
738
+ //#region src/version-bump.ts
739
+ var CHANGESET_CONFIG_LOCATION = join(".changeset", "config.json");
740
+ function normalizePath(path) {
741
+ return resolve(path);
742
+ }
743
+ function isWithinDirectory(parent, child) {
744
+ const relativePath = relative(parent, child);
745
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
746
+ }
747
+ function getPackageLabel(directory) {
748
+ return directory.replaceAll("\\", "/");
749
+ }
750
+ function getSelectedDirectory(cwd, directory) {
751
+ return resolve(cwd, directory);
752
+ }
753
+ function getSelectedDirectories({ cwd, directories, directory }) {
754
+ return (directories && directories.length > 0 ? directories : directory ? [directory] : []).map((selectedDirectory) => ({
755
+ input: selectedDirectory,
756
+ root: getSelectedDirectory(cwd, selectedDirectory)
757
+ }));
758
+ }
759
+ function getPackageSourceSnapshot(pkg, versionOverride) {
760
+ return {
761
+ ...pkg,
762
+ root: resolve(pkg.root, "src"),
763
+ version: versionOverride ?? pkg.version
764
+ };
765
+ }
766
+ function getPackageSourceSnapshots(packages, versionOverride) {
767
+ return packages.map((pkg) => getPackageSourceSnapshot(pkg, versionOverride));
768
+ }
769
+ function normalizeGitPath(path) {
770
+ return path.split(sep).join("/");
771
+ }
772
+ function getBaseBranch({ configPath = CHANGESET_CONFIG_LOCATION, cwd }) {
773
+ return JSON.parse(readFileSync(join(cwd, configPath), "utf-8")).baseBranch ?? "main";
774
+ }
775
+ function getPackageVersionAtRef(pkg, diffRef, cwd) {
776
+ const packageJsonPath = normalizeGitPath(relative(cwd, join(pkg.root, "package.json")));
777
+ try {
778
+ const packageJson = execFileSync("git", ["show", `${diffRef}:${packageJsonPath}`], {
779
+ cwd,
780
+ encoding: "utf-8"
781
+ });
782
+ return JSON.parse(packageJson).version;
783
+ } catch {
784
+ return;
785
+ }
786
+ }
787
+ function isGitRefResolvable(diffRef, cwd) {
788
+ try {
789
+ execFileSync("git", [
790
+ "rev-parse",
791
+ "--verify",
792
+ `${diffRef}^{commit}`
793
+ ], {
794
+ cwd,
795
+ stdio: "ignore"
796
+ });
797
+ return true;
798
+ } catch {
799
+ return false;
800
+ }
801
+ }
802
+ function getPackagesChangedSinceRef({ cwd, diffRef, packages }) {
803
+ if (!isGitRefResolvable(diffRef, cwd)) throw new Error(`Git ref "${diffRef}" could not be resolved.`);
804
+ return packages.filter((pkg) => getPackageVersionAtRef(pkg, diffRef, cwd) !== pkg.version);
805
+ }
806
+ function findContainingPackage(directory, packages) {
807
+ return packages.filter((pkg) => isWithinDirectory(normalizePath(pkg.root), directory)).sort((a, b) => normalizePath(b.root).length - normalizePath(a.root).length).at(0);
808
+ }
809
+ function selectCustomCheckSnapshots(directories, packages) {
810
+ return directories.map(({ input, root }) => {
811
+ const containingPackage = findContainingPackage(root, packages);
812
+ return {
813
+ name: containingPackage?.name ?? getPackageLabel(input),
814
+ root,
815
+ version: containingPackage?.version
816
+ };
817
+ });
818
+ }
819
+ function selectCustomUpdateSnapshots(directories, packages, versionOverride) {
820
+ return directories.map(({ input, root }) => {
821
+ const containingPackage = findContainingPackage(root, packages);
822
+ const version = versionOverride ?? containingPackage?.version;
823
+ if (!version) throw new Error(`Directory "${input}" is not within a package with a version. Pass --version to update arbitrary directories.`);
824
+ return {
825
+ name: containingPackage?.name ?? getPackageLabel(input),
826
+ root,
827
+ version
828
+ };
829
+ });
830
+ }
831
+ function selectDefaultPackageSourceSnapshots({ configPath, cwd = process.cwd(), diffRef, directories, directory, packages, version }) {
832
+ const selectedDirectories = getSelectedDirectories({
833
+ cwd,
834
+ directories,
835
+ directory
836
+ });
837
+ if (selectedDirectories.length === 0) return getPackageSourceSnapshots(getPackagesChangedSinceRef({
838
+ cwd,
839
+ diffRef: diffRef ?? getBaseBranch({
840
+ configPath,
841
+ cwd
842
+ }),
843
+ packages
844
+ }), version);
845
+ return selectCustomUpdateSnapshots(selectedDirectories, packages, version);
846
+ }
847
+ function selectCheckPackageSnapshots({ cwd = process.cwd(), directories, directory, packages }) {
848
+ const selectedDirectories = getSelectedDirectories({
849
+ cwd,
850
+ directories,
851
+ directory
852
+ });
853
+ if (selectedDirectories.length === 0) return getPackageSourceSnapshots(packages);
854
+ return selectCustomCheckSnapshots(selectedDirectories, packages);
855
+ }
856
+ function getPackageSnapshots(cwd = process.cwd()) {
857
+ return getPackagesSync(cwd).packages.flatMap((pkg) => {
858
+ const { name, version } = pkg.packageJson;
859
+ if (!name) return [];
860
+ if (!existsSync(join(pkg.dir, "src"))) return [];
861
+ return [{
862
+ name,
863
+ root: pkg.dir,
864
+ version
865
+ }];
866
+ });
867
+ }
868
+ function getCheckPackageSnapshots({ cwd = process.cwd(), directories, directory, packages = getPackageSnapshots(cwd) } = {}) {
869
+ return selectCheckPackageSnapshots({
870
+ cwd,
871
+ directories,
872
+ directory,
873
+ packages
874
+ });
875
+ }
876
+ function getUpdatePackageSnapshots({ configPath, cwd = process.cwd(), diffRef, directories, directory, packages = getPackageSnapshots(cwd), version } = {}) {
877
+ return selectDefaultPackageSourceSnapshots({
878
+ configPath,
879
+ cwd,
880
+ diffRef,
881
+ directories,
882
+ directory,
883
+ packages,
884
+ version
885
+ });
886
+ }
887
+ function bumpVersionsAndMaybeUpdateJsDocSinceTags({ exec = (command) => execSync(command, { stdio: "inherit" }), packageManager = "pnpm" } = {}) {
888
+ exec(`${packageManager} changeset version`);
889
+ }
890
+ //#endregion
891
+ //#region src/cli.ts
892
+ function buildSteps(options) {
893
+ const pm = options.packageManager ?? "pnpm";
894
+ return [
895
+ {
896
+ description: "Generate changesets from conventional commits",
897
+ name: "changeset-generate",
898
+ run: () => conventionalCommitChangeset({
899
+ commitSha: options.commitSha,
900
+ configPath: options.config,
901
+ includeCommitLinks: options.includeCommitLinks
902
+ })
903
+ },
904
+ {
905
+ description: "Bump versions and generate changelogs",
906
+ name: "bump",
907
+ run: () => bumpVersionsAndMaybeUpdateJsDocSinceTags({ packageManager: pm })
908
+ },
909
+ {
910
+ description: "Consolidate changelog formatting",
911
+ name: "consolidate-changelogs",
912
+ run: () => consolidateChangelogs()
913
+ },
914
+ {
915
+ description: "Generate combined release notes",
916
+ name: "generate-release-notes",
917
+ run: async () => {
918
+ await generateReleaseNotes();
919
+ }
920
+ }
921
+ ];
922
+ }
923
+ async function waitForConfirmation(stepName) {
924
+ const rl = createInterface({
925
+ input: stdin,
926
+ output: stdout
927
+ });
928
+ try {
929
+ return (await rl.question(`\nStep "${stepName}" completed. Continue to next step? [Y/n] `)).trim().toLowerCase() !== "n";
930
+ } finally {
931
+ rl.close();
932
+ }
933
+ }
934
+ async function run(options) {
935
+ const steps = buildSteps(options);
936
+ for (let i = 0; i < steps.length; i++) {
937
+ const step = steps[i];
938
+ const stepLabel = `[${i + 1}/${steps.length}]`;
939
+ console.log(`\n${stepLabel} ${step.description}...`);
940
+ try {
941
+ await step.run();
942
+ } catch (e) {
943
+ console.error(`\nStep "${step.name}" failed. Aborting prep-release.`);
944
+ console.error(e);
945
+ process.exit(1);
946
+ }
947
+ if (options.inSteps && i < steps.length - 1) {
948
+ if (!await waitForConfirmation(step.name)) {
949
+ console.log("Aborted by user.");
950
+ process.exit(0);
951
+ }
952
+ }
953
+ }
954
+ console.log("\nAll prep-release steps completed.");
955
+ }
956
+ program.allowUnknownOption(false);
957
+ program.command("prep-release").description("Run all prep-release steps sequentially").option("--in-steps", "Pause after each step and wait for confirmation before continuing", false).option("--commit-sha <sha>", "Diff each package against the target commit instead of the repository's base branch").option("--include-commit-links", "Embed commit hashes in changeset summaries for changelog links", false).option("--package-manager <command>", "Package manager command to use for changeset version", "pnpm").option("--config <path>", "Path to the changesets config file, relative to the project root").action((options) => run(options));
958
+ program.command("changeset-generate").description("Generate changesets from conventional commits").option("--commit-sha <sha>", "Diff each package against the target commit instead of the repository's base branch").option("--include-commit-links", "Embed commit hashes in changeset summaries for changelog links", false).option("--config <path>", "Path to the changesets config file, relative to the project root").action(({ config, ...options }) => conventionalCommitChangeset({
959
+ ...options,
960
+ configPath: config
961
+ }));
962
+ program.command("consolidate-changelogs").description("Consolidate changelog formatting").action(() => consolidateChangelogs());
963
+ program.command("generate-release-notes").description("Generate combined release notes").action(async () => {
964
+ await generateReleaseNotes();
965
+ });
966
+ program.command("check-versions").description("Check which packages have newer versions than what is published on npm").option("--config <path>", "Path to the changesets config file, relative to the project root").action((options) => checkVersions({ configPath: options.config }));
967
+ program.command("check-jsdoc-since-tags").argument("[directories...]", "Directories to scan instead of package sources. If this is omitted, the `src` folder will be used by default").description("Check for JSDoc @since next-release tags without modifying files").action(async (directories) => {
968
+ const result = await checkJsDocSinceTags({ packages: getCheckPackageSnapshots({ directories }) });
969
+ for (const line of formatJsDocSinceCheckResult(result)) console.log(line);
970
+ });
971
+ program.command("update-jsdoc-since-tags").argument("[directories...]", "Directories to update instead of package sources").option("--version <version>", "Version to use instead of each containing package's current version").option("--diff-ref <git-ref>", "Git ref to compare package versions against (defaults to the changesets base branch)").description("Replace JSDoc @since next-release tags with package or explicit versions").action(async (directories, options) => {
972
+ console.log(formatJsDocSinceUpdateStartMessage());
973
+ const lines = formatJsDocSinceUpdateResult(await updateJsDocSinceTagsForPackages({
974
+ onProgress: console.log,
975
+ packages: getUpdatePackageSnapshots({
976
+ diffRef: options.diffRef,
977
+ directories,
978
+ version: options.version
979
+ })
980
+ }));
981
+ if (lines.length === 0) {
982
+ console.log("No JSDoc @since next-release tags updated.");
983
+ return;
984
+ }
985
+ for (const line of lines) console.log(line);
986
+ });
987
+ program.command("create-github-releases").description("Create GitHub releases for published packages from changelogs").option("--token <token>", "GitHub token for authentication (falls back to TOKEN or GITHUB_TOKEN env vars)").option("--repo <owner/repo>", "GitHub repository in owner/repo format (defaults to git remote origin)").option("--config <path>", "Path to the changesets config file, relative to the project root").action((options) => {
988
+ const token = options.token ?? process.env.TOKEN ?? process.env.GITHUB_TOKEN;
989
+ if (!token) {
990
+ console.error("GitHub token is required. Use --token, or set TOKEN or GITHUB_TOKEN env var.");
991
+ process.exit(1);
992
+ }
993
+ return createGitHubReleases({
994
+ configPath: options.config,
995
+ repo: options.repo,
996
+ token
997
+ });
998
+ });
999
+ program.parse(process.argv);
1000
+ //#endregion
1001
+ export {};
1002
+
1003
+ //# sourceMappingURL=cli.js.map