@gpc-cli/core 0.9.50 → 0.9.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +110 -1
- package/dist/index.js +767 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -3723,10 +3723,10 @@ ${bullets}`);
|
|
|
3723
3723
|
}
|
|
3724
3724
|
return { text, truncated };
|
|
3725
3725
|
}
|
|
3726
|
-
async function gitExec(args) {
|
|
3726
|
+
async function gitExec(args, opts) {
|
|
3727
3727
|
try {
|
|
3728
|
-
const {
|
|
3729
|
-
return stdout.trim();
|
|
3728
|
+
const result = opts?.cwd ? await execFile("git", args, { encoding: "utf8", cwd: opts.cwd }) : await execFile("git", args);
|
|
3729
|
+
return result.stdout.trim();
|
|
3730
3730
|
} catch (error) {
|
|
3731
3731
|
const err = error;
|
|
3732
3732
|
if (err.code === "ENOENT") {
|
|
@@ -7361,6 +7361,753 @@ function formatChangelogEntry(entry) {
|
|
|
7361
7361
|
return lines.join("\n");
|
|
7362
7362
|
}
|
|
7363
7363
|
|
|
7364
|
+
// src/commands/changelog-generate.ts
|
|
7365
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
7366
|
+
"feat",
|
|
7367
|
+
"fix",
|
|
7368
|
+
"perf",
|
|
7369
|
+
"breaking",
|
|
7370
|
+
"docs",
|
|
7371
|
+
"ci",
|
|
7372
|
+
"chore",
|
|
7373
|
+
"refactor",
|
|
7374
|
+
"test",
|
|
7375
|
+
"build",
|
|
7376
|
+
"style",
|
|
7377
|
+
"release"
|
|
7378
|
+
]);
|
|
7379
|
+
var FILTERED_TYPES = /* @__PURE__ */ new Set(["chore", "refactor", "test", "build", "style", "merge"]);
|
|
7380
|
+
var SECTION_ORDER = ["breaking", "feat", "fix", "perf", "docs", "ci", "release", "other"];
|
|
7381
|
+
var FIXUP_PATTERNS = [
|
|
7382
|
+
/^wip\b/i,
|
|
7383
|
+
/^fix\s+typo\b/i,
|
|
7384
|
+
/^fix\s+typos\b/i,
|
|
7385
|
+
/^address\s+review\b/i,
|
|
7386
|
+
/^review\s+fixes\b/i,
|
|
7387
|
+
/^fixup!/i,
|
|
7388
|
+
/^squash!/i
|
|
7389
|
+
];
|
|
7390
|
+
var REVERT_PATTERN = /^Revert\s+"(.+?)"\s*$/i;
|
|
7391
|
+
var VERB_CANONICALIZATIONS = [
|
|
7392
|
+
[/^Added\b/, "add"],
|
|
7393
|
+
[/^Adds\b/, "add"],
|
|
7394
|
+
[/^Add\b/, "add"],
|
|
7395
|
+
[/^Fixed\b/, "fix"],
|
|
7396
|
+
[/^Fixes\b/, "fix"],
|
|
7397
|
+
[/^Fix\b/, "fix"],
|
|
7398
|
+
[/^Updated\b/, "update"],
|
|
7399
|
+
[/^Updates\b/, "update"],
|
|
7400
|
+
[/^Update\b/, "update"],
|
|
7401
|
+
[/^Removed\b/, "remove"],
|
|
7402
|
+
[/^Removes\b/, "remove"],
|
|
7403
|
+
[/^Remove\b/, "remove"]
|
|
7404
|
+
];
|
|
7405
|
+
var EMOJI_PREFIX = /^(?:[\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*)+/u;
|
|
7406
|
+
var INTERNAL_JARGON = [
|
|
7407
|
+
"mutex",
|
|
7408
|
+
"token bucket",
|
|
7409
|
+
"barrel export",
|
|
7410
|
+
"barrel exports",
|
|
7411
|
+
"homedir",
|
|
7412
|
+
"at module level",
|
|
7413
|
+
"ESM",
|
|
7414
|
+
"tsup",
|
|
7415
|
+
"vi.stubGlobal",
|
|
7416
|
+
"execFile",
|
|
7417
|
+
"lazy-import"
|
|
7418
|
+
];
|
|
7419
|
+
var RECORD_START = "";
|
|
7420
|
+
var FIELD_SEP = "";
|
|
7421
|
+
var COMMIT_FORMAT = `${RECORD_START}%H${FIELD_SEP}%s${FIELD_SEP}%aI${FIELD_SEP}%b`;
|
|
7422
|
+
function parseRawCommits(stdout) {
|
|
7423
|
+
if (!stdout.trim()) return [];
|
|
7424
|
+
const blocks = stdout.split(RECORD_START).filter((b) => b.trim());
|
|
7425
|
+
const commits = [];
|
|
7426
|
+
for (const block of blocks) {
|
|
7427
|
+
const headerEnd = block.indexOf("\n");
|
|
7428
|
+
const header = headerEnd === -1 ? block : block.slice(0, headerEnd);
|
|
7429
|
+
const rest = headerEnd === -1 ? "" : block.slice(headerEnd + 1);
|
|
7430
|
+
const parts = header.split(FIELD_SEP);
|
|
7431
|
+
const sha = parts[0]?.trim() ?? "";
|
|
7432
|
+
const subject = parts[1]?.trim() ?? "";
|
|
7433
|
+
const authorDate = parts[2]?.trim() ?? "";
|
|
7434
|
+
const body = (parts[3] ?? "").trim();
|
|
7435
|
+
if (!sha) continue;
|
|
7436
|
+
const files = [];
|
|
7437
|
+
let additions = 0;
|
|
7438
|
+
let deletions = 0;
|
|
7439
|
+
for (const line of rest.split("\n")) {
|
|
7440
|
+
const trimmed = line.trim();
|
|
7441
|
+
if (!trimmed) continue;
|
|
7442
|
+
const match = trimmed.match(/^(\d+|-)\s+(\d+|-)\s+(.+)$/);
|
|
7443
|
+
if (match) {
|
|
7444
|
+
const a = match[1] === "-" ? 0 : Number(match[1]);
|
|
7445
|
+
const d = match[2] === "-" ? 0 : Number(match[2]);
|
|
7446
|
+
additions += a;
|
|
7447
|
+
deletions += d;
|
|
7448
|
+
files.push(match[3] ?? "");
|
|
7449
|
+
}
|
|
7450
|
+
}
|
|
7451
|
+
commits.push({ sha, subject, body, files, additions, deletions, authorDate });
|
|
7452
|
+
}
|
|
7453
|
+
return commits;
|
|
7454
|
+
}
|
|
7455
|
+
var defaultGitRunner = {
|
|
7456
|
+
async log({ from, to, cwd }) {
|
|
7457
|
+
const stdout = await gitExec(
|
|
7458
|
+
[
|
|
7459
|
+
"log",
|
|
7460
|
+
"--no-merges",
|
|
7461
|
+
`--format=${COMMIT_FORMAT}`,
|
|
7462
|
+
"--numstat",
|
|
7463
|
+
"--end-of-options",
|
|
7464
|
+
`${from}..${to}`
|
|
7465
|
+
],
|
|
7466
|
+
{ cwd }
|
|
7467
|
+
);
|
|
7468
|
+
return parseRawCommits(stdout);
|
|
7469
|
+
},
|
|
7470
|
+
async describeLatestTag(cwd) {
|
|
7471
|
+
try {
|
|
7472
|
+
const out = await gitExec(["describe", "--tags", "--match", "v*", "--abbrev=0"], { cwd });
|
|
7473
|
+
return out.trim() || null;
|
|
7474
|
+
} catch {
|
|
7475
|
+
return null;
|
|
7476
|
+
}
|
|
7477
|
+
},
|
|
7478
|
+
async verifyRef(ref, cwd) {
|
|
7479
|
+
try {
|
|
7480
|
+
await gitExec(["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], { cwd });
|
|
7481
|
+
return true;
|
|
7482
|
+
} catch {
|
|
7483
|
+
return false;
|
|
7484
|
+
}
|
|
7485
|
+
},
|
|
7486
|
+
async remoteUrl(cwd) {
|
|
7487
|
+
try {
|
|
7488
|
+
const out = await gitExec(["remote", "get-url", "origin"], { cwd });
|
|
7489
|
+
return out.trim() || null;
|
|
7490
|
+
} catch {
|
|
7491
|
+
return null;
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
7494
|
+
};
|
|
7495
|
+
function parseRemoteUrl(url) {
|
|
7496
|
+
if (!url) return null;
|
|
7497
|
+
const https = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?\/?$/i);
|
|
7498
|
+
if (https) return `${https[1]}/${https[2]}`;
|
|
7499
|
+
const ssh = url.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/i);
|
|
7500
|
+
if (ssh) return `${ssh[1]}/${ssh[2]}`;
|
|
7501
|
+
return null;
|
|
7502
|
+
}
|
|
7503
|
+
function stripEmojiAndCanonicalize(subject) {
|
|
7504
|
+
let s = subject.replace(EMOJI_PREFIX, "");
|
|
7505
|
+
for (const [pattern, replacement] of VERB_CANONICALIZATIONS) {
|
|
7506
|
+
if (pattern.test(s)) {
|
|
7507
|
+
s = s.replace(pattern, replacement);
|
|
7508
|
+
break;
|
|
7509
|
+
}
|
|
7510
|
+
}
|
|
7511
|
+
return s.trim();
|
|
7512
|
+
}
|
|
7513
|
+
function isFixupSubject(subject) {
|
|
7514
|
+
return FIXUP_PATTERNS.some((p) => p.test(subject));
|
|
7515
|
+
}
|
|
7516
|
+
var CONVENTIONAL_RE = /^(?<type>\w+)(?:\((?<scope>[^)]+)\))?(?<bang>!?):\s*(?<rest>.+)$/;
|
|
7517
|
+
var PR_REF_RE = /\s*\(#(\d+)\)\s*$/;
|
|
7518
|
+
function parseCommit(raw) {
|
|
7519
|
+
const subject = raw.subject;
|
|
7520
|
+
const revertMatch = subject.match(REVERT_PATTERN);
|
|
7521
|
+
const isRevert = !!revertMatch;
|
|
7522
|
+
const effectiveSubject = revertMatch?.[1] ?? subject;
|
|
7523
|
+
const match = effectiveSubject.match(CONVENTIONAL_RE);
|
|
7524
|
+
let type = "other";
|
|
7525
|
+
let scope;
|
|
7526
|
+
let rest = effectiveSubject;
|
|
7527
|
+
if (match?.groups) {
|
|
7528
|
+
const rawType = match.groups["type"]?.toLowerCase() ?? "other";
|
|
7529
|
+
type = KNOWN_TYPES.has(rawType) ? rawType : "other";
|
|
7530
|
+
scope = match.groups["scope"];
|
|
7531
|
+
rest = match.groups["rest"] ?? effectiveSubject;
|
|
7532
|
+
if (match.groups["bang"]) type = "breaking";
|
|
7533
|
+
}
|
|
7534
|
+
let prRef;
|
|
7535
|
+
const prMatch = rest.match(PR_REF_RE);
|
|
7536
|
+
if (prMatch) {
|
|
7537
|
+
prRef = `#${prMatch[1]}`;
|
|
7538
|
+
rest = rest.replace(PR_REF_RE, "").trim();
|
|
7539
|
+
}
|
|
7540
|
+
const cleaned = stripEmojiAndCanonicalize(rest);
|
|
7541
|
+
const finalSubject = prRef ? `${cleaned} (${prRef})` : cleaned;
|
|
7542
|
+
return {
|
|
7543
|
+
sha: raw.sha,
|
|
7544
|
+
type,
|
|
7545
|
+
scope,
|
|
7546
|
+
subject: finalSubject,
|
|
7547
|
+
prRef,
|
|
7548
|
+
files: raw.files,
|
|
7549
|
+
weight: raw.additions + raw.deletions,
|
|
7550
|
+
isRevert,
|
|
7551
|
+
isFixup: isFixupSubject(rest) || isFixupSubject(subject),
|
|
7552
|
+
authorDate: raw.authorDate
|
|
7553
|
+
};
|
|
7554
|
+
}
|
|
7555
|
+
function dedupRevertPairs(commits) {
|
|
7556
|
+
const reverted = /* @__PURE__ */ new Set();
|
|
7557
|
+
for (const c of commits) {
|
|
7558
|
+
if (c.isRevert) reverted.add(c.subject);
|
|
7559
|
+
}
|
|
7560
|
+
return commits.filter((c) => {
|
|
7561
|
+
if (c.isRevert) return false;
|
|
7562
|
+
if (reverted.has(c.subject)) return false;
|
|
7563
|
+
return true;
|
|
7564
|
+
});
|
|
7565
|
+
}
|
|
7566
|
+
function topPathPrefix(file, depth = 2) {
|
|
7567
|
+
return file.split("/").slice(0, depth).join("/");
|
|
7568
|
+
}
|
|
7569
|
+
function tokenize2(subject) {
|
|
7570
|
+
const STOP = /* @__PURE__ */ new Set([
|
|
7571
|
+
"the",
|
|
7572
|
+
"a",
|
|
7573
|
+
"an",
|
|
7574
|
+
"and",
|
|
7575
|
+
"or",
|
|
7576
|
+
"of",
|
|
7577
|
+
"to",
|
|
7578
|
+
"in",
|
|
7579
|
+
"on",
|
|
7580
|
+
"for",
|
|
7581
|
+
"with",
|
|
7582
|
+
"is",
|
|
7583
|
+
"be",
|
|
7584
|
+
"by",
|
|
7585
|
+
"at"
|
|
7586
|
+
]);
|
|
7587
|
+
return new Set(
|
|
7588
|
+
subject.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP.has(w))
|
|
7589
|
+
);
|
|
7590
|
+
}
|
|
7591
|
+
function jaccard(a, b) {
|
|
7592
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
7593
|
+
let inter = 0;
|
|
7594
|
+
for (const t of a) if (b.has(t)) inter++;
|
|
7595
|
+
const union = a.size + b.size - inter;
|
|
7596
|
+
return union === 0 ? 0 : inter / union;
|
|
7597
|
+
}
|
|
7598
|
+
function shouldCluster(a, b, ta, tb) {
|
|
7599
|
+
const aPrefixes = new Set(a.files.map((f) => topPathPrefix(f)));
|
|
7600
|
+
for (const f of b.files) {
|
|
7601
|
+
if (aPrefixes.has(topPathPrefix(f))) return true;
|
|
7602
|
+
}
|
|
7603
|
+
if (jaccard(ta, tb) > 0.4) return true;
|
|
7604
|
+
const aDate = Date.parse(a.authorDate);
|
|
7605
|
+
const bDate = Date.parse(b.authorDate);
|
|
7606
|
+
if (!Number.isNaN(aDate) && !Number.isNaN(bDate)) {
|
|
7607
|
+
const dayDiff = Math.abs(aDate - bDate) / 864e5;
|
|
7608
|
+
if (dayDiff <= 2) {
|
|
7609
|
+
const aFiles = new Set(a.files);
|
|
7610
|
+
for (const f of b.files) if (aFiles.has(f)) return true;
|
|
7611
|
+
}
|
|
7612
|
+
}
|
|
7613
|
+
return false;
|
|
7614
|
+
}
|
|
7615
|
+
var TYPE_PRIORITY = {
|
|
7616
|
+
breaking: 0,
|
|
7617
|
+
feat: 1,
|
|
7618
|
+
fix: 2,
|
|
7619
|
+
perf: 3,
|
|
7620
|
+
docs: 4,
|
|
7621
|
+
ci: 5,
|
|
7622
|
+
release: 6,
|
|
7623
|
+
other: 7
|
|
7624
|
+
};
|
|
7625
|
+
function clusterCommits(commits) {
|
|
7626
|
+
const n = commits.length;
|
|
7627
|
+
const parent = Array.from({ length: n }, (_, i) => i);
|
|
7628
|
+
const find = (i) => {
|
|
7629
|
+
while (parent[i] !== i) {
|
|
7630
|
+
parent[i] = parent[parent[i]];
|
|
7631
|
+
i = parent[i];
|
|
7632
|
+
}
|
|
7633
|
+
return i;
|
|
7634
|
+
};
|
|
7635
|
+
const union = (i, j) => {
|
|
7636
|
+
const ri = find(i);
|
|
7637
|
+
const rj = find(j);
|
|
7638
|
+
if (ri !== rj) parent[ri] = rj;
|
|
7639
|
+
};
|
|
7640
|
+
const tokens = commits.map((c) => tokenize2(c.subject));
|
|
7641
|
+
for (let i = 0; i < n; i++) {
|
|
7642
|
+
for (let j = i + 1; j < n; j++) {
|
|
7643
|
+
if (shouldCluster(commits[i], commits[j], tokens[i], tokens[j])) union(i, j);
|
|
7644
|
+
}
|
|
7645
|
+
}
|
|
7646
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7647
|
+
for (let i = 0; i < n; i++) {
|
|
7648
|
+
const root = find(i);
|
|
7649
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
7650
|
+
groups.get(root).push(commits[i]);
|
|
7651
|
+
}
|
|
7652
|
+
const clusters = [];
|
|
7653
|
+
for (const [, members] of groups) {
|
|
7654
|
+
const weight = members.reduce((s, m) => s + m.weight, 0);
|
|
7655
|
+
const primaryType = members.map((m) => m.type).sort((a, b) => (TYPE_PRIORITY[a] ?? 99) - (TYPE_PRIORITY[b] ?? 99))[0];
|
|
7656
|
+
const label = clusterLabel(members);
|
|
7657
|
+
clusters.push({
|
|
7658
|
+
id: label.toLowerCase().replace(/\s+/g, "-"),
|
|
7659
|
+
label,
|
|
7660
|
+
commits: members,
|
|
7661
|
+
weight,
|
|
7662
|
+
primaryType
|
|
7663
|
+
});
|
|
7664
|
+
}
|
|
7665
|
+
return clusters;
|
|
7666
|
+
}
|
|
7667
|
+
function clusterLabel(members) {
|
|
7668
|
+
const pathCounts = /* @__PURE__ */ new Map();
|
|
7669
|
+
for (const m of members) {
|
|
7670
|
+
for (const f of m.files) {
|
|
7671
|
+
const top = topPathPrefix(f, 2);
|
|
7672
|
+
pathCounts.set(top, (pathCounts.get(top) ?? 0) + 1);
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
let bestPath = null;
|
|
7676
|
+
let bestPathCount = 0;
|
|
7677
|
+
for (const [p, c] of pathCounts) {
|
|
7678
|
+
if (c > bestPathCount) {
|
|
7679
|
+
bestPath = p;
|
|
7680
|
+
bestPathCount = c;
|
|
7681
|
+
}
|
|
7682
|
+
}
|
|
7683
|
+
if (bestPath) return bestPath;
|
|
7684
|
+
const tokenCounts = /* @__PURE__ */ new Map();
|
|
7685
|
+
for (const m of members) {
|
|
7686
|
+
for (const t of tokenize2(m.subject)) {
|
|
7687
|
+
tokenCounts.set(t, (tokenCounts.get(t) ?? 0) + 1);
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
let bestToken = null;
|
|
7691
|
+
let bestTokenCount = 0;
|
|
7692
|
+
for (const [t, c] of tokenCounts) {
|
|
7693
|
+
if (c > bestTokenCount) {
|
|
7694
|
+
bestToken = t;
|
|
7695
|
+
bestTokenCount = c;
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
return bestToken ?? members[0].subject.slice(0, 30);
|
|
7699
|
+
}
|
|
7700
|
+
function scoreHeadlines(clusters) {
|
|
7701
|
+
return [...clusters].sort((a, b) => {
|
|
7702
|
+
if (b.weight !== a.weight) return b.weight - a.weight;
|
|
7703
|
+
return (TYPE_PRIORITY[a.primaryType] ?? 99) - (TYPE_PRIORITY[b.primaryType] ?? 99);
|
|
7704
|
+
}).slice(0, 3);
|
|
7705
|
+
}
|
|
7706
|
+
function lintJargon(commits) {
|
|
7707
|
+
const warnings = [];
|
|
7708
|
+
for (const c of commits) {
|
|
7709
|
+
const lower = c.subject.toLowerCase();
|
|
7710
|
+
for (const word of INTERNAL_JARGON) {
|
|
7711
|
+
if (lower.includes(word.toLowerCase())) {
|
|
7712
|
+
warnings.push(`jargon: "${word}" in subject "${c.subject}" (${c.sha.slice(0, 7)})`);
|
|
7713
|
+
}
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
7716
|
+
return warnings;
|
|
7717
|
+
}
|
|
7718
|
+
async function generateChangelog(opts = {}, runner = defaultGitRunner) {
|
|
7719
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
7720
|
+
let from = opts.from;
|
|
7721
|
+
if (!from) {
|
|
7722
|
+
from = await runner.describeLatestTag(cwd) ?? void 0;
|
|
7723
|
+
if (!from) {
|
|
7724
|
+
throw new GpcError(
|
|
7725
|
+
"No git tags found and --from was not provided",
|
|
7726
|
+
"CHANGELOG_NO_TAG",
|
|
7727
|
+
1,
|
|
7728
|
+
"Pass --from <ref> explicitly, or create an initial tag (e.g., git tag v0.0.1)."
|
|
7729
|
+
);
|
|
7730
|
+
}
|
|
7731
|
+
}
|
|
7732
|
+
const to = opts.to ?? "HEAD";
|
|
7733
|
+
if (!await runner.verifyRef(from, cwd)) {
|
|
7734
|
+
throw new GpcError(
|
|
7735
|
+
`Invalid --from ref: "${from}"`,
|
|
7736
|
+
"CHANGELOG_BAD_REF",
|
|
7737
|
+
1,
|
|
7738
|
+
"Verify the ref exists with: git rev-parse --verify <ref>"
|
|
7739
|
+
);
|
|
7740
|
+
}
|
|
7741
|
+
if (!await runner.verifyRef(to, cwd)) {
|
|
7742
|
+
throw new GpcError(
|
|
7743
|
+
`Invalid --to ref: "${to}"`,
|
|
7744
|
+
"CHANGELOG_BAD_REF",
|
|
7745
|
+
1,
|
|
7746
|
+
"Verify the ref exists with: git rev-parse --verify <ref>"
|
|
7747
|
+
);
|
|
7748
|
+
}
|
|
7749
|
+
const repo = opts.repo ?? parseRemoteUrl(await runner.remoteUrl(cwd));
|
|
7750
|
+
const raw = await runner.log({ from, to, cwd });
|
|
7751
|
+
const rawCommitCount = raw.length;
|
|
7752
|
+
const parsed = raw.map(parseCommit);
|
|
7753
|
+
const warnings = [];
|
|
7754
|
+
const scopeLeak = parsed.find((c) => c.scope);
|
|
7755
|
+
if (scopeLeak) {
|
|
7756
|
+
warnings.push(
|
|
7757
|
+
`scope: dropped per project convention (e.g., "${scopeLeak.scope}" in ${scopeLeak.sha.slice(0, 7)})`
|
|
7758
|
+
);
|
|
7759
|
+
}
|
|
7760
|
+
const afterRevert = dedupRevertPairs(parsed);
|
|
7761
|
+
const nonFixup = afterRevert.filter((c) => !c.isFixup);
|
|
7762
|
+
const visible = nonFixup.filter((c) => !FILTERED_TYPES.has(c.type));
|
|
7763
|
+
const clusters = clusterCommits(visible);
|
|
7764
|
+
const headlineCandidates = scoreHeadlines(clusters);
|
|
7765
|
+
const grouped = {};
|
|
7766
|
+
for (const c of visible) {
|
|
7767
|
+
if (!grouped[c.type]) grouped[c.type] = [];
|
|
7768
|
+
grouped[c.type].push(c);
|
|
7769
|
+
}
|
|
7770
|
+
warnings.push(...lintJargon(visible));
|
|
7771
|
+
return {
|
|
7772
|
+
from,
|
|
7773
|
+
to,
|
|
7774
|
+
repo,
|
|
7775
|
+
rawCommitCount,
|
|
7776
|
+
commits: parsed,
|
|
7777
|
+
clusters,
|
|
7778
|
+
grouped,
|
|
7779
|
+
headlineCandidates,
|
|
7780
|
+
warnings
|
|
7781
|
+
};
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7784
|
+
// src/commands/changelog-renderers/markdown.ts
|
|
7785
|
+
function safeSubject(s) {
|
|
7786
|
+
return s.replace(/[\r\n]+/g, " ").trim();
|
|
7787
|
+
}
|
|
7788
|
+
function renderMarkdown(g) {
|
|
7789
|
+
if (g.commits.length === 0) {
|
|
7790
|
+
const compare2 = g.repo ? `
|
|
7791
|
+
**Full Changelog**: https://github.com/${g.repo}/compare/${g.from}...${g.to}` : "";
|
|
7792
|
+
return `## What's Changed
|
|
7793
|
+
|
|
7794
|
+
_No notable changes._
|
|
7795
|
+
${compare2}`.trim();
|
|
7796
|
+
}
|
|
7797
|
+
const lines = ["## What's Changed", ""];
|
|
7798
|
+
let emittedAny = false;
|
|
7799
|
+
for (const type of SECTION_ORDER) {
|
|
7800
|
+
const commits = g.grouped[type] ?? [];
|
|
7801
|
+
if (commits.length === 0) continue;
|
|
7802
|
+
for (const commit of commits) {
|
|
7803
|
+
lines.push(`- ${type}: ${safeSubject(commit.subject)}`);
|
|
7804
|
+
emittedAny = true;
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
if (!emittedAny) {
|
|
7808
|
+
lines.push("_No notable changes._");
|
|
7809
|
+
}
|
|
7810
|
+
if (g.repo) {
|
|
7811
|
+
lines.push("");
|
|
7812
|
+
lines.push(`**Full Changelog**: https://github.com/${g.repo}/compare/${g.from}...${g.to}`);
|
|
7813
|
+
}
|
|
7814
|
+
return lines.join("\n");
|
|
7815
|
+
}
|
|
7816
|
+
|
|
7817
|
+
// src/commands/changelog-renderers/json.ts
|
|
7818
|
+
function renderJson(g) {
|
|
7819
|
+
return JSON.stringify(g, null, 2);
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
// src/commands/changelog-renderers/prompt.ts
|
|
7823
|
+
function safeLine(s) {
|
|
7824
|
+
return s.replace(/[\r\n]+/g, " ").trim();
|
|
7825
|
+
}
|
|
7826
|
+
function renderPrompt(g) {
|
|
7827
|
+
const lines = [];
|
|
7828
|
+
lines.push("You are writing a draft of GitHub Release notes from clustered git commits.");
|
|
7829
|
+
lines.push("");
|
|
7830
|
+
lines.push("VOICE RULES (from project conventions):");
|
|
7831
|
+
lines.push("- Terse, present tense, user-facing language");
|
|
7832
|
+
lines.push('- No internal jargon (no "mutex", "token bucket", "barrel exports", "homedir")');
|
|
7833
|
+
lines.push("- One bullet per feature/fix, not one per commit");
|
|
7834
|
+
lines.push("- Drop conventional-commit scopes (e.g., feat(cli) \u2192 feat:)");
|
|
7835
|
+
lines.push("- Open with a single-sentence highlight describing the release theme");
|
|
7836
|
+
lines.push("");
|
|
7837
|
+
lines.push(`RANGE: ${g.from}..${g.to}`);
|
|
7838
|
+
if (g.repo) lines.push(`REPO: ${g.repo}`);
|
|
7839
|
+
lines.push(`COMMITS: ${g.rawCommitCount} raw, ${g.clusters.length} clusters after dedup`);
|
|
7840
|
+
lines.push("");
|
|
7841
|
+
if (g.headlineCandidates.length > 0) {
|
|
7842
|
+
lines.push("HEADLINE CANDIDATES (largest first):");
|
|
7843
|
+
for (const c of g.headlineCandidates) {
|
|
7844
|
+
lines.push(` ${c.label} (weight ${c.weight}, ${c.commits.length} commits, primary ${c.primaryType})`);
|
|
7845
|
+
}
|
|
7846
|
+
lines.push("");
|
|
7847
|
+
}
|
|
7848
|
+
lines.push("CLUSTERED COMMITS:");
|
|
7849
|
+
lines.push("");
|
|
7850
|
+
for (const cluster of g.clusters) {
|
|
7851
|
+
lines.push(`[cluster: ${cluster.label}, weight ${cluster.weight}, ${cluster.commits.length} commits, primary ${cluster.primaryType}]`);
|
|
7852
|
+
for (const commit of cluster.commits) {
|
|
7853
|
+
lines.push(`- ${commit.type}: ${safeLine(commit.subject)} (${commit.sha.slice(0, 7)})`);
|
|
7854
|
+
if (commit.files.length > 0) {
|
|
7855
|
+
const fileSummary = commit.files.slice(0, 3).join(", ");
|
|
7856
|
+
const more = commit.files.length > 3 ? ` (+${commit.files.length - 3} more)` : "";
|
|
7857
|
+
lines.push(` files: ${fileSummary}${more}`);
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7860
|
+
lines.push("");
|
|
7861
|
+
}
|
|
7862
|
+
if (g.warnings.length > 0) {
|
|
7863
|
+
lines.push("LINTER WARNINGS (review before publishing):");
|
|
7864
|
+
for (const w of g.warnings) lines.push(` - ${w}`);
|
|
7865
|
+
lines.push("");
|
|
7866
|
+
}
|
|
7867
|
+
const compare2 = g.repo ? `https://github.com/${g.repo}/compare/${g.from}...${g.to}` : `${g.from}..${g.to}`;
|
|
7868
|
+
lines.push("OUTPUT FORMAT (match exactly):");
|
|
7869
|
+
lines.push("```markdown");
|
|
7870
|
+
lines.push("<one-sentence highlight>");
|
|
7871
|
+
lines.push("");
|
|
7872
|
+
lines.push("## What's Changed");
|
|
7873
|
+
lines.push("");
|
|
7874
|
+
lines.push("- breaking: ...");
|
|
7875
|
+
lines.push("- feat: ...");
|
|
7876
|
+
lines.push("- fix: ...");
|
|
7877
|
+
lines.push("- perf: ...");
|
|
7878
|
+
lines.push("");
|
|
7879
|
+
lines.push(`**Full Changelog**: ${compare2}`);
|
|
7880
|
+
lines.push("```");
|
|
7881
|
+
return lines.join("\n");
|
|
7882
|
+
}
|
|
7883
|
+
|
|
7884
|
+
// src/commands/changelog-renderers/play-store.ts
|
|
7885
|
+
var PLAY_STORE_LIMIT = 500;
|
|
7886
|
+
var PLACEHOLDER_TEXT = "[needs translation \u2014 pass --ai once v0.9.63 ships, or paste the prompt emitted by --format prompt]";
|
|
7887
|
+
function safeLine2(s) {
|
|
7888
|
+
return s.replace(/[\r\n]+/g, " ").trim();
|
|
7889
|
+
}
|
|
7890
|
+
function countChars(s) {
|
|
7891
|
+
return [...s].length;
|
|
7892
|
+
}
|
|
7893
|
+
function truncateToLimit(text, limit) {
|
|
7894
|
+
if (countChars(text) <= limit) return text;
|
|
7895
|
+
const chars = [...text];
|
|
7896
|
+
return chars.slice(0, limit - 1).join("") + "\u2026";
|
|
7897
|
+
}
|
|
7898
|
+
function renderEnglishBullets(g) {
|
|
7899
|
+
const lines = [];
|
|
7900
|
+
for (const type of SECTION_ORDER) {
|
|
7901
|
+
const commits = g.grouped[type] ?? [];
|
|
7902
|
+
for (const commit of commits) {
|
|
7903
|
+
lines.push(`- ${type}: ${safeLine2(commit.subject)}`);
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
return lines.join("\n");
|
|
7907
|
+
}
|
|
7908
|
+
function buildLocaleBundle(g, opts) {
|
|
7909
|
+
const sourceLanguage = opts.sourceLanguage ?? "en-US";
|
|
7910
|
+
const sourceText = renderEnglishBullets(g);
|
|
7911
|
+
const isEmpty = sourceText.length === 0;
|
|
7912
|
+
const overflows = [];
|
|
7913
|
+
const entries = opts.locales.map((language) => {
|
|
7914
|
+
if (language === sourceLanguage) {
|
|
7915
|
+
if (isEmpty) {
|
|
7916
|
+
const emptyText = "No notable changes.";
|
|
7917
|
+
return {
|
|
7918
|
+
language,
|
|
7919
|
+
text: emptyText,
|
|
7920
|
+
chars: countChars(emptyText),
|
|
7921
|
+
limit: PLAY_STORE_LIMIT,
|
|
7922
|
+
status: "empty"
|
|
7923
|
+
};
|
|
7924
|
+
}
|
|
7925
|
+
const chars = countChars(sourceText);
|
|
7926
|
+
const status = chars > PLAY_STORE_LIMIT ? "over" : "ok";
|
|
7927
|
+
if (status === "over") overflows.push(language);
|
|
7928
|
+
const text = status === "over" ? truncateToLimit(sourceText, PLAY_STORE_LIMIT) : sourceText;
|
|
7929
|
+
return {
|
|
7930
|
+
language,
|
|
7931
|
+
text,
|
|
7932
|
+
chars,
|
|
7933
|
+
limit: PLAY_STORE_LIMIT,
|
|
7934
|
+
status
|
|
7935
|
+
};
|
|
7936
|
+
}
|
|
7937
|
+
return {
|
|
7938
|
+
language,
|
|
7939
|
+
text: PLACEHOLDER_TEXT,
|
|
7940
|
+
chars: countChars(PLACEHOLDER_TEXT),
|
|
7941
|
+
limit: PLAY_STORE_LIMIT,
|
|
7942
|
+
status: "placeholder"
|
|
7943
|
+
};
|
|
7944
|
+
});
|
|
7945
|
+
return {
|
|
7946
|
+
from: g.from,
|
|
7947
|
+
to: g.to,
|
|
7948
|
+
limit: PLAY_STORE_LIMIT,
|
|
7949
|
+
sourceLanguage,
|
|
7950
|
+
locales: entries,
|
|
7951
|
+
overflows
|
|
7952
|
+
};
|
|
7953
|
+
}
|
|
7954
|
+
function renderPlayStoreMd(bundle) {
|
|
7955
|
+
const lines = [];
|
|
7956
|
+
lines.push(`# Play Store release notes (${bundle.from} \u2192 ${bundle.to})`);
|
|
7957
|
+
lines.push("");
|
|
7958
|
+
for (const entry of bundle.locales) {
|
|
7959
|
+
let heading;
|
|
7960
|
+
if (entry.status === "placeholder") {
|
|
7961
|
+
heading = `## ${entry.language} (needs translation)`;
|
|
7962
|
+
} else if (entry.status === "empty") {
|
|
7963
|
+
heading = `## ${entry.language} (empty)`;
|
|
7964
|
+
} else {
|
|
7965
|
+
const suffix = entry.status === "over" ? ` \u26A0 truncated` : "";
|
|
7966
|
+
heading = `## ${entry.language} (${entry.chars}/${entry.limit})${suffix}`;
|
|
7967
|
+
}
|
|
7968
|
+
lines.push(heading);
|
|
7969
|
+
lines.push(entry.text);
|
|
7970
|
+
lines.push("");
|
|
7971
|
+
}
|
|
7972
|
+
lines.push("## Summary");
|
|
7973
|
+
for (const entry of bundle.locales) {
|
|
7974
|
+
if (entry.status === "placeholder") {
|
|
7975
|
+
lines.push(`- ${entry.language}: placeholder`);
|
|
7976
|
+
} else if (entry.status === "empty") {
|
|
7977
|
+
lines.push(`- ${entry.language}: empty`);
|
|
7978
|
+
} else {
|
|
7979
|
+
const mark = entry.status === "over" ? "\u2717 over limit" : "\u2713";
|
|
7980
|
+
lines.push(`- ${entry.language}: ${entry.chars}/${entry.limit} ${mark}`);
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
return lines.join("\n");
|
|
7984
|
+
}
|
|
7985
|
+
function renderPlayStoreJson(bundle) {
|
|
7986
|
+
return JSON.stringify(bundle, null, 2);
|
|
7987
|
+
}
|
|
7988
|
+
function renderPlayStorePrompt(bundle, g) {
|
|
7989
|
+
const source = bundle.locales.find((e) => e.language === bundle.sourceLanguage);
|
|
7990
|
+
const targets = bundle.locales.filter((e) => e.language !== bundle.sourceLanguage);
|
|
7991
|
+
const lines = [];
|
|
7992
|
+
lines.push(`You are translating Play Store "What's new" release notes from ${bundle.sourceLanguage}.`);
|
|
7993
|
+
lines.push("");
|
|
7994
|
+
lines.push("TARGETS:");
|
|
7995
|
+
for (const t of targets) lines.push(` - ${t.language}`);
|
|
7996
|
+
if (targets.length === 0) lines.push(" (none \u2014 source-only bundle)");
|
|
7997
|
+
lines.push("");
|
|
7998
|
+
lines.push("CONSTRAINTS:");
|
|
7999
|
+
lines.push(`- Each translation MUST be \u2264 ${bundle.limit} Unicode code points`);
|
|
8000
|
+
lines.push('- Preserve the bullet format (one item per line, starts with "- ")');
|
|
8001
|
+
lines.push("- Keep a user-facing tone (no internal jargon)");
|
|
8002
|
+
lines.push('- Do not translate technical names (package names, CLI flags, "GPC")');
|
|
8003
|
+
lines.push("- Drop the conventional-commit prefix (feat:/fix:) if it feels unnatural in the target language");
|
|
8004
|
+
lines.push("");
|
|
8005
|
+
lines.push(`SOURCE (${bundle.sourceLanguage}, ${source?.chars ?? 0}/${bundle.limit} chars):`);
|
|
8006
|
+
lines.push("```");
|
|
8007
|
+
lines.push(source?.text ?? "(empty)");
|
|
8008
|
+
lines.push("```");
|
|
8009
|
+
lines.push("");
|
|
8010
|
+
if (g.headlineCandidates.length > 0) {
|
|
8011
|
+
lines.push("CONTEXT (clusters, for theme awareness):");
|
|
8012
|
+
for (const c of g.headlineCandidates) {
|
|
8013
|
+
lines.push(` - ${c.label} (${c.commits.length} commits, primary ${c.primaryType})`);
|
|
8014
|
+
}
|
|
8015
|
+
lines.push("");
|
|
8016
|
+
}
|
|
8017
|
+
lines.push("OUTPUT FORMAT (one heading + body per target language):");
|
|
8018
|
+
lines.push("```markdown");
|
|
8019
|
+
for (const t of targets) {
|
|
8020
|
+
lines.push(`## ${t.language}`);
|
|
8021
|
+
lines.push("<translation>");
|
|
8022
|
+
lines.push("");
|
|
8023
|
+
}
|
|
8024
|
+
if (targets.length === 0) {
|
|
8025
|
+
lines.push("(no target locales \u2014 nothing to translate)");
|
|
8026
|
+
}
|
|
8027
|
+
lines.push("```");
|
|
8028
|
+
return lines.join("\n");
|
|
8029
|
+
}
|
|
8030
|
+
function renderPlayStore(g, opts) {
|
|
8031
|
+
const bundle = buildLocaleBundle(g, opts);
|
|
8032
|
+
switch (opts.format) {
|
|
8033
|
+
case "md":
|
|
8034
|
+
return { output: renderPlayStoreMd(bundle), bundle };
|
|
8035
|
+
case "json":
|
|
8036
|
+
return { output: renderPlayStoreJson(bundle), bundle };
|
|
8037
|
+
case "prompt":
|
|
8038
|
+
return { output: renderPlayStorePrompt(bundle, g), bundle };
|
|
8039
|
+
}
|
|
8040
|
+
}
|
|
8041
|
+
|
|
8042
|
+
// src/commands/changelog-renderers/index.ts
|
|
8043
|
+
var RENDERERS = {
|
|
8044
|
+
md: renderMarkdown,
|
|
8045
|
+
json: renderJson,
|
|
8046
|
+
prompt: renderPrompt
|
|
8047
|
+
};
|
|
8048
|
+
|
|
8049
|
+
// src/commands/changelog-locales.ts
|
|
8050
|
+
async function resolveLocales(input, options = {}) {
|
|
8051
|
+
const trimmed = input.trim();
|
|
8052
|
+
if (!trimmed) {
|
|
8053
|
+
throw new GpcError(
|
|
8054
|
+
"--locales is empty",
|
|
8055
|
+
"CHANGELOG_LOCALES_REQUIRED",
|
|
8056
|
+
2,
|
|
8057
|
+
"Pass a comma-separated list (e.g., --locales en-US,fr-FR) or --locales auto."
|
|
8058
|
+
);
|
|
8059
|
+
}
|
|
8060
|
+
if (trimmed === "auto") {
|
|
8061
|
+
const { client, packageName } = options;
|
|
8062
|
+
if (!client || !packageName) {
|
|
8063
|
+
throw new GpcError(
|
|
8064
|
+
"--locales auto requires an authenticated API client and --app",
|
|
8065
|
+
"CHANGELOG_LOCALES_AUTO_NO_APP",
|
|
8066
|
+
2,
|
|
8067
|
+
"Pass --app <package> or set config.app, and ensure credentials are configured."
|
|
8068
|
+
);
|
|
8069
|
+
}
|
|
8070
|
+
const edit = await client.edits.insert(packageName);
|
|
8071
|
+
try {
|
|
8072
|
+
const listings = await client.listings.list(packageName, edit.id);
|
|
8073
|
+
await client.edits.delete(packageName, edit.id);
|
|
8074
|
+
const langs = listings.map((l) => l.language);
|
|
8075
|
+
if (langs.length === 0) {
|
|
8076
|
+
throw new GpcError(
|
|
8077
|
+
`No Play Store listings found for ${packageName}`,
|
|
8078
|
+
"CHANGELOG_LOCALES_EMPTY",
|
|
8079
|
+
1,
|
|
8080
|
+
"Create at least one listing in Play Console, or pass --locales explicitly."
|
|
8081
|
+
);
|
|
8082
|
+
}
|
|
8083
|
+
return langs;
|
|
8084
|
+
} catch (error) {
|
|
8085
|
+
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
8086
|
+
});
|
|
8087
|
+
throw error;
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
const locales = trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
8091
|
+
if (locales.length === 0) {
|
|
8092
|
+
throw new GpcError(
|
|
8093
|
+
"--locales parsed to an empty list",
|
|
8094
|
+
"CHANGELOG_LOCALES_REQUIRED",
|
|
8095
|
+
2,
|
|
8096
|
+
"Pass a comma-separated list (e.g., --locales en-US,fr-FR) or --locales auto."
|
|
8097
|
+
);
|
|
8098
|
+
}
|
|
8099
|
+
const invalid = locales.filter((l) => !isValidBcp47(l));
|
|
8100
|
+
if (invalid.length > 0) {
|
|
8101
|
+
throw new GpcError(
|
|
8102
|
+
`Invalid locale(s): ${invalid.join(", ")}`,
|
|
8103
|
+
"CHANGELOG_LOCALES_INVALID",
|
|
8104
|
+
2,
|
|
8105
|
+
"Use BCP 47 codes recognized by Google Play (e.g., en-US, fr-FR, de-DE)."
|
|
8106
|
+
);
|
|
8107
|
+
}
|
|
8108
|
+
return locales;
|
|
8109
|
+
}
|
|
8110
|
+
|
|
7364
8111
|
// src/commands/rtdn.ts
|
|
7365
8112
|
var SUBSCRIPTION_NOTIFICATION_TYPES = {
|
|
7366
8113
|
1: "SUBSCRIPTION_RECOVERED",
|
|
@@ -7462,7 +8209,11 @@ export {
|
|
|
7462
8209
|
GpcError,
|
|
7463
8210
|
NetworkError,
|
|
7464
8211
|
PERMISSION_PROPAGATION_WARNING,
|
|
8212
|
+
PLACEHOLDER_TEXT,
|
|
8213
|
+
PLAY_STORE_LIMIT,
|
|
7465
8214
|
PluginManager,
|
|
8215
|
+
RENDERERS,
|
|
8216
|
+
SECTION_ORDER,
|
|
7466
8217
|
SENSITIVE_ARG_KEYS,
|
|
7467
8218
|
SENSITIVE_KEYS,
|
|
7468
8219
|
SEVERITY_ORDER,
|
|
@@ -7478,6 +8229,7 @@ export {
|
|
|
7478
8229
|
analyzeReviews2 as analyzeReviews,
|
|
7479
8230
|
batchGetOrders,
|
|
7480
8231
|
batchSyncInAppProducts,
|
|
8232
|
+
buildLocaleBundle,
|
|
7481
8233
|
cancelRecoveryAction,
|
|
7482
8234
|
cancelSubscriptionPurchase,
|
|
7483
8235
|
cancelSubscriptionV2,
|
|
@@ -7506,6 +8258,7 @@ export {
|
|
|
7506
8258
|
deactivateBasePlan,
|
|
7507
8259
|
deactivateOffer,
|
|
7508
8260
|
decodeNotification,
|
|
8261
|
+
defaultGitRunner,
|
|
7509
8262
|
deferSubscriptionPurchase,
|
|
7510
8263
|
deferSubscriptionV2,
|
|
7511
8264
|
deleteBasePlan,
|
|
@@ -7544,6 +8297,7 @@ export {
|
|
|
7544
8297
|
formatStatusSummary,
|
|
7545
8298
|
formatStatusTable,
|
|
7546
8299
|
formatWordDiff,
|
|
8300
|
+
generateChangelog,
|
|
7547
8301
|
generateMigrationPlan,
|
|
7548
8302
|
generateNotesFromGit,
|
|
7549
8303
|
getAllScannerNames,
|
|
@@ -7617,9 +8371,11 @@ export {
|
|
|
7617
8371
|
maybePaginate,
|
|
7618
8372
|
migratePrices,
|
|
7619
8373
|
parseAppfile,
|
|
8374
|
+
parseCommit,
|
|
7620
8375
|
parseFastfile,
|
|
7621
8376
|
parseGrantArg,
|
|
7622
8377
|
parseMonth,
|
|
8378
|
+
parseRemoteUrl,
|
|
7623
8379
|
pauseTrain,
|
|
7624
8380
|
promoteRelease,
|
|
7625
8381
|
publish,
|
|
@@ -7635,7 +8391,15 @@ export {
|
|
|
7635
8391
|
relativeTime,
|
|
7636
8392
|
removeTesters,
|
|
7637
8393
|
removeUser,
|
|
8394
|
+
renderJson,
|
|
8395
|
+
renderMarkdown,
|
|
8396
|
+
renderPlayStore,
|
|
8397
|
+
renderPlayStoreJson,
|
|
8398
|
+
renderPlayStoreMd,
|
|
8399
|
+
renderPlayStorePrompt,
|
|
8400
|
+
renderPrompt,
|
|
7638
8401
|
replyToReview,
|
|
8402
|
+
resolveLocales,
|
|
7639
8403
|
revokeSubscriptionPurchase,
|
|
7640
8404
|
runPreflight,
|
|
7641
8405
|
runWatchLoop,
|