@cellajs/cli 0.3.0 → 0.3.2
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/README.md +150 -30
- package/dist/config.d.ts +14 -22
- package/dist/config.js +13 -14
- package/dist/config.js.map +1 -1
- package/dist/index.js +1247 -723
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cella-cli.ts
|
|
4
4
|
import { existsSync as existsSync13, statSync as statSync3 } from "fs";
|
|
5
|
-
import { join as
|
|
5
|
+
import { join as join19, resolve as resolve8 } from "path";
|
|
6
6
|
import process9 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import { readFileSync } from "fs";
|
|
10
|
-
import { join as join8 } from "path";
|
|
11
9
|
import process5 from "process";
|
|
12
10
|
import { select } from "@inquirer/prompts";
|
|
13
11
|
import { Command } from "commander";
|
|
@@ -53,32 +51,31 @@ var pc = {
|
|
|
53
51
|
var colors_default = pc;
|
|
54
52
|
|
|
55
53
|
// src/utils/config.ts
|
|
56
|
-
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
54
|
+
import { existsSync as existsSync2, readFileSync, realpathSync } from "fs";
|
|
57
55
|
import { join as join2, resolve } from "path";
|
|
58
56
|
import { z as z2 } from "zod";
|
|
59
57
|
|
|
60
58
|
// src/config/types.ts
|
|
61
59
|
import { z } from "zod";
|
|
60
|
+
var PACKAGE_JSON_SYNC_KEYS = [
|
|
61
|
+
"dependencies",
|
|
62
|
+
"devDependencies",
|
|
63
|
+
"peerDependencies",
|
|
64
|
+
"optionalDependencies",
|
|
65
|
+
"scripts",
|
|
66
|
+
"engines",
|
|
67
|
+
"packageManager",
|
|
68
|
+
"overrides",
|
|
69
|
+
"exports",
|
|
70
|
+
"pnpm"
|
|
71
|
+
];
|
|
62
72
|
var cellaConfigSchema = z.object({
|
|
63
73
|
settings: z.object({
|
|
64
74
|
upstreamUrl: z.string().min(1),
|
|
65
75
|
upstreamBranch: z.string().min(1).optional(),
|
|
66
76
|
upstreamTrack: z.enum(["release", "branch"]).optional(),
|
|
67
77
|
releaseBase: z.string().min(1).optional(),
|
|
68
|
-
packageJsonSync: z.array(
|
|
69
|
-
z.enum([
|
|
70
|
-
"dependencies",
|
|
71
|
-
"devDependencies",
|
|
72
|
-
"peerDependencies",
|
|
73
|
-
"optionalDependencies",
|
|
74
|
-
"scripts",
|
|
75
|
-
"engines",
|
|
76
|
-
"packageManager",
|
|
77
|
-
"overrides",
|
|
78
|
-
"exports",
|
|
79
|
-
"pnpm"
|
|
80
|
-
])
|
|
81
|
-
).optional(),
|
|
78
|
+
packageJsonSync: z.array(z.enum(PACKAGE_JSON_SYNC_KEYS)).optional(),
|
|
82
79
|
syncWithPackages: z.boolean().optional(),
|
|
83
80
|
fileLinkMode: z.enum(["commit", "file", "local"]).optional()
|
|
84
81
|
}).strict(),
|
|
@@ -95,6 +92,12 @@ var cellaConfigSchema = z.object({
|
|
|
95
92
|
}).strict()
|
|
96
93
|
).optional()
|
|
97
94
|
}).strict();
|
|
95
|
+
var SYNC_APPLIED_STATUSES = ["behind", "diverged", "renamed", "ignored", "pinned"];
|
|
96
|
+
|
|
97
|
+
// src/utils/errors.ts
|
|
98
|
+
function errorMessage(error) {
|
|
99
|
+
return error instanceof Error ? error.message : String(error);
|
|
100
|
+
}
|
|
98
101
|
|
|
99
102
|
// src/utils/managed-files.ts
|
|
100
103
|
var CONFIG_FILE = "cella/cella.config.ts";
|
|
@@ -160,6 +163,10 @@ var DEFAULT_UPSTREAM_REMOTE = "cella-upstream";
|
|
|
160
163
|
var DEFAULT_BRANCH = "main";
|
|
161
164
|
var DEFAULT_SYNC_PREFIX = "cella/sync";
|
|
162
165
|
var DEFAULT_RELEASE_BASE = "main";
|
|
166
|
+
var DEFAULT_PACKAGE_JSON_SYNC = ["dependencies", "devDependencies"];
|
|
167
|
+
function isUpstreamRepo(repoPath) {
|
|
168
|
+
return JSON.parse(readFileSync(join2(repoPath, "package.json"), "utf8")).name === "cella";
|
|
169
|
+
}
|
|
163
170
|
function isTemporarySyncBranch(branch) {
|
|
164
171
|
return branch.startsWith(`${DEFAULT_SYNC_PREFIX}/`);
|
|
165
172
|
}
|
|
@@ -192,8 +199,7 @@ async function loadConfig(forkPath) {
|
|
|
192
199
|
try {
|
|
193
200
|
configModule = await import(realConfigPath);
|
|
194
201
|
} catch (error) {
|
|
195
|
-
|
|
196
|
-
throw new Error(`failed to load ${configPath}: ${message}`);
|
|
202
|
+
throw new Error(`failed to load ${configPath}: ${errorMessage(error)}`);
|
|
197
203
|
}
|
|
198
204
|
const result = cellaConfigSchema.safeParse(configModule.default);
|
|
199
205
|
if (!result.success) {
|
|
@@ -220,7 +226,7 @@ import process4 from "process";
|
|
|
220
226
|
// package.json
|
|
221
227
|
var package_default = {
|
|
222
228
|
name: "@cellajs/cli",
|
|
223
|
-
version: "0.3.
|
|
229
|
+
version: "0.3.2",
|
|
224
230
|
private: false,
|
|
225
231
|
license: "MIT",
|
|
226
232
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -287,7 +293,7 @@ var package_default = {
|
|
|
287
293
|
rimraf: "^6.1.3",
|
|
288
294
|
tsup: "^8.5.1",
|
|
289
295
|
tsx: "^4.23.15",
|
|
290
|
-
typescript: "
|
|
296
|
+
typescript: "6.0.3",
|
|
291
297
|
vitest: "^5.0.3"
|
|
292
298
|
},
|
|
293
299
|
publishConfig: {
|
|
@@ -346,6 +352,8 @@ var package_default = {
|
|
|
346
352
|
var NAME = "cella cli";
|
|
347
353
|
var VERSION = package_default.version;
|
|
348
354
|
var DIVIDER = "\u2500".repeat(60);
|
|
355
|
+
var MENU_DIVIDER = "\u2500".repeat(40);
|
|
356
|
+
var COMMIT_LIST_MAX = 50;
|
|
349
357
|
var warningMark = colors_default.yellow("\u26A0");
|
|
350
358
|
var checkMark = colors_default.green("\u2713");
|
|
351
359
|
var TerminalSpinner = class {
|
|
@@ -454,7 +462,7 @@ function getVsCodeOpenFileLink(filePath, options) {
|
|
|
454
462
|
const url = `vscode://file${absolutePath}`;
|
|
455
463
|
return hyperlink(filePath, url);
|
|
456
464
|
}
|
|
457
|
-
function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, maxShownCommits =
|
|
465
|
+
function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, maxShownCommits = COMMIT_LIST_MAX) {
|
|
458
466
|
const commitLabel = commitCount === 1 ? "1 new commit" : `${commitCount} new commits`;
|
|
459
467
|
const lines = [`${commitLabel} since last merge`];
|
|
460
468
|
const shown = commits.slice(0, maxShownCommits);
|
|
@@ -538,19 +546,7 @@ var summaryStatusConfig = {
|
|
|
538
546
|
var statusOrder = Object.keys(statusConfig);
|
|
539
547
|
function printSummary(summary, title = "summary") {
|
|
540
548
|
printSectionHeader(colors_default.cyan(title));
|
|
541
|
-
const maxCount = Math.max(
|
|
542
|
-
summary.managed,
|
|
543
|
-
summary.ignored,
|
|
544
|
-
summary.identical,
|
|
545
|
-
summary.ahead,
|
|
546
|
-
summary.local,
|
|
547
|
-
summary.drifted,
|
|
548
|
-
summary.behind,
|
|
549
|
-
summary.diverged,
|
|
550
|
-
summary.pinned,
|
|
551
|
-
summary.deleted,
|
|
552
|
-
summary.renamed
|
|
553
|
-
);
|
|
549
|
+
const maxCount = Math.max(...statusOrder.map((status) => summary[status]), summary.managed);
|
|
554
550
|
const countWidth = String(maxCount).length + 1;
|
|
555
551
|
const printLine = (status, count) => {
|
|
556
552
|
const { icon, label, color, description } = summaryStatusConfig[status];
|
|
@@ -613,11 +609,23 @@ function formatUpstreamChangedLines(file) {
|
|
|
613
609
|
const n = file.upstreamChangedLines;
|
|
614
610
|
return colors_default.yellow(` \xB7 ${n} ${n === 1 ? "line" : "lines"} changed upstream`);
|
|
615
611
|
}
|
|
612
|
+
function protectedDiffHints(source, paths) {
|
|
613
|
+
const range = source?.upstreamDiffRange;
|
|
614
|
+
if (!range) return [];
|
|
615
|
+
const listed = new Set(paths);
|
|
616
|
+
const groups = (source?.protectedUpstreamChanges ?? []).filter((group) => group.paths.some((p) => listed.has(p)));
|
|
617
|
+
if (groups.length === 0) return [];
|
|
618
|
+
return [
|
|
619
|
+
"what upstream changed since the last sync, per pinned/ignored entry:",
|
|
620
|
+
...groups.map((group) => ` git diff ${range} -- ${shellPath(group.entry)}`)
|
|
621
|
+
];
|
|
622
|
+
}
|
|
616
623
|
function formatUpstreamLinesAbsent(file) {
|
|
617
624
|
const n = file.upstreamLinesAbsent ?? 0;
|
|
618
625
|
return n > 0 ? colors_default.yellow(` \xB7 ${n} upstream ${n === 1 ? "line" : "lines"} absent`) : "";
|
|
619
626
|
}
|
|
620
|
-
function printAnalysisFileGroups(files, linkOptions) {
|
|
627
|
+
function printAnalysisFileGroups(files, linkOptions, diffSource) {
|
|
628
|
+
const protectedBehind = findProtectedBehind(files);
|
|
621
629
|
printFileGroup(files, "behind", linkOptions, {
|
|
622
630
|
title: colors_default.cyan("\u2193 behind on upstream")
|
|
623
631
|
});
|
|
@@ -629,17 +637,18 @@ function printAnalysisFileGroups(files, linkOptions) {
|
|
|
629
637
|
"pinned files keep the fork side on every conflict; a count here is upstream content the fork never received, deliberately or not: diff and decide."
|
|
630
638
|
]
|
|
631
639
|
});
|
|
632
|
-
printFileSection(
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
640
|
+
printFileSection(protectedBehind, `${warningMark} ${colors_default.yellow("protected but behind upstream")}`, linkOptions, {
|
|
641
|
+
icon: protectionIcon,
|
|
642
|
+
suffix: formatUpstreamChangedLines,
|
|
643
|
+
dateSource: "upstream",
|
|
644
|
+
hint: [
|
|
645
|
+
PROTECTED_BEHIND_HINT,
|
|
646
|
+
...protectedDiffHints(
|
|
647
|
+
diffSource,
|
|
648
|
+
protectedBehind.map((file) => file.path)
|
|
649
|
+
)
|
|
650
|
+
]
|
|
651
|
+
});
|
|
643
652
|
printFileGroup(files, "drifted", linkOptions, {
|
|
644
653
|
title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
645
654
|
hint: "these files have fork changes but are not pinned or ignored."
|
|
@@ -653,7 +662,7 @@ function printAnalysisFileGroups(files, linkOptions) {
|
|
|
653
662
|
function writeLogFile(forkPath, files) {
|
|
654
663
|
const logPath = join3(forkPath, "cella-sync.log");
|
|
655
664
|
const lines = [
|
|
656
|
-
`cella sync analysis
|
|
665
|
+
`cella sync analysis: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
657
666
|
DIVIDER,
|
|
658
667
|
"",
|
|
659
668
|
`complete file list (${files.length} files)`,
|
|
@@ -671,7 +680,7 @@ function writeLogFile(forkPath, files) {
|
|
|
671
680
|
const icon = config.icon.replace(/\x1b\[[0-9;]*m/g, "");
|
|
672
681
|
lines.push(` ${icon} ${config.label.padEnd(12)} ${file.path}`);
|
|
673
682
|
}
|
|
674
|
-
writeFileSync(logPath, lines.join("\n"), "
|
|
683
|
+
writeFileSync(logPath, lines.join("\n"), "utf8");
|
|
675
684
|
return logPath;
|
|
676
685
|
}
|
|
677
686
|
function printSyncComplete(result, options = {}) {
|
|
@@ -681,12 +690,12 @@ function printSyncComplete(result, options = {}) {
|
|
|
681
690
|
const conflicts = result.conflicts.length;
|
|
682
691
|
console.info();
|
|
683
692
|
if (options.stagedBranch) {
|
|
684
|
-
console.info(`${
|
|
693
|
+
console.info(`${checkMark} sync merge staged on '${options.stagedBranch}'`);
|
|
685
694
|
console.info(
|
|
686
|
-
colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts.
|
|
695
|
+
colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. committing next...`)
|
|
687
696
|
);
|
|
688
697
|
} else {
|
|
689
|
-
console.info(`${
|
|
698
|
+
console.info(`${checkMark} sync complete`);
|
|
690
699
|
console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
|
|
691
700
|
}
|
|
692
701
|
printProtectedConflicts(result);
|
|
@@ -702,18 +711,18 @@ function printProtectedConflicts(result) {
|
|
|
702
711
|
`${paths.length} protected ${paths.length === 1 ? "file kept" : "files kept"} the fork version, dropping upstream changes:`
|
|
703
712
|
)}`
|
|
704
713
|
);
|
|
705
|
-
for (const
|
|
706
|
-
const file = byPath.get(
|
|
714
|
+
for (const path of paths) {
|
|
715
|
+
const file = byPath.get(path);
|
|
707
716
|
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
708
|
-
console.info(` ${icon} ${
|
|
717
|
+
console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
709
718
|
}
|
|
710
|
-
console.info(colors_default.dim(` ${
|
|
719
|
+
for (const line of [PROTECTED_BEHIND_HINT, ...protectedDiffHints(result, paths)]) console.info(colors_default.dim(` ${line}`));
|
|
711
720
|
}
|
|
712
721
|
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
713
722
|
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
714
723
|
}
|
|
715
|
-
function shellPath(
|
|
716
|
-
return /^[\w./@+-]+$/.test(
|
|
724
|
+
function shellPath(path) {
|
|
725
|
+
return /^[\w./@+-]+$/.test(path) ? path : `'${path.replace(/'/g, `'\\''`)}'`;
|
|
717
726
|
}
|
|
718
727
|
function printIgnoredUpstreamChanges(result) {
|
|
719
728
|
const groups = result.ignoredUpstreamChanges ?? [];
|
|
@@ -741,19 +750,19 @@ function printUpstreamOverrideChanges(result) {
|
|
|
741
750
|
if (report.kind === "unreadable") {
|
|
742
751
|
const side = report.side === "base" ? "the last sync point" : "the incoming ref";
|
|
743
752
|
console.info(
|
|
744
|
-
colors_default.dim(`upstream
|
|
753
|
+
colors_default.dim(`upstream sync config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
|
|
745
754
|
);
|
|
746
755
|
return;
|
|
747
756
|
}
|
|
748
757
|
const lines = [];
|
|
749
|
-
for (const list of ["pinned", "ignored"]) {
|
|
758
|
+
for (const list of ["pinned", "ignored", "packageJsonSync"]) {
|
|
750
759
|
for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
|
|
751
760
|
for (const entry of report[list].removed) {
|
|
752
761
|
lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
|
|
753
762
|
}
|
|
754
763
|
}
|
|
755
764
|
console.info(
|
|
756
|
-
`${warningMark} ${colors_default.yellow(`upstream changed its sync
|
|
765
|
+
`${warningMark} ${colors_default.yellow(`upstream changed its sync config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
|
|
757
766
|
);
|
|
758
767
|
for (const line of lines) console.info(line);
|
|
759
768
|
console.info(colors_default.dim(` ${CONFIG_FILE} never syncs: add or drop these by hand where they fit your app.`));
|
|
@@ -770,7 +779,7 @@ function printMaskingPinWarning(files) {
|
|
|
770
779
|
console.info();
|
|
771
780
|
console.info(
|
|
772
781
|
`${warningMark} ${colors_default.yellow(
|
|
773
|
-
`${masking.length} pinned ${many ? "files match" : "file matches"} the previous upstream but changed upstream
|
|
782
|
+
`${masking.length} pinned ${many ? "files match" : "file matches"} the previous upstream but changed upstream:`
|
|
774
783
|
)}`
|
|
775
784
|
);
|
|
776
785
|
console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
|
|
@@ -778,7 +787,7 @@ function printMaskingPinWarning(files) {
|
|
|
778
787
|
console.info(colors_default.dim(` \u2A00 ${file.path}`));
|
|
779
788
|
}
|
|
780
789
|
console.info(
|
|
781
|
-
colors_default.yellow("
|
|
790
|
+
colors_default.yellow(" if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
|
|
782
791
|
);
|
|
783
792
|
console.info();
|
|
784
793
|
}
|
|
@@ -795,33 +804,40 @@ function printFlagWarnings(options) {
|
|
|
795
804
|
)
|
|
796
805
|
);
|
|
797
806
|
}
|
|
798
|
-
console.info(colors_default.yellow("
|
|
807
|
+
console.info(colors_default.yellow(" be extra careful and cherry-pick what you want only."));
|
|
808
|
+
console.info();
|
|
809
|
+
}
|
|
810
|
+
function printEngineReports(result, title) {
|
|
811
|
+
printSummary(result.summary, title);
|
|
812
|
+
printIgnoredUpstreamChanges(result);
|
|
813
|
+
printUpstreamOverrideChanges(result);
|
|
814
|
+
printMaskingPinWarning(result.files);
|
|
815
|
+
}
|
|
816
|
+
function printLogFileReport(config, files) {
|
|
817
|
+
if (!config.logFile) return;
|
|
818
|
+
const logPath = writeLogFile(config.forkPath, files);
|
|
799
819
|
console.info();
|
|
820
|
+
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
800
821
|
}
|
|
801
822
|
|
|
802
823
|
// src/utils/overrides.ts
|
|
803
824
|
import { existsSync as existsSync5 } from "fs";
|
|
804
825
|
import { join as join7 } from "path";
|
|
805
826
|
|
|
806
|
-
// src/utils/migration-notes.ts
|
|
807
|
-
import { existsSync as existsSync4 } from "fs";
|
|
808
|
-
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
809
|
-
import { dirname as dirname3, join as join6 } from "path";
|
|
810
|
-
|
|
811
827
|
// src/utils/git.ts
|
|
812
828
|
import { execFile } from "child_process";
|
|
813
829
|
import { existsSync as existsSync3 } from "fs";
|
|
814
830
|
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
815
|
-
import { dirname as dirname2, join as join5 } from "path";
|
|
831
|
+
import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
|
|
816
832
|
import { promisify } from "util";
|
|
817
833
|
|
|
818
834
|
// src/utils/manifest.ts
|
|
819
835
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
820
836
|
import { dirname, join as join4 } from "path";
|
|
821
837
|
var MANIFEST_FILE = "cella/cella.manifest.json";
|
|
822
|
-
|
|
838
|
+
function parseSyncManifest(raw) {
|
|
823
839
|
try {
|
|
824
|
-
const parsed = JSON.parse(
|
|
840
|
+
const parsed = JSON.parse(raw);
|
|
825
841
|
const commit = parsed?.upstream?.commit;
|
|
826
842
|
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
827
843
|
return parsed;
|
|
@@ -829,6 +845,13 @@ async function readSyncManifest(cwd) {
|
|
|
829
845
|
return null;
|
|
830
846
|
}
|
|
831
847
|
}
|
|
848
|
+
async function readSyncManifest(cwd) {
|
|
849
|
+
try {
|
|
850
|
+
return parseSyncManifest(await readFile(join4(cwd, MANIFEST_FILE), "utf8"));
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
832
855
|
async function readManifestBase(cwd) {
|
|
833
856
|
const manifest = await readSyncManifest(cwd);
|
|
834
857
|
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
@@ -842,7 +865,8 @@ async function writeSyncManifest(cwd, manifest) {
|
|
|
842
865
|
|
|
843
866
|
// src/utils/git.ts
|
|
844
867
|
var execFileAsync = promisify(execFile);
|
|
845
|
-
|
|
868
|
+
var MAX_BUFFER = 50 * 1024 * 1024;
|
|
869
|
+
function redactUrlSecrets(text) {
|
|
846
870
|
return text.replace(/https?:\/\/[^@\s]+@/g, (match) => {
|
|
847
871
|
const protocol = match.startsWith("https") ? "https" : "http";
|
|
848
872
|
return `${protocol}://***@`;
|
|
@@ -854,14 +878,14 @@ async function git(args, cwd, options = {}) {
|
|
|
854
878
|
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
855
879
|
...options.env
|
|
856
880
|
};
|
|
857
|
-
const maxBuffer = options.maxBuffer ??
|
|
881
|
+
const maxBuffer = options.maxBuffer ?? MAX_BUFFER;
|
|
858
882
|
try {
|
|
859
883
|
const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer });
|
|
860
884
|
return stdout.trim();
|
|
861
885
|
} catch (error) {
|
|
862
886
|
if (options.ignoreErrors) return "";
|
|
863
887
|
if (error instanceof Error) {
|
|
864
|
-
error.message =
|
|
888
|
+
error.message = redactUrlSecrets(error.message);
|
|
865
889
|
}
|
|
866
890
|
throw error;
|
|
867
891
|
}
|
|
@@ -872,8 +896,26 @@ async function getCurrentBranch(cwd) {
|
|
|
872
896
|
async function switchBranch(cwd, branch) {
|
|
873
897
|
await git(["switch", branch], cwd);
|
|
874
898
|
}
|
|
899
|
+
async function switchDetached(cwd, ref) {
|
|
900
|
+
await git(["switch", "--detach", ref], cwd);
|
|
901
|
+
}
|
|
875
902
|
async function createBranchFrom(cwd, branch, startPoint) {
|
|
876
|
-
await git(["switch", "-c", branch, startPoint], cwd);
|
|
903
|
+
await git(["switch", "-c", branch, "--no-track", startPoint], cwd);
|
|
904
|
+
}
|
|
905
|
+
async function branchExists(cwd, branch) {
|
|
906
|
+
return await git(["rev-parse", "-q", "--verify", `refs/heads/${branch}`], cwd, { ignoreErrors: true }) !== "";
|
|
907
|
+
}
|
|
908
|
+
async function getBranchWorktree(cwd, branch) {
|
|
909
|
+
const output = await git(["worktree", "list", "--porcelain"], cwd, { ignoreErrors: true });
|
|
910
|
+
let worktree = null;
|
|
911
|
+
for (const line of output.split("\n")) {
|
|
912
|
+
if (line.startsWith("worktree ")) worktree = line.slice("worktree ".length);
|
|
913
|
+
else if (line === `branch refs/heads/${branch}`) return worktree;
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
async function fastForwardBranch(cwd, branch, ref) {
|
|
918
|
+
await git(["fetch", "--quiet", ".", `${ref}:refs/heads/${branch}`], cwd);
|
|
877
919
|
}
|
|
878
920
|
async function deleteBranch(cwd, branch) {
|
|
879
921
|
await git(["branch", "-D", branch], cwd, { ignoreErrors: true });
|
|
@@ -881,14 +923,21 @@ async function deleteBranch(cwd, branch) {
|
|
|
881
923
|
async function pullFastForward(cwd) {
|
|
882
924
|
await git(["pull", "--ff-only"], cwd, { ignoreErrors: true, skipEditor: true });
|
|
883
925
|
}
|
|
884
|
-
async function
|
|
885
|
-
const upstream = await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name",
|
|
926
|
+
async function getBranchUpstream(cwd, branch = "") {
|
|
927
|
+
const upstream = await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", `${branch}@{upstream}`], cwd, {
|
|
886
928
|
ignoreErrors: true
|
|
887
929
|
});
|
|
930
|
+
return upstream || null;
|
|
931
|
+
}
|
|
932
|
+
async function getUpstreamStatus(cwd, branch) {
|
|
933
|
+
const local = branch ? `refs/heads/${branch}` : "HEAD";
|
|
934
|
+
const upstream = await getBranchUpstream(cwd, branch);
|
|
888
935
|
if (!upstream) return { upstream: null, ahead: 0, behind: 0 };
|
|
889
936
|
const remote = upstream.split("/")[0];
|
|
890
937
|
await git(["fetch", remote], cwd, { ignoreErrors: true, skipEditor: true });
|
|
891
|
-
const counts = await git(["rev-list", "--left-right", "--count",
|
|
938
|
+
const counts = await git(["rev-list", "--left-right", "--count", `${local}...${upstream}`], cwd, {
|
|
939
|
+
ignoreErrors: true
|
|
940
|
+
});
|
|
892
941
|
const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
893
942
|
return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
894
943
|
}
|
|
@@ -907,16 +956,19 @@ async function flattenBranch(cwd, baseRef, message) {
|
|
|
907
956
|
}
|
|
908
957
|
async function commitSquash(cwd, message) {
|
|
909
958
|
for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
|
|
910
|
-
const
|
|
911
|
-
if (existsSync3(
|
|
959
|
+
const path = await gitPath(cwd, file);
|
|
960
|
+
if (existsSync3(path)) await unlink(path);
|
|
912
961
|
}
|
|
913
962
|
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
914
963
|
}
|
|
964
|
+
async function gitPath(cwd, file) {
|
|
965
|
+
return resolve3(cwd, await git(["rev-parse", "--git-path", file], cwd));
|
|
966
|
+
}
|
|
915
967
|
async function stageAll(cwd) {
|
|
916
968
|
await git(["add", "-A"], cwd);
|
|
917
969
|
}
|
|
918
|
-
function mergeInProgress(cwd) {
|
|
919
|
-
return
|
|
970
|
+
async function mergeInProgress(cwd) {
|
|
971
|
+
return await git(["rev-parse", "-q", "--verify", "MERGE_HEAD"], cwd, { ignoreErrors: true }) !== "";
|
|
920
972
|
}
|
|
921
973
|
async function getShortSha(cwd, ref) {
|
|
922
974
|
return git(["rev-parse", "--short", ref], cwd);
|
|
@@ -937,7 +989,7 @@ async function ensureRemote(cwd, remoteName, url) {
|
|
|
937
989
|
await git(["remote", "add", remoteName, url], cwd);
|
|
938
990
|
}
|
|
939
991
|
}
|
|
940
|
-
async function
|
|
992
|
+
async function fetchRemote(cwd, remoteName) {
|
|
941
993
|
await git(["fetch", remoteName], cwd);
|
|
942
994
|
}
|
|
943
995
|
async function fetchUpstreamTags(cwd, remoteName) {
|
|
@@ -957,6 +1009,33 @@ async function resolveLatestReleaseTag(cwd, remoteName) {
|
|
|
957
1009
|
}
|
|
958
1010
|
return null;
|
|
959
1011
|
}
|
|
1012
|
+
async function resolveUpstreamCommit(cwd, remoteName, ref) {
|
|
1013
|
+
if (ref.startsWith("-")) return null;
|
|
1014
|
+
const tagRef = `refs/${remoteName}/tags/${ref}`;
|
|
1015
|
+
for (const candidate of [`refs/remotes/${remoteName}/${ref}`, tagRef, ref]) {
|
|
1016
|
+
const sha = await git(["rev-parse", "-q", "--verify", `${candidate}^{commit}`], cwd, { ignoreErrors: true });
|
|
1017
|
+
if (!sha) continue;
|
|
1018
|
+
const release = candidate === tagRef && /^v\d[\w.+-]*$/.test(ref) ? { tag: ref, ref: tagRef } : void 0;
|
|
1019
|
+
return { sha, release };
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
async function isPublishedUpstream(cwd, remoteName, branchRef, sha) {
|
|
1024
|
+
const containing = await git(
|
|
1025
|
+
[
|
|
1026
|
+
"for-each-ref",
|
|
1027
|
+
"--count=1",
|
|
1028
|
+
"--format=%(refname)",
|
|
1029
|
+
"--contains",
|
|
1030
|
+
sha,
|
|
1031
|
+
`refs/remotes/${branchRef}`,
|
|
1032
|
+
`refs/${remoteName}/tags/v*`
|
|
1033
|
+
],
|
|
1034
|
+
cwd,
|
|
1035
|
+
{ ignoreErrors: true }
|
|
1036
|
+
);
|
|
1037
|
+
return containing !== "";
|
|
1038
|
+
}
|
|
960
1039
|
async function getCommitInfo(cwd, ref) {
|
|
961
1040
|
const format = "%H%n%s%n%ar";
|
|
962
1041
|
const output = await git(["log", "-1", `--format=${format}`, ref], cwd);
|
|
@@ -968,10 +1047,8 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
|
968
1047
|
return Number.parseInt(output.trim(), 10) || 0;
|
|
969
1048
|
}
|
|
970
1049
|
async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
971
|
-
const {
|
|
972
|
-
const args = ["log", "--format=%H%x1f%s%x1f%ar"];
|
|
973
|
-
if (oldestFirst) args.push("--reverse");
|
|
974
|
-
args.push(`${fromRef}..${toRef}`);
|
|
1050
|
+
const { skip, limit } = options;
|
|
1051
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
|
|
975
1052
|
const output = await git(args, cwd, { ignoreErrors: true });
|
|
976
1053
|
if (!output) return [];
|
|
977
1054
|
const commits = [];
|
|
@@ -981,14 +1058,8 @@ async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
|
981
1058
|
if (!hash || !message || !date) continue;
|
|
982
1059
|
commits.push({ hash, message, date });
|
|
983
1060
|
}
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
if (normalizedSkip === 0 && normalizedLimit === void 0) {
|
|
987
|
-
return commits;
|
|
988
|
-
}
|
|
989
|
-
const start = Math.min(normalizedSkip, commits.length);
|
|
990
|
-
const end = normalizedLimit === void 0 ? commits.length : Math.min(start + normalizedLimit, commits.length);
|
|
991
|
-
return commits.slice(start, end);
|
|
1061
|
+
const start = Math.max(0, skip ?? 0);
|
|
1062
|
+
return limit && limit > 0 ? commits.slice(start, start + limit) : commits.slice(start);
|
|
992
1063
|
}
|
|
993
1064
|
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
994
1065
|
const info = /* @__PURE__ */ new Map();
|
|
@@ -1043,12 +1114,9 @@ async function listWorktrees(cwd) {
|
|
|
1043
1114
|
}
|
|
1044
1115
|
return paths;
|
|
1045
1116
|
}
|
|
1046
|
-
async function merge(cwd, ref
|
|
1047
|
-
const args = ["merge", "--no-ff", ref];
|
|
1048
|
-
if (options.noCommit) args.push("--no-commit");
|
|
1049
|
-
if (options.noEdit) args.push("--no-edit");
|
|
1117
|
+
async function merge(cwd, ref) {
|
|
1050
1118
|
try {
|
|
1051
|
-
await git(
|
|
1119
|
+
await git(["merge", "--no-ff", "--no-commit", "--no-edit", ref], cwd, { skipEditor: true });
|
|
1052
1120
|
return { success: true, conflicts: [] };
|
|
1053
1121
|
} catch (error) {
|
|
1054
1122
|
const conflicts = await getConflictedFiles(cwd);
|
|
@@ -1076,6 +1144,22 @@ async function checkoutFromRef(cwd, ref, filePath) {
|
|
|
1076
1144
|
async function restoreWorktreeFromRef(cwd, ref, filePath) {
|
|
1077
1145
|
await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
|
|
1078
1146
|
}
|
|
1147
|
+
async function batchRestoreWorktreeFromRef(cwd, ref, filePaths) {
|
|
1148
|
+
const batchSize = 200;
|
|
1149
|
+
for (let i = 0; i < filePaths.length; i += batchSize) {
|
|
1150
|
+
const batch = filePaths.slice(i, i + batchSize);
|
|
1151
|
+
await git(["--literal-pathspecs", "restore", `--source=${ref}`, "--worktree", "--", ...batch], cwd);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async function listIdenticalToRef(cwd, ref) {
|
|
1155
|
+
const nulSeparated = (out) => out.split("\0").filter(Boolean);
|
|
1156
|
+
const [tracked, changed] = await Promise.all([
|
|
1157
|
+
git(["ls-tree", "-r", "--name-only", "-z", ref], cwd),
|
|
1158
|
+
git(["diff", "--name-only", "--no-renames", "-z", ref, "--"], cwd)
|
|
1159
|
+
]);
|
|
1160
|
+
const differs = new Set(nulSeparated(changed));
|
|
1161
|
+
return new Set(nulSeparated(tracked).filter((path) => !differs.has(path)));
|
|
1162
|
+
}
|
|
1079
1163
|
async function getMergeBase(cwd, ref1, ref2) {
|
|
1080
1164
|
return git(["merge-base", ref1, ref2], cwd);
|
|
1081
1165
|
}
|
|
@@ -1085,6 +1169,7 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1085
1169
|
});
|
|
1086
1170
|
const changes = /* @__PURE__ */ new Map();
|
|
1087
1171
|
if (!output) return changes;
|
|
1172
|
+
const hashOrNull = (hash) => hash === "0".repeat(40) ? null : hash;
|
|
1088
1173
|
for (const line of output.split("\n")) {
|
|
1089
1174
|
if (!line) continue;
|
|
1090
1175
|
const renameMatch = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) R\d*\s+(.+)\t(.+)$/);
|
|
@@ -1092,10 +1177,9 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1092
1177
|
const [, baseHash, targetHash, oldPath, newPath] = renameMatch;
|
|
1093
1178
|
changes.set(newPath, {
|
|
1094
1179
|
status: "R",
|
|
1095
|
-
baseHash: baseHash
|
|
1096
|
-
targetHash: targetHash
|
|
1097
|
-
oldPath
|
|
1098
|
-
newPath
|
|
1180
|
+
baseHash: hashOrNull(baseHash),
|
|
1181
|
+
targetHash: hashOrNull(targetHash),
|
|
1182
|
+
oldPath
|
|
1099
1183
|
});
|
|
1100
1184
|
continue;
|
|
1101
1185
|
}
|
|
@@ -1104,8 +1188,8 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1104
1188
|
const [, baseHash, targetHash, status, filePath] = match;
|
|
1105
1189
|
changes.set(filePath, {
|
|
1106
1190
|
status,
|
|
1107
|
-
baseHash: baseHash
|
|
1108
|
-
targetHash: targetHash
|
|
1191
|
+
baseHash: hashOrNull(baseHash),
|
|
1192
|
+
targetHash: hashOrNull(targetHash)
|
|
1109
1193
|
});
|
|
1110
1194
|
}
|
|
1111
1195
|
}
|
|
@@ -1119,9 +1203,9 @@ async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
|
1119
1203
|
for (const line of out.split("\n")) {
|
|
1120
1204
|
if (!line.trim()) continue;
|
|
1121
1205
|
const [a, d, ...rest] = line.split(" ");
|
|
1122
|
-
const
|
|
1123
|
-
if (!
|
|
1124
|
-
stat.set(
|
|
1206
|
+
const path = rest.join(" ");
|
|
1207
|
+
if (!path) continue;
|
|
1208
|
+
stat.set(path, {
|
|
1125
1209
|
additions: a === "-" ? null : Number(a),
|
|
1126
1210
|
deletions: d === "-" ? null : Number(d)
|
|
1127
1211
|
});
|
|
@@ -1203,8 +1287,8 @@ async function storeLastSyncRef(cwd, upstreamHash) {
|
|
|
1203
1287
|
await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
|
|
1204
1288
|
}
|
|
1205
1289
|
}
|
|
1206
|
-
async function stagePath(cwd,
|
|
1207
|
-
await git(["add", "--",
|
|
1290
|
+
async function stagePath(cwd, path) {
|
|
1291
|
+
await git(["add", "--", path], cwd, { ignoreErrors: true });
|
|
1208
1292
|
}
|
|
1209
1293
|
async function getStoredSyncRef(cwd) {
|
|
1210
1294
|
const ref = await git(["rev-parse", "refs/cella/last-sync"], cwd, { ignoreErrors: true });
|
|
@@ -1217,14 +1301,7 @@ async function getStoredSyncHead(cwd) {
|
|
|
1217
1301
|
async function readManifestAtRef(cwd, ref) {
|
|
1218
1302
|
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
1219
1303
|
if (!raw) return null;
|
|
1220
|
-
|
|
1221
|
-
const parsed = JSON.parse(raw);
|
|
1222
|
-
const commit = parsed?.upstream?.commit;
|
|
1223
|
-
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1224
|
-
return parsed;
|
|
1225
|
-
} catch {
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1304
|
+
return parseSyncManifest(raw);
|
|
1228
1305
|
}
|
|
1229
1306
|
async function readManifestBaseAtRef(cwd, ref) {
|
|
1230
1307
|
const manifest = await readManifestAtRef(cwd, ref);
|
|
@@ -1345,8 +1422,8 @@ async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
|
1345
1422
|
throw new Error(
|
|
1346
1423
|
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1347
1424
|
|
|
1348
|
-
This fork has unrelated history with upstream
|
|
1349
|
-
upstream squashed its history
|
|
1425
|
+
This fork has unrelated history with upstream (typical for a create-cella scaffold, or after
|
|
1426
|
+
upstream squashed its history) and no recorded or inferable base commit exists after fetching
|
|
1350
1427
|
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1351
1428
|
|
|
1352
1429
|
git update-ref refs/cella/last-sync <upstream-sha>
|
|
@@ -1405,9 +1482,15 @@ async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
|
1405
1482
|
}
|
|
1406
1483
|
|
|
1407
1484
|
// src/utils/migration-notes.ts
|
|
1485
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1486
|
+
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1487
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1408
1488
|
var NOTES_DIR = "cella/migrations";
|
|
1409
1489
|
var PENDING_FILE = "cella/cella.migrations.json";
|
|
1410
1490
|
var NOTE_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1491
|
+
function isCodemodFile(name) {
|
|
1492
|
+
return name.endsWith(".ts") && !name.endsWith(".test.ts");
|
|
1493
|
+
}
|
|
1411
1494
|
function isUpstreamOnly(filePath) {
|
|
1412
1495
|
return filePath === NOTES_DIR || filePath.startsWith(`${NOTES_DIR}/`);
|
|
1413
1496
|
}
|
|
@@ -1438,7 +1521,7 @@ function parseNote(id, source, fileNames) {
|
|
|
1438
1521
|
syncBreaking: meta.get("syncBreaking") === "true",
|
|
1439
1522
|
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1440
1523
|
roots: (meta.get("roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean),
|
|
1441
|
-
codemod: fileNames.find(
|
|
1524
|
+
codemod: fileNames.find(isCodemodFile) ?? null
|
|
1442
1525
|
};
|
|
1443
1526
|
}
|
|
1444
1527
|
async function listNoteIds(cwd, ref) {
|
|
@@ -1457,23 +1540,23 @@ async function arrivedNoteIds(cwd, from, to) {
|
|
|
1457
1540
|
return after.filter((id) => !known.has(id));
|
|
1458
1541
|
}
|
|
1459
1542
|
async function readPending(cwd, syncRef) {
|
|
1460
|
-
const
|
|
1461
|
-
if (!existsSync4(
|
|
1462
|
-
const parsed = JSON.parse(await readFile2(
|
|
1543
|
+
const path = join6(cwd, PENDING_FILE);
|
|
1544
|
+
if (!existsSync4(path)) return [];
|
|
1545
|
+
const parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
1463
1546
|
if (Array.isArray(parsed.pending)) return parsed.pending.filter((id) => typeof id === "string");
|
|
1464
1547
|
if (!Array.isArray(parsed.applied) || !syncRef) return [];
|
|
1465
1548
|
const applied = new Set(parsed.applied);
|
|
1466
1549
|
return (await listNoteIds(cwd, syncRef)).filter((id) => !applied.has(id));
|
|
1467
1550
|
}
|
|
1468
1551
|
async function writePending(cwd, ids) {
|
|
1469
|
-
const
|
|
1552
|
+
const path = join6(cwd, PENDING_FILE);
|
|
1470
1553
|
const pending = [...new Set(ids)].sort();
|
|
1471
1554
|
if (pending.length === 0) {
|
|
1472
|
-
await rm(
|
|
1555
|
+
await rm(path, { force: true });
|
|
1473
1556
|
return;
|
|
1474
1557
|
}
|
|
1475
|
-
await mkdir3(dirname3(
|
|
1476
|
-
await writeFile2(
|
|
1558
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1559
|
+
await writeFile2(path, `${JSON.stringify({ pending }, null, 2)}
|
|
1477
1560
|
`, "utf8");
|
|
1478
1561
|
}
|
|
1479
1562
|
function noteUrl(upstreamGitHubUrl, commit, id) {
|
|
@@ -1517,6 +1600,19 @@ function groupIgnoredUpstreamChanges(files, ignored) {
|
|
|
1517
1600
|
for (const group of groups.values()) group.paths.sort();
|
|
1518
1601
|
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
1519
1602
|
}
|
|
1603
|
+
function groupProtectedUpstreamChanges(paths, config) {
|
|
1604
|
+
const entries = [...config.overrides?.pinned ?? [], ...config.overrides?.ignored ?? []].map((entry) => entry.replace(/\/+$/, "")).filter(Boolean);
|
|
1605
|
+
const mostSpecificFirst = [...new Set(entries)].sort((a, b) => b.length - a.length);
|
|
1606
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1607
|
+
for (const path of paths) {
|
|
1608
|
+
const entry = mostSpecificFirst.find((candidate) => isUnderAnyFolder(path, [candidate])) ?? path;
|
|
1609
|
+
const group = groups.get(entry) ?? { entry, paths: [] };
|
|
1610
|
+
group.paths.push(path);
|
|
1611
|
+
groups.set(entry, group);
|
|
1612
|
+
}
|
|
1613
|
+
for (const group of groups.values()) group.paths.sort();
|
|
1614
|
+
return [...groups.values()];
|
|
1615
|
+
}
|
|
1520
1616
|
function isPinnedForSync(filePath, config, unpinned) {
|
|
1521
1617
|
if (isManagedFile(filePath)) return true;
|
|
1522
1618
|
if (unpinned) return false;
|
|
@@ -1525,7 +1621,7 @@ function isPinnedForSync(filePath, config, unpinned) {
|
|
|
1525
1621
|
function hasGlobChars(entry) {
|
|
1526
1622
|
return entry.includes("*") || entry.includes("?");
|
|
1527
1623
|
}
|
|
1528
|
-
function validateOverrides(config, forkPath) {
|
|
1624
|
+
async function validateOverrides(config, forkPath, upstreamRef) {
|
|
1529
1625
|
const warnings = [];
|
|
1530
1626
|
const pinned = config.overrides?.pinned || [];
|
|
1531
1627
|
for (const entry of pinned) {
|
|
@@ -1552,6 +1648,7 @@ function validateOverrides(config, forkPath) {
|
|
|
1552
1648
|
message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
|
|
1553
1649
|
});
|
|
1554
1650
|
} else if (!existsSync5(join7(forkPath, entry.replace(/\/+$/, "")))) {
|
|
1651
|
+
if (upstreamRef && await fileExistsAtRef(forkPath, upstreamRef, entry.replace(/\/+$/, ""))) continue;
|
|
1555
1652
|
warnings.push({
|
|
1556
1653
|
type: "ignored-not-found",
|
|
1557
1654
|
pattern: entry,
|
|
@@ -1568,75 +1665,124 @@ function printWarnings(warnings) {
|
|
|
1568
1665
|
}
|
|
1569
1666
|
|
|
1570
1667
|
// src/cli.ts
|
|
1571
|
-
|
|
1572
|
-
|
|
1668
|
+
var str = (value) => typeof value === "string" ? value : void 0;
|
|
1669
|
+
var flag = (value) => value === true;
|
|
1670
|
+
function readOptions(opts, operands = []) {
|
|
1671
|
+
const scope = str(opts.scope);
|
|
1573
1672
|
if (scope && scope !== "all" && scope !== "risk" && scope !== "protected") {
|
|
1574
1673
|
throw new Error(`invalid --scope '${scope}'. expected one of: all, risk, protected`);
|
|
1575
1674
|
}
|
|
1576
1675
|
const normalizedScope = scope;
|
|
1676
|
+
const run = str(opts.run);
|
|
1677
|
+
if (operands.length > 0 && !run) {
|
|
1678
|
+
throw new Error(`unexpected argument '${operands[0]}'. arguments after the options only go with --run <id>`);
|
|
1679
|
+
}
|
|
1577
1680
|
return {
|
|
1578
|
-
logFile: opts.log
|
|
1579
|
-
verbose: opts.verbose
|
|
1580
|
-
list: opts.list
|
|
1581
|
-
json: opts.json
|
|
1582
|
-
diff:
|
|
1583
|
-
openDiff:
|
|
1681
|
+
logFile: flag(opts.log),
|
|
1682
|
+
verbose: flag(opts.verbose),
|
|
1683
|
+
list: flag(opts.list),
|
|
1684
|
+
json: flag(opts.json),
|
|
1685
|
+
diff: str(opts.diff),
|
|
1686
|
+
openDiff: str(opts.openDiff),
|
|
1584
1687
|
scope: normalizedScope,
|
|
1585
|
-
fork:
|
|
1586
|
-
hard: opts.hard
|
|
1587
|
-
unpinned: opts.unpinned
|
|
1688
|
+
fork: str(opts.fork),
|
|
1689
|
+
hard: flag(opts.hard),
|
|
1690
|
+
unpinned: flag(opts.unpinned),
|
|
1588
1691
|
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1692
|
+
ref: str(opts.ref) || void 0,
|
|
1693
|
+
keepConfig: flag(opts.keepConfig),
|
|
1694
|
+
force: flag(opts.force),
|
|
1695
|
+
checkOverrides: flag(opts.checkOverrides),
|
|
1696
|
+
coverage: flag(opts.coverage),
|
|
1697
|
+
since: str(opts.since) || void 0,
|
|
1698
|
+
md: flag(opts.md),
|
|
1699
|
+
all: flag(opts.all),
|
|
1700
|
+
show: str(opts.show),
|
|
1701
|
+
extract: str(opts.extract),
|
|
1702
|
+
run,
|
|
1703
|
+
script: str(opts.script),
|
|
1704
|
+
runArgs: run ? operands : void 0,
|
|
1595
1705
|
mark: Array.isArray(opts.mark) ? opts.mark.filter((id) => typeof id === "string") : void 0
|
|
1596
1706
|
};
|
|
1597
1707
|
}
|
|
1708
|
+
var logOption = {
|
|
1709
|
+
flags: "--log",
|
|
1710
|
+
description: "write complete file list to cella-sync.log"
|
|
1711
|
+
};
|
|
1712
|
+
var jsonOption = {
|
|
1713
|
+
flags: "--json",
|
|
1714
|
+
description: "machine-readable output for tooling/agents"
|
|
1715
|
+
};
|
|
1716
|
+
var trackOption = {
|
|
1717
|
+
flags: "--track <mode>",
|
|
1718
|
+
description: "override upstream tracking for this run: release|branch"
|
|
1719
|
+
};
|
|
1720
|
+
var refOption = {
|
|
1721
|
+
flags: "--ref <ref>",
|
|
1722
|
+
description: "pin the upstream commit for this run: sha, release tag or branch"
|
|
1723
|
+
};
|
|
1724
|
+
var hardOption = {
|
|
1725
|
+
flags: "--hard",
|
|
1726
|
+
description: "overwrite drifted files with upstream version (aggressive realignment)"
|
|
1727
|
+
};
|
|
1728
|
+
var verboseOption = {
|
|
1729
|
+
flags: "-V, --verbose",
|
|
1730
|
+
description: "show detailed output during operations"
|
|
1731
|
+
};
|
|
1732
|
+
var forkOption = {
|
|
1733
|
+
flags: "--fork <name>",
|
|
1734
|
+
description: "pre-select a fork by name (skips the fork selection prompt)"
|
|
1735
|
+
};
|
|
1598
1736
|
var serviceDefinitions = [
|
|
1599
1737
|
{
|
|
1600
1738
|
name: "analyze",
|
|
1601
1739
|
description: "dry run to see what would change on sync",
|
|
1602
1740
|
options: [
|
|
1603
|
-
|
|
1741
|
+
logOption,
|
|
1604
1742
|
{ flags: "--list", description: "non-interactive output for tooling (one file per line)" },
|
|
1605
|
-
|
|
1743
|
+
jsonOption,
|
|
1606
1744
|
{ flags: "--scope <scope>", description: "analyze scope for --list/--json: all|risk|protected" },
|
|
1607
|
-
|
|
1608
|
-
|
|
1745
|
+
trackOption,
|
|
1746
|
+
refOption,
|
|
1747
|
+
{ flags: "--diff <path>", description: "print the unified diff for one file, then exit" },
|
|
1609
1748
|
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
1610
1749
|
],
|
|
1611
1750
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1612
1751
|
},
|
|
1613
1752
|
{
|
|
1614
1753
|
name: "sync",
|
|
1615
|
-
description: "merge upstream changes
|
|
1754
|
+
description: "merge upstream changes onto a fresh branch, sync package.json and open a squash-merge PR",
|
|
1616
1755
|
options: [
|
|
1617
|
-
|
|
1618
|
-
|
|
1756
|
+
logOption,
|
|
1757
|
+
hardOption,
|
|
1619
1758
|
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
1620
|
-
|
|
1759
|
+
trackOption,
|
|
1760
|
+
refOption,
|
|
1761
|
+
{ flags: "--keep-config", description: "merge with your sync config as it stands when upstream changed its own" }
|
|
1621
1762
|
],
|
|
1622
|
-
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1623
|
-
menuDescription: () => "merge upstream changes + sync package.json"
|
|
1763
|
+
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1624
1764
|
},
|
|
1625
1765
|
{
|
|
1626
1766
|
name: "migrate",
|
|
1627
1767
|
description: "list the upstream migration notes this app has not handled yet",
|
|
1628
1768
|
options: [
|
|
1629
1769
|
{ flags: "--all", description: "list every upstream note, handled or not" },
|
|
1630
|
-
|
|
1770
|
+
jsonOption,
|
|
1631
1771
|
{ flags: "--show <id>", description: "print one note's README" },
|
|
1632
1772
|
{ flags: "--extract <id>", description: "write one note's folder under node_modules/.cache to run its codemod" },
|
|
1773
|
+
{ flags: "--run <id>", description: "run one note's codemod; files identical to upstream stay as they are" },
|
|
1774
|
+
{ flags: "--script <file>", description: "with --run: the script to run when the note folder holds several" },
|
|
1633
1775
|
{ flags: "--mark <ids...>", description: "record notes as handled" }
|
|
1634
1776
|
],
|
|
1777
|
+
operands: {
|
|
1778
|
+
flags: "[codemodArgs...]",
|
|
1779
|
+
description: "with --run, after `--`: arguments for the codemod (default: inventory and the note's roots)"
|
|
1780
|
+
},
|
|
1635
1781
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1636
1782
|
},
|
|
1637
1783
|
{
|
|
1638
1784
|
name: "audit",
|
|
1639
|
-
description: "check for outdated packages
|
|
1785
|
+
description: "check for outdated packages and vulnerabilities",
|
|
1640
1786
|
options: [
|
|
1641
1787
|
{ flags: "--list", description: "skip interactive update prompts after printing audit results" },
|
|
1642
1788
|
{ flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
|
|
@@ -1645,23 +1791,27 @@ var serviceDefinitions = [
|
|
|
1645
1791
|
},
|
|
1646
1792
|
{
|
|
1647
1793
|
name: "forks",
|
|
1648
|
-
description: "sync
|
|
1794
|
+
description: "run normal sync inside local fork repositories",
|
|
1649
1795
|
options: [
|
|
1650
|
-
|
|
1796
|
+
forkOption,
|
|
1651
1797
|
{ flags: "--log", description: "write complete file list to cella-sync.log for each synced fork" },
|
|
1652
|
-
|
|
1653
|
-
|
|
1798
|
+
verboseOption,
|
|
1799
|
+
hardOption,
|
|
1800
|
+
{
|
|
1801
|
+
flags: "--keep-config",
|
|
1802
|
+
description: "merge with the fork's sync config as it stands when upstream changed its own"
|
|
1803
|
+
}
|
|
1654
1804
|
],
|
|
1655
1805
|
includeInMenu: (context) => context.hasForks
|
|
1656
1806
|
},
|
|
1657
1807
|
{
|
|
1658
1808
|
name: "contributions",
|
|
1659
|
-
description: "pull and adopt changes from forks",
|
|
1809
|
+
description: "pull and adopt changes from local forks",
|
|
1660
1810
|
options: [
|
|
1661
|
-
|
|
1662
|
-
{ flags: "--list", description: "non-interactive output
|
|
1663
|
-
|
|
1664
|
-
{ flags: "--diff <path>", description: "print the unified diff for
|
|
1811
|
+
forkOption,
|
|
1812
|
+
{ flags: "--list", description: "non-interactive output for tooling (tab-separated rows)" },
|
|
1813
|
+
jsonOption,
|
|
1814
|
+
{ flags: "--diff <path>", description: "print the unified diff for one contributed file, then exit" }
|
|
1665
1815
|
],
|
|
1666
1816
|
includeInMenu: (context) => context.hasForks
|
|
1667
1817
|
},
|
|
@@ -1669,26 +1819,31 @@ var serviceDefinitions = [
|
|
|
1669
1819
|
name: "stats",
|
|
1670
1820
|
description: "count files by category and workspace package",
|
|
1671
1821
|
options: [
|
|
1672
|
-
|
|
1673
|
-
{ flags: "--coverage", description: "regenerate test coverage before showing the stats summary" }
|
|
1822
|
+
verboseOption,
|
|
1823
|
+
{ flags: "--coverage", description: "regenerate test coverage before showing the stats summary" },
|
|
1824
|
+
{
|
|
1825
|
+
flags: "--since <ref>",
|
|
1826
|
+
description: "count what the current branch changed since it left <ref>, by kind of file"
|
|
1827
|
+
},
|
|
1828
|
+
{ flags: "--md", description: "with --since: print the table as markdown for a pull request description" }
|
|
1674
1829
|
]
|
|
1675
1830
|
}
|
|
1676
1831
|
];
|
|
1677
|
-
|
|
1832
|
+
function getMenuContext(userConfig, forkPath) {
|
|
1678
1833
|
return {
|
|
1679
1834
|
hasForks: (userConfig.forks?.length ?? 0) > 0,
|
|
1680
|
-
isUpstreamRepo:
|
|
1835
|
+
isUpstreamRepo: isUpstreamRepo(forkPath)
|
|
1681
1836
|
};
|
|
1682
1837
|
}
|
|
1683
1838
|
function buildServiceChoices(context) {
|
|
1684
1839
|
const label = (name) => name.padEnd(14);
|
|
1685
1840
|
const baseChoices = serviceDefinitions.filter((service) => service.includeInMenu?.(context) ?? true).map((service) => ({
|
|
1686
1841
|
value: service.name,
|
|
1687
|
-
name: `${label(service.name)}${colors_default.dim(service.
|
|
1842
|
+
name: `${label(service.name)}${colors_default.dim(service.description)}`
|
|
1688
1843
|
}));
|
|
1689
1844
|
return [
|
|
1690
1845
|
...baseChoices,
|
|
1691
|
-
{ type: "separator", separator:
|
|
1846
|
+
{ type: "separator", separator: MENU_DIVIDER },
|
|
1692
1847
|
{ value: "exit", name: colors_default.red(`${label("exit")}${colors_default.dim("quit without doing anything")}`) }
|
|
1693
1848
|
];
|
|
1694
1849
|
}
|
|
@@ -1697,10 +1852,13 @@ function addServiceCommand(program, definition, setSelection) {
|
|
|
1697
1852
|
for (const option of definition.options ?? []) {
|
|
1698
1853
|
command.option(option.flags, option.description);
|
|
1699
1854
|
}
|
|
1700
|
-
command.
|
|
1855
|
+
if (definition.operands) command.argument(definition.operands.flags, definition.operands.description);
|
|
1856
|
+
command.action((...args) => {
|
|
1857
|
+
const operands = definition.operands ? args[0] : [];
|
|
1858
|
+
const opts = definition.operands ? args[1] : args[0];
|
|
1701
1859
|
setSelection({
|
|
1702
1860
|
service: definition.name,
|
|
1703
|
-
options: readOptions(opts)
|
|
1861
|
+
options: readOptions(opts, operands)
|
|
1704
1862
|
});
|
|
1705
1863
|
});
|
|
1706
1864
|
}
|
|
@@ -1716,8 +1874,11 @@ function buildProgram(setSelection) {
|
|
|
1716
1874
|
" $ cella sync --hard",
|
|
1717
1875
|
" $ cella sync --unpinned",
|
|
1718
1876
|
" $ cella sync --track branch",
|
|
1877
|
+
" $ cella sync --ref 4f7d87c",
|
|
1878
|
+
" $ cella migrate --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
|
|
1719
1879
|
" $ cella migrate --mark 20261002T0614-config-switch",
|
|
1720
1880
|
" $ cella audit --check-overrides",
|
|
1881
|
+
" $ cella stats --since origin/main --md",
|
|
1721
1882
|
" $ cella contributions --fork raak --json"
|
|
1722
1883
|
].join("\n")
|
|
1723
1884
|
);
|
|
@@ -1740,7 +1901,7 @@ function parseCommandLine(argv) {
|
|
|
1740
1901
|
async function promptForService(userConfig, forkPath) {
|
|
1741
1902
|
const selected = await select({
|
|
1742
1903
|
message: "choose a service:",
|
|
1743
|
-
choices: buildServiceChoices(
|
|
1904
|
+
choices: buildServiceChoices(getMenuContext(userConfig, forkPath)),
|
|
1744
1905
|
loop: false
|
|
1745
1906
|
});
|
|
1746
1907
|
if (selected === "exit") {
|
|
@@ -1763,9 +1924,10 @@ function buildRuntimeConfig(userConfig, forkPath, selection) {
|
|
|
1763
1924
|
}
|
|
1764
1925
|
async function parseCli(userConfig, forkPath) {
|
|
1765
1926
|
const selection = parseCommandLine(process5.argv);
|
|
1766
|
-
|
|
1927
|
+
const machineList = selection.options.list && selection.service !== "audit";
|
|
1928
|
+
if (selection.options.json || selection.options.diff || selection.options.md || machineList) setJsonMode(true);
|
|
1767
1929
|
printHeader();
|
|
1768
|
-
const warnings = validateOverrides(userConfig, forkPath);
|
|
1930
|
+
const warnings = await validateOverrides(userConfig, forkPath, resolveUpstream(userConfig.settings).branchRef);
|
|
1769
1931
|
if (warnings.length > 0) {
|
|
1770
1932
|
printWarnings(warnings);
|
|
1771
1933
|
console.info();
|
|
@@ -1783,14 +1945,14 @@ import { basename as basename4 } from "path";
|
|
|
1783
1945
|
import { execFileSync } from "child_process";
|
|
1784
1946
|
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1785
1947
|
import { tmpdir } from "os";
|
|
1786
|
-
import { basename as basename2, join as
|
|
1948
|
+
import { basename as basename2, join as join8 } from "path";
|
|
1787
1949
|
import process6 from "process";
|
|
1788
1950
|
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
1789
1951
|
const args = ["diff", "--no-color"];
|
|
1790
1952
|
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
1791
1953
|
args.push(range, "--", filePath);
|
|
1792
1954
|
try {
|
|
1793
|
-
return execFileSync("git", args, { cwd, maxBuffer:
|
|
1955
|
+
return execFileSync("git", args, { cwd, maxBuffer: MAX_BUFFER });
|
|
1794
1956
|
} catch (error) {
|
|
1795
1957
|
const stderr = error.stderr?.toString().trim();
|
|
1796
1958
|
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
@@ -1877,9 +2039,9 @@ function openWithDefaultApp(target) {
|
|
|
1877
2039
|
}
|
|
1878
2040
|
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
1879
2041
|
const html = await renderDiffPage(patch, meta);
|
|
1880
|
-
const dir =
|
|
2042
|
+
const dir = join8(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1881
2043
|
mkdirSync(dir, { recursive: true });
|
|
1882
|
-
const outPath =
|
|
2044
|
+
const outPath = join8(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
1883
2045
|
writeFileSync2(outPath, html);
|
|
1884
2046
|
openWithDefaultApp(outPath);
|
|
1885
2047
|
return outPath;
|
|
@@ -1888,24 +2050,24 @@ async function openDiffInBrowser(patch, meta, repoPath) {
|
|
|
1888
2050
|
// src/services/merge-engine.ts
|
|
1889
2051
|
import { existsSync as existsSync7 } from "fs";
|
|
1890
2052
|
import { rm as rm2 } from "fs/promises";
|
|
1891
|
-
import { join as
|
|
2053
|
+
import { join as join10 } from "path";
|
|
1892
2054
|
|
|
1893
2055
|
// src/utils/cleanup.ts
|
|
1894
2056
|
import { createHash } from "crypto";
|
|
1895
2057
|
import { existsSync as existsSync6, rmSync } from "fs";
|
|
1896
2058
|
import { tmpdir as tmpdir2 } from "os";
|
|
1897
|
-
import { basename as basename3, join as
|
|
2059
|
+
import { basename as basename3, join as join9, resolve as resolve4 } from "path";
|
|
1898
2060
|
import process7 from "process";
|
|
1899
2061
|
var WORKTREE_PREFIXES = {
|
|
1900
2062
|
sync: "cella-sync-",
|
|
1901
2063
|
view: "cella-view-"
|
|
1902
2064
|
};
|
|
1903
2065
|
function buildWorktreePath(kind, repoPath) {
|
|
1904
|
-
const key = createHash("sha256").update(
|
|
1905
|
-
return
|
|
2066
|
+
const key = createHash("sha256").update(resolve4(repoPath)).digest("hex").slice(0, 8);
|
|
2067
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
|
|
1906
2068
|
}
|
|
1907
2069
|
function legacyWorktreePath(kind, repoPath) {
|
|
1908
|
-
return
|
|
2070
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
1909
2071
|
}
|
|
1910
2072
|
function getWorktreePath(repoPath) {
|
|
1911
2073
|
return buildWorktreePath("sync", repoPath);
|
|
@@ -1944,7 +2106,7 @@ async function cleanupLeftoverWorktrees(repoPath) {
|
|
|
1944
2106
|
}
|
|
1945
2107
|
async function handleAbort(signal) {
|
|
1946
2108
|
console.info();
|
|
1947
|
-
console.info(`${warningMark}
|
|
2109
|
+
console.info(`${warningMark} interrupted (${signal}): cleaning up...`);
|
|
1948
2110
|
if (currentRepoPath && currentWorktreePath) {
|
|
1949
2111
|
try {
|
|
1950
2112
|
await mergeAbort(currentWorktreePath);
|
|
@@ -1952,11 +2114,9 @@ async function handleAbort(signal) {
|
|
|
1952
2114
|
}
|
|
1953
2115
|
try {
|
|
1954
2116
|
await cleanupWorktree(currentRepoPath, currentWorktreePath);
|
|
1955
|
-
console.info(`${
|
|
2117
|
+
console.info(`${checkMark} no changes were made to your repository.`);
|
|
1956
2118
|
} catch (error) {
|
|
1957
|
-
console.error(
|
|
1958
|
-
`${colors_default.red("\u2717")} failed to clean up worktree: ${error instanceof Error ? error.message : "unknown error"}`
|
|
1959
|
-
);
|
|
2119
|
+
console.error(`${colors_default.red("\u2717")} failed to clean up worktree: ${errorMessage(error)}`);
|
|
1960
2120
|
}
|
|
1961
2121
|
}
|
|
1962
2122
|
process7.exit(1);
|
|
@@ -2052,25 +2212,44 @@ function readProperty(object, name, sourceFile) {
|
|
|
2052
2212
|
}
|
|
2053
2213
|
return void 0;
|
|
2054
2214
|
}
|
|
2215
|
+
function readStringEntries(node) {
|
|
2216
|
+
if (!Node.isArrayLiteralExpression(node)) return null;
|
|
2217
|
+
const entries = [];
|
|
2218
|
+
for (const element of node.getElements()) {
|
|
2219
|
+
if (Node.isStringLiteral(element) || Node.isNoSubstitutionTemplateLiteral(element)) {
|
|
2220
|
+
entries.push(element.getLiteralValue());
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
return entries;
|
|
2224
|
+
}
|
|
2225
|
+
function readPackageJsonSync(config, sourceFile) {
|
|
2226
|
+
const settings = readProperty(config, "settings", sourceFile);
|
|
2227
|
+
if (settings === void 0) return [...DEFAULT_PACKAGE_JSON_SYNC];
|
|
2228
|
+
if (!settings || !Node.isObjectLiteralExpression(settings)) return null;
|
|
2229
|
+
const keys = readProperty(settings, "packageJsonSync", sourceFile);
|
|
2230
|
+
if (keys === void 0) return [...DEFAULT_PACKAGE_JSON_SYNC];
|
|
2231
|
+
return keys ? readStringEntries(keys) : null;
|
|
2232
|
+
}
|
|
2055
2233
|
function parseOverrideLists(source) {
|
|
2056
2234
|
try {
|
|
2057
2235
|
const project = new Project2({ useInMemoryFileSystem: true, skipLoadingLibFiles: true });
|
|
2058
2236
|
const sourceFile = project.createSourceFile("cella.config.ts", source);
|
|
2059
2237
|
const config = findConfigObject(sourceFile);
|
|
2060
2238
|
if (!config) return null;
|
|
2239
|
+
const lists = {
|
|
2240
|
+
pinned: [],
|
|
2241
|
+
ignored: [],
|
|
2242
|
+
packageJsonSync: readPackageJsonSync(config, sourceFile)
|
|
2243
|
+
};
|
|
2061
2244
|
const overrides = readProperty(config, "overrides", sourceFile);
|
|
2062
|
-
if (overrides === void 0) return
|
|
2245
|
+
if (overrides === void 0) return lists;
|
|
2063
2246
|
if (!overrides || !Node.isObjectLiteralExpression(overrides)) return null;
|
|
2064
|
-
const lists = { pinned: [], ignored: [] };
|
|
2065
2247
|
for (const key of LIST_KEYS) {
|
|
2066
2248
|
const list = readProperty(overrides, key, sourceFile);
|
|
2067
2249
|
if (list === void 0) continue;
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
lists[key].push(element.getLiteralValue());
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2250
|
+
const entries = list ? readStringEntries(list) : null;
|
|
2251
|
+
if (!entries) return null;
|
|
2252
|
+
lists[key] = entries;
|
|
2074
2253
|
}
|
|
2075
2254
|
return lists;
|
|
2076
2255
|
} catch {
|
|
@@ -2090,11 +2269,19 @@ function diffOverrideLists(base, incoming, fork) {
|
|
|
2090
2269
|
removed: [...baseSet].filter((entry) => !incomingSet.has(entry) && forkEntries.includes(entry))
|
|
2091
2270
|
};
|
|
2092
2271
|
};
|
|
2093
|
-
|
|
2272
|
+
const diffKeys = () => {
|
|
2273
|
+
const [baseKeys, incomingKeys, forkKeys] = [base.packageJsonSync, incoming.packageJsonSync, fork.packageJsonSync];
|
|
2274
|
+
if (!baseKeys || !incomingKeys || !forkKeys) return { added: [], removed: [] };
|
|
2275
|
+
return {
|
|
2276
|
+
added: [...new Set(incomingKeys)].filter((key) => !baseKeys.includes(key) && !forkKeys.includes(key)),
|
|
2277
|
+
removed: [...new Set(baseKeys)].filter((key) => !incomingKeys.includes(key) && forkKeys.includes(key))
|
|
2278
|
+
};
|
|
2279
|
+
};
|
|
2280
|
+
return { pinned: diff("pinned"), ignored: diff("ignored"), packageJsonSync: diffKeys() };
|
|
2094
2281
|
}
|
|
2095
2282
|
async function readConfigSourceAtRef(repoPath, ref) {
|
|
2096
|
-
for (const
|
|
2097
|
-
const source = await git(["show", `${ref}:${
|
|
2283
|
+
for (const path of CONFIG_FILE_PATHS) {
|
|
2284
|
+
const source = await git(["show", `${ref}:${path}`], repoPath, { ignoreErrors: true });
|
|
2098
2285
|
if (source) return source;
|
|
2099
2286
|
}
|
|
2100
2287
|
return null;
|
|
@@ -2111,10 +2298,11 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
|
|
|
2111
2298
|
if (!incoming) return { kind: "unreadable", side: "incoming" };
|
|
2112
2299
|
const fork = {
|
|
2113
2300
|
pinned: forkConfig.overrides?.pinned ?? [],
|
|
2114
|
-
ignored: forkConfig.overrides?.ignored ?? []
|
|
2301
|
+
ignored: forkConfig.overrides?.ignored ?? [],
|
|
2302
|
+
packageJsonSync: forkConfig.settings.packageJsonSync ?? DEFAULT_PACKAGE_JSON_SYNC
|
|
2115
2303
|
};
|
|
2116
2304
|
const changes = diffOverrideLists(base, incoming, fork);
|
|
2117
|
-
const count =
|
|
2305
|
+
const count = Object.values(changes).reduce((n, list) => n + list.added.length + list.removed.length, 0);
|
|
2118
2306
|
return count > 0 ? { kind: "changes", ...changes } : void 0;
|
|
2119
2307
|
}
|
|
2120
2308
|
|
|
@@ -2238,10 +2426,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
2238
2426
|
status = fileIsPinned ? "pinned" : "diverged";
|
|
2239
2427
|
} else if (forkChanged && !upstreamChanged2) {
|
|
2240
2428
|
status = fileIsPinned ? "ahead" : predicates.hard ? "behind" : "drifted";
|
|
2241
|
-
} else if (!forkChanged && upstreamChanged2) {
|
|
2242
|
-
status = "behind";
|
|
2243
2429
|
} else {
|
|
2244
|
-
status = "
|
|
2430
|
+
status = "behind";
|
|
2245
2431
|
}
|
|
2246
2432
|
}
|
|
2247
2433
|
const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
|
|
@@ -2326,6 +2512,21 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
2326
2512
|
}
|
|
2327
2513
|
|
|
2328
2514
|
// src/services/merge-engine.ts
|
|
2515
|
+
var BehindSyncPointError = class extends Error {
|
|
2516
|
+
};
|
|
2517
|
+
var UpstreamConfigChangedError = class extends Error {
|
|
2518
|
+
report;
|
|
2519
|
+
constructor(report) {
|
|
2520
|
+
super(
|
|
2521
|
+
[
|
|
2522
|
+
"upstream changed its sync config and yours does not follow it: stopped before the merge, nothing changed.",
|
|
2523
|
+
` add or drop the entries that fit your app in ${CONFIG_FILE}, commit, then rerun.`,
|
|
2524
|
+
" to merge with your config as it stands, rerun with --keep-config."
|
|
2525
|
+
].join("\n")
|
|
2526
|
+
);
|
|
2527
|
+
this.report = report;
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2329
2530
|
function getGitHubBaseUrl(remoteUrl) {
|
|
2330
2531
|
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^.]+)(?:\.git)?$/);
|
|
2331
2532
|
if (sshMatch) {
|
|
@@ -2337,10 +2538,13 @@ function getGitHubBaseUrl(remoteUrl) {
|
|
|
2337
2538
|
}
|
|
2338
2539
|
return null;
|
|
2339
2540
|
}
|
|
2541
|
+
function isProtectedFile(file) {
|
|
2542
|
+
return file.isIgnored || file.isPinned;
|
|
2543
|
+
}
|
|
2340
2544
|
function syncPredicates(config) {
|
|
2341
2545
|
return {
|
|
2342
|
-
isIgnored: (
|
|
2343
|
-
isPinned: (
|
|
2546
|
+
isIgnored: (path) => isIgnored(path, config),
|
|
2547
|
+
isPinned: (path) => isPinnedForSync(path, config, config.unpinned),
|
|
2344
2548
|
hard: config.hard
|
|
2345
2549
|
};
|
|
2346
2550
|
}
|
|
@@ -2359,6 +2563,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2359
2563
|
for (const file of analyzedFiles) {
|
|
2360
2564
|
const { path: filePath, status, isPinned: pinned, isIgnored: ignored, existsInFork } = file;
|
|
2361
2565
|
if (status === "identical" || status === "ahead") continue;
|
|
2566
|
+
if (!isProtectedFile(file)) continue;
|
|
2362
2567
|
if (ignored && existsInFork) {
|
|
2363
2568
|
batchRestorePaths.push(filePath);
|
|
2364
2569
|
} else if (ignored && !existsInFork) {
|
|
@@ -2370,12 +2575,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2370
2575
|
}
|
|
2371
2576
|
}
|
|
2372
2577
|
onProgress?.("starting merge in fork...");
|
|
2373
|
-
await withTemporarySyncBaseGraft(
|
|
2374
|
-
forkPath,
|
|
2375
|
-
"HEAD",
|
|
2376
|
-
mergeBaseRef,
|
|
2377
|
-
() => merge(forkPath, upstreamRef, { noCommit: true, noEdit: true })
|
|
2378
|
-
);
|
|
2578
|
+
await withTemporarySyncBaseGraft(forkPath, "HEAD", mergeBaseRef, () => merge(forkPath, upstreamRef));
|
|
2379
2579
|
if (batchRestorePaths.length > 0) {
|
|
2380
2580
|
onProgress?.(`batch restoring ${batchRestorePaths.length} pinned/ignored files...`);
|
|
2381
2581
|
await batchRestoreToHead(forkPath, batchRestorePaths);
|
|
@@ -2388,29 +2588,11 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2388
2588
|
}
|
|
2389
2589
|
}
|
|
2390
2590
|
for (const file of analyzedFiles) {
|
|
2391
|
-
const { path: filePath, status,
|
|
2591
|
+
const { path: filePath, status, existsInFork } = file;
|
|
2392
2592
|
if (status === "identical" || status === "ahead") {
|
|
2393
2593
|
continue;
|
|
2394
2594
|
}
|
|
2395
|
-
if (
|
|
2396
|
-
if (pinned && !file.renamedFrom) continue;
|
|
2397
|
-
if (pinned && file.renamedFrom) {
|
|
2398
|
-
const oldPathExists = await fileExistsInWorktree(forkPath, file.renamedFrom);
|
|
2399
|
-
if (oldPathExists) {
|
|
2400
|
-
onProgress?.(`\u2192 ${file.renamedFrom} \u2192 ${filePath}: moving fork content (pinned rename)`);
|
|
2401
|
-
try {
|
|
2402
|
-
await gitMv(forkPath, file.renamedFrom, filePath);
|
|
2403
|
-
} catch {
|
|
2404
|
-
await removeFileFully(forkPath, file.renamedFrom);
|
|
2405
|
-
await checkoutFromRef(forkPath, "HEAD", file.renamedFrom).catch(() => {
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
} else {
|
|
2409
|
-
onProgress?.(`\u2192 ${filePath}: keeping fork content (pinned rename, old path removed)`);
|
|
2410
|
-
await restoreToHead(forkPath, filePath);
|
|
2411
|
-
}
|
|
2412
|
-
continue;
|
|
2413
|
-
}
|
|
2595
|
+
if (isProtectedFile(file)) continue;
|
|
2414
2596
|
if (status === "diverged") {
|
|
2415
2597
|
onProgress?.(`\u2192 ${filePath}: using git merge result (diverged)`);
|
|
2416
2598
|
continue;
|
|
@@ -2455,6 +2637,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2455
2637
|
}
|
|
2456
2638
|
}
|
|
2457
2639
|
const protectedConflicts = analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path);
|
|
2640
|
+
const protectedConflictSet = new Set(protectedConflicts);
|
|
2458
2641
|
const gitConflicts = await getConflictedFiles(forkPath);
|
|
2459
2642
|
for (const filePath of gitConflicts) {
|
|
2460
2643
|
const isProtected = isIgnored(filePath, config) || isPinnedForSync(filePath, config, config.unpinned);
|
|
@@ -2462,7 +2645,10 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2462
2645
|
if (await fileExistsAtRef(forkPath, "HEAD", filePath)) {
|
|
2463
2646
|
onProgress?.(`\u2192 ${filePath}: keeping fork (protected conflict)`);
|
|
2464
2647
|
await restoreToHead(forkPath, filePath);
|
|
2465
|
-
if (!
|
|
2648
|
+
if (!protectedConflictSet.has(filePath)) {
|
|
2649
|
+
protectedConflictSet.add(filePath);
|
|
2650
|
+
protectedConflicts.push(filePath);
|
|
2651
|
+
}
|
|
2466
2652
|
} else {
|
|
2467
2653
|
onProgress?.(`\u2192 ${filePath}: removing (protected conflict, not in fork)`);
|
|
2468
2654
|
await removeFileFully(forkPath, filePath);
|
|
@@ -2494,8 +2680,7 @@ function calculateSummary(files) {
|
|
|
2494
2680
|
pinned: 0,
|
|
2495
2681
|
ignored: 0,
|
|
2496
2682
|
deleted: 0,
|
|
2497
|
-
renamed: 0
|
|
2498
|
-
total: files.length
|
|
2683
|
+
renamed: 0
|
|
2499
2684
|
};
|
|
2500
2685
|
for (const file of files) {
|
|
2501
2686
|
if (file.status !== "identical" && isManagedFile(file.path)) {
|
|
@@ -2511,6 +2696,20 @@ function formatLocalCheckoutDetail(forkPath, branch, headSha, changeCount, upstr
|
|
|
2511
2696
|
const workingTree = changeCount === 0 ? " working tree clean" : ` ${changeCount} uncommitted change${changeCount === 1 ? "" : "s"} in working tree, not included in analysis`;
|
|
2512
2697
|
return [`${forkPath}`, ` ${branch} @ ${headSha}${tracking}`, workingTree].join("\n");
|
|
2513
2698
|
}
|
|
2699
|
+
async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
|
|
2700
|
+
const resolved = await resolveUpstreamCommit(forkPath, remoteName, ref);
|
|
2701
|
+
if (!resolved) {
|
|
2702
|
+
throw new Error(
|
|
2703
|
+
`--ref '${ref}' does not resolve to a commit. Pass a commit sha, a release tag (v*) or a branch on ${config.settings.upstreamUrl}.`
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
if (!await isPublishedUpstream(forkPath, remoteName, branchRef, resolved.sha)) {
|
|
2707
|
+
throw new Error(
|
|
2708
|
+
`--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release (v*). Sync only to commits upstream has published there.`
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
return resolved.release ? { ref: resolved.release.ref, releaseTag: resolved.release.tag } : { ref: resolved.sha };
|
|
2712
|
+
}
|
|
2514
2713
|
async function prepareUpstream(config, onProgress, onStep) {
|
|
2515
2714
|
const { forkPath } = config;
|
|
2516
2715
|
onProgress?.("checking local checkout...");
|
|
@@ -2527,10 +2726,15 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2527
2726
|
const track = config.track ?? configTrack;
|
|
2528
2727
|
await ensureRemote(forkPath, remoteName, config.settings.upstreamUrl);
|
|
2529
2728
|
onProgress?.(`fetching upstream (${remoteName})...`);
|
|
2530
|
-
await
|
|
2729
|
+
await fetchRemote(forkPath, remoteName);
|
|
2531
2730
|
let upstreamRef = branchRef;
|
|
2532
2731
|
let releaseTag;
|
|
2533
|
-
if (
|
|
2732
|
+
if (config.ref) {
|
|
2733
|
+
await fetchUpstreamTags(forkPath, remoteName);
|
|
2734
|
+
const pinned = await resolvePinnedRef(forkPath, config, remoteName, branchRef, config.ref);
|
|
2735
|
+
upstreamRef = pinned.ref;
|
|
2736
|
+
releaseTag = pinned.releaseTag;
|
|
2737
|
+
} else if (track === "release") {
|
|
2534
2738
|
await fetchUpstreamTags(forkPath, remoteName);
|
|
2535
2739
|
const latest = await resolveLatestReleaseTag(forkPath, remoteName);
|
|
2536
2740
|
if (!latest) {
|
|
@@ -2541,8 +2745,9 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2541
2745
|
upstreamRef = latest.ref;
|
|
2542
2746
|
releaseTag = latest.tag;
|
|
2543
2747
|
}
|
|
2748
|
+
const upstreamLabel = releaseTag ?? (config.ref ? `--ref ${config.ref}` : upstreamRef);
|
|
2544
2749
|
config.upstreamRef = upstreamRef;
|
|
2545
|
-
onStep?.("remote configured", `${
|
|
2750
|
+
onStep?.("remote configured", `${upstreamLabel} \u2192 ${config.settings.upstreamUrl}`);
|
|
2546
2751
|
const cliRange = await readUpstreamCliRange(forkPath, upstreamRef);
|
|
2547
2752
|
const cliMismatch = cliRange ? cliVersionMismatch(VERSION, cliRange) : null;
|
|
2548
2753
|
if (cliMismatch) throw new Error(cliMismatch);
|
|
@@ -2550,18 +2755,32 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2550
2755
|
await ensureSyncBase(forkPath, "HEAD", upstreamRef);
|
|
2551
2756
|
const upstreamGitHubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2552
2757
|
const aggressive = config.hard === true || config.unpinned === true;
|
|
2553
|
-
const
|
|
2758
|
+
const syncPoint = (await getEffectiveMergeBase(forkPath, "HEAD", upstreamRef)).base;
|
|
2759
|
+
const mergeBase = aggressive ? await getMergeBase(forkPath, "HEAD", upstreamRef) : syncPoint;
|
|
2554
2760
|
const upstreamCommit = await getCommitInfo(forkPath, upstreamRef);
|
|
2761
|
+
if (upstreamCommit.hash !== syncPoint && await isAncestor(forkPath, upstreamCommit.hash, syncPoint)) {
|
|
2762
|
+
const [upstreamShort, syncShort] = await Promise.all([
|
|
2763
|
+
getShortSha(forkPath, upstreamCommit.hash),
|
|
2764
|
+
getShortSha(forkPath, syncPoint)
|
|
2765
|
+
]);
|
|
2766
|
+
const hint = config.ref ? "Pick a newer --ref." : releaseTag ? `Nothing to sync until a release past ${syncShort}; to follow the tip instead, run with --track branch.` : `Nothing to sync until '${branchRef}' moves past ${syncShort}.`;
|
|
2767
|
+
throw new BehindSyncPointError(
|
|
2768
|
+
`upstream ${upstreamLabel} (${upstreamShort}) is behind the last sync point ${syncShort}: the fork already has it, and syncing it would revert the upstream changes after it.
|
|
2769
|
+
${hint}`
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2555
2772
|
const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
|
|
2556
|
-
const
|
|
2557
|
-
const
|
|
2558
|
-
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, {
|
|
2559
|
-
oldestFirst: true,
|
|
2560
|
-
skip: commitSkip,
|
|
2561
|
-
limit: commitListMax
|
|
2562
|
-
}) : [];
|
|
2773
|
+
const commitSkip = commitCount > COMMIT_LIST_MAX ? commitCount - COMMIT_LIST_MAX : 0;
|
|
2774
|
+
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, { skip: commitSkip, limit: COMMIT_LIST_MAX }) : [];
|
|
2563
2775
|
onStep?.("fetched upstream", formatFetchedUpstreamDetail(commitCount, upstreamCommits, upstreamGitHubUrl));
|
|
2564
|
-
return { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit, upstreamCommits };
|
|
2776
|
+
return { upstreamRef, releaseTag, mergeBase, syncPoint, upstreamGitHubUrl, upstreamCommit, upstreamCommits };
|
|
2777
|
+
}
|
|
2778
|
+
async function assertSyncConfigFollowed(config, ctx) {
|
|
2779
|
+
if (config.keepConfig) return;
|
|
2780
|
+
const report = await compareUpstreamOverrides(config.forkPath, ctx.syncPoint, ctx.upstreamRef, config).catch(
|
|
2781
|
+
() => void 0
|
|
2782
|
+
);
|
|
2783
|
+
if (report?.kind === "changes") throw new UpstreamConfigChangedError(report);
|
|
2565
2784
|
}
|
|
2566
2785
|
function resultMeta(config, ctx) {
|
|
2567
2786
|
return {
|
|
@@ -2573,7 +2792,7 @@ function resultMeta(config, ctx) {
|
|
|
2573
2792
|
upstreamCommits: ctx.upstreamCommits
|
|
2574
2793
|
};
|
|
2575
2794
|
}
|
|
2576
|
-
async function upstreamOnlyReports(config, ctx, files) {
|
|
2795
|
+
async function upstreamOnlyReports(config, ctx, files, protectedConflicts) {
|
|
2577
2796
|
const { forkPath } = config;
|
|
2578
2797
|
try {
|
|
2579
2798
|
const [baseSha, upstreamSha, upstreamOverrides] = await Promise.all([
|
|
@@ -2585,7 +2804,8 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2585
2804
|
return {
|
|
2586
2805
|
upstreamDiffRange: `${baseSha}..${upstreamSha}`,
|
|
2587
2806
|
upstreamOverrides,
|
|
2588
|
-
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? [])
|
|
2807
|
+
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? []),
|
|
2808
|
+
protectedUpstreamChanges: groupProtectedUpstreamChanges(protectedConflicts, config)
|
|
2589
2809
|
};
|
|
2590
2810
|
} catch {
|
|
2591
2811
|
return {};
|
|
@@ -2594,12 +2814,10 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2594
2814
|
async function readNotesInRange(config, ctx) {
|
|
2595
2815
|
const { forkPath } = config;
|
|
2596
2816
|
try {
|
|
2597
|
-
const aggressive = config.hard === true || config.unpinned === true;
|
|
2598
|
-
const syncBase = aggressive ? (await getEffectiveMergeBase(forkPath, "HEAD", ctx.upstreamRef)).base : ctx.mergeBase;
|
|
2599
2817
|
const [total, arrived, pending] = await Promise.all([
|
|
2600
2818
|
listNoteIds(forkPath, ctx.upstreamRef),
|
|
2601
|
-
arrivedNoteIds(forkPath,
|
|
2602
|
-
readPending(forkPath,
|
|
2819
|
+
arrivedNoteIds(forkPath, ctx.syncPoint, ctx.upstreamRef),
|
|
2820
|
+
readPending(forkPath, ctx.syncPoint)
|
|
2603
2821
|
]);
|
|
2604
2822
|
return { total: total.length, arrived, pending: [.../* @__PURE__ */ new Set([...pending, ...arrived])] };
|
|
2605
2823
|
} catch {
|
|
@@ -2610,14 +2828,15 @@ function notesResult(notes) {
|
|
|
2610
2828
|
return notes ? { migrationNotes: { total: notes.total, arrived: notes.arrived, open: notes.pending.length } } : {};
|
|
2611
2829
|
}
|
|
2612
2830
|
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2613
|
-
if (!existsSync7(
|
|
2831
|
+
if (!existsSync7(join10(forkPath, NOTES_DIR))) return;
|
|
2614
2832
|
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", NOTES_DIR], forkPath);
|
|
2615
|
-
await rm2(
|
|
2833
|
+
await rm2(join10(forkPath, NOTES_DIR), { recursive: true, force: true });
|
|
2616
2834
|
onStep?.("migration notes", `removed ${NOTES_DIR}/: notes stay upstream, list them with pnpm cella migrate`);
|
|
2617
2835
|
}
|
|
2618
2836
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2619
2837
|
const { forkPath } = config;
|
|
2620
2838
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2839
|
+
await assertSyncConfigFollowed(config, ctx);
|
|
2621
2840
|
const notes = await readNotesInRange(config, ctx);
|
|
2622
2841
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2623
2842
|
forkPath,
|
|
@@ -2628,9 +2847,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2628
2847
|
);
|
|
2629
2848
|
const summary = calculateSummary(analyzedFiles);
|
|
2630
2849
|
const synced = analyzedFiles.filter((file) => ["behind", "diverged", "renamed"].includes(file.status)).length;
|
|
2631
|
-
const totalResolved = analyzedFiles.filter(
|
|
2632
|
-
(file) => ["behind", "diverged", "renamed", "ignored", "pinned"].includes(file.status)
|
|
2633
|
-
).length;
|
|
2850
|
+
const totalResolved = analyzedFiles.filter((file) => SYNC_APPLIED_STATUSES.includes(file.status)).length;
|
|
2634
2851
|
const syncManifest = {
|
|
2635
2852
|
upstream: {
|
|
2636
2853
|
repo: upstreamGitHubUrl ? upstreamGitHubUrl.replace("https://github.com/", "") : void 0,
|
|
@@ -2673,7 +2890,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2673
2890
|
autoMergedFiles,
|
|
2674
2891
|
protectedConflicts,
|
|
2675
2892
|
...resultMeta(config, ctx),
|
|
2676
|
-
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2893
|
+
...await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts),
|
|
2677
2894
|
...notesResult(notes)
|
|
2678
2895
|
};
|
|
2679
2896
|
}
|
|
@@ -2685,12 +2902,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2685
2902
|
await createWorktree(forkPath, worktreePath, "HEAD");
|
|
2686
2903
|
onStep?.("worktree created", worktreePath);
|
|
2687
2904
|
onProgress?.("performing merge in worktree...");
|
|
2688
|
-
await withTemporarySyncBaseGraft(
|
|
2689
|
-
worktreePath,
|
|
2690
|
-
"HEAD",
|
|
2691
|
-
ctx.mergeBase,
|
|
2692
|
-
() => merge(worktreePath, ctx.upstreamRef, { noCommit: true, noEdit: true })
|
|
2693
|
-
);
|
|
2905
|
+
await withTemporarySyncBaseGraft(worktreePath, "HEAD", ctx.mergeBase, () => merge(worktreePath, ctx.upstreamRef));
|
|
2694
2906
|
onStep?.("merge complete", "upstream merged into worktree");
|
|
2695
2907
|
onProgress?.("analyzing files...");
|
|
2696
2908
|
const analyzedFiles = await analyzeRefs(
|
|
@@ -2702,8 +2914,9 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2702
2914
|
onProgress
|
|
2703
2915
|
);
|
|
2704
2916
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2705
|
-
const
|
|
2706
|
-
|
|
2917
|
+
const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
|
|
2918
|
+
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts);
|
|
2919
|
+
onStep?.("analysis complete", `${analyzedFiles.length} files analyzed, dry run, no changes applied`);
|
|
2707
2920
|
onProgress?.("cleaning up worktree...");
|
|
2708
2921
|
await cleanupWorktree(forkPath, worktreePath);
|
|
2709
2922
|
return {
|
|
@@ -2712,7 +2925,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2712
2925
|
summary: calculateSummary(analyzedFiles),
|
|
2713
2926
|
// For analyze mode, count diverged files as potential conflicts
|
|
2714
2927
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2715
|
-
protectedConflicts
|
|
2928
|
+
protectedConflicts,
|
|
2716
2929
|
...resultMeta(config, ctx),
|
|
2717
2930
|
...reports,
|
|
2718
2931
|
...notesResult(notes)
|
|
@@ -2732,23 +2945,39 @@ async function runMergeEngine(config, options) {
|
|
|
2732
2945
|
throw error;
|
|
2733
2946
|
}
|
|
2734
2947
|
}
|
|
2948
|
+
async function runEngineWithSpinner(config, apply) {
|
|
2949
|
+
return runMergeEngine(config, {
|
|
2950
|
+
apply,
|
|
2951
|
+
onProgress: (message) => {
|
|
2952
|
+
spinnerText(message);
|
|
2953
|
+
},
|
|
2954
|
+
onStep: (label, detail) => {
|
|
2955
|
+
spinnerSuccess(label, detail);
|
|
2956
|
+
createSpinner("...");
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2735
2960
|
|
|
2736
2961
|
// src/services/migrate.ts
|
|
2737
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
2962
|
+
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
2738
2963
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2739
|
-
import { dirname as dirname4, join as
|
|
2964
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
2740
2965
|
var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
|
|
2966
|
+
var RESTORED_LIST_MAX = 20;
|
|
2967
|
+
async function ensureUpstreamCommit(config, sha) {
|
|
2968
|
+
const { forkPath } = config;
|
|
2969
|
+
const present = async () => !!await git(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`], forkPath, { ignoreErrors: true });
|
|
2970
|
+
if (await present()) return;
|
|
2971
|
+
await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
|
|
2972
|
+
await fetchRemote(forkPath, DEFAULT_UPSTREAM_REMOTE);
|
|
2973
|
+
if (!await present())
|
|
2974
|
+
throw new Error(`upstream commit ${sha.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2975
|
+
}
|
|
2741
2976
|
async function resolveNotesRef(config) {
|
|
2742
2977
|
const { forkPath } = config;
|
|
2743
2978
|
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2744
2979
|
if (!ref) return null;
|
|
2745
|
-
|
|
2746
|
-
if (!await present()) {
|
|
2747
|
-
await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
|
|
2748
|
-
await fetch2(forkPath, DEFAULT_UPSTREAM_REMOTE);
|
|
2749
|
-
if (!await present())
|
|
2750
|
-
throw new Error(`upstream commit ${ref.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2751
|
-
}
|
|
2980
|
+
await ensureUpstreamCommit(config, ref);
|
|
2752
2981
|
return ref;
|
|
2753
2982
|
}
|
|
2754
2983
|
async function readNotesStatus(config) {
|
|
@@ -2790,27 +3019,88 @@ function printNote(index, note, url) {
|
|
|
2790
3019
|
if (note.summary) console.info(` ${note.summary}`);
|
|
2791
3020
|
console.info(colors_default.dim(` read: pnpm cella migrate --show ${note.id}`));
|
|
2792
3021
|
if (url) console.info(colors_default.dim(` ${url}`));
|
|
2793
|
-
if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --
|
|
3022
|
+
if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --run ${note.id}`));
|
|
2794
3023
|
console.info();
|
|
2795
3024
|
}
|
|
2796
|
-
async function
|
|
3025
|
+
async function writeNoteFolder(config, ref, id) {
|
|
2797
3026
|
const { forkPath } = config;
|
|
2798
3027
|
const note = await readNote(forkPath, ref, id);
|
|
2799
3028
|
if (!note) throw new Error(`no migration note '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2800
3029
|
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${NOTES_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2801
|
-
const target =
|
|
3030
|
+
const target = join11(EXTRACT_DIR, id);
|
|
2802
3031
|
for (const file of files) {
|
|
2803
3032
|
const content = execFileSync2("git", ["show", `${ref}:${NOTES_DIR}/${id}/${file}`], {
|
|
2804
3033
|
cwd: forkPath,
|
|
2805
|
-
maxBuffer:
|
|
3034
|
+
maxBuffer: MAX_BUFFER
|
|
2806
3035
|
});
|
|
2807
|
-
const
|
|
2808
|
-
await mkdir4(dirname4(
|
|
2809
|
-
await writeFile3(
|
|
3036
|
+
const path = join11(forkPath, target, file);
|
|
3037
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
3038
|
+
await writeFile3(path, content);
|
|
2810
3039
|
}
|
|
2811
|
-
|
|
3040
|
+
return { note, target, files };
|
|
3041
|
+
}
|
|
3042
|
+
async function extractNote(config, ref, id) {
|
|
3043
|
+
const { note, target, files } = await writeNoteFolder(config, ref, id);
|
|
3044
|
+
console.info(`${checkMark} extracted ${files.length} file(s) to ${target}/`);
|
|
2812
3045
|
console.info(colors_default.dim(` the README's ${NOTES_DIR}/${id}/ paths are this folder.`));
|
|
2813
|
-
if (note.codemod)
|
|
3046
|
+
if (note.codemod) {
|
|
3047
|
+
console.info(colors_default.dim(` pnpm exec tsx ${target}/${note.codemod} inventory ${note.roots.join(" ")}`));
|
|
3048
|
+
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella migrate --run ${id}`));
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
function resolveCodemod(id, files, script) {
|
|
3052
|
+
if (script) {
|
|
3053
|
+
const name = script.replace(/^\.\//, "");
|
|
3054
|
+
if (!files.includes(name)) {
|
|
3055
|
+
throw new Error(`migration note '${id}' has no file '${script}'. its files: ${files.join(", ")}`);
|
|
3056
|
+
}
|
|
3057
|
+
return name;
|
|
3058
|
+
}
|
|
3059
|
+
const scripts = files.filter((file) => !file.includes("/") && isCodemodFile(file));
|
|
3060
|
+
if (scripts.length === 0) {
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
`migration note '${id}' ships no codemod script: its steps are manual, read them with \`pnpm cella migrate --show ${id}\`.`
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
if (scripts.length > 1) {
|
|
3066
|
+
throw new Error(
|
|
3067
|
+
`migration note '${id}' ships ${scripts.length} scripts (${scripts.join(", ")}): name the one to run with --script <file>.`
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
return scripts[0];
|
|
3071
|
+
}
|
|
3072
|
+
async function runNote(config, ref, id) {
|
|
3073
|
+
const { forkPath } = config;
|
|
3074
|
+
const { note, target, files } = await writeNoteFolder(config, ref, id);
|
|
3075
|
+
const script = join11(target, resolveCodemod(id, files, config.script));
|
|
3076
|
+
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...note.roots];
|
|
3077
|
+
const upstreamCommit = await readManifestBase(forkPath) ?? ref;
|
|
3078
|
+
await ensureUpstreamCommit(config, upstreamCommit);
|
|
3079
|
+
const identical = await listIdenticalToRef(forkPath, upstreamCommit);
|
|
3080
|
+
console.info(colors_default.dim(`running pnpm exec tsx ${script} ${args.join(" ")}`));
|
|
3081
|
+
console.info();
|
|
3082
|
+
const child = spawnSync("pnpm", ["exec", "tsx", script, ...args], { cwd: forkPath, stdio: "inherit" });
|
|
3083
|
+
if (child.error) throw new Error(`could not run the codemod: ${child.error.message}`);
|
|
3084
|
+
const stillIdentical = await listIdenticalToRef(forkPath, upstreamCommit);
|
|
3085
|
+
const restored = [...identical].filter((path) => !stillIdentical.has(path)).sort();
|
|
3086
|
+
await batchRestoreWorktreeFromRef(forkPath, upstreamCommit, restored);
|
|
3087
|
+
const upstreamShort = upstreamCommit.slice(0, 9);
|
|
3088
|
+
console.info();
|
|
3089
|
+
if (restored.length > 0) {
|
|
3090
|
+
console.info(`${checkMark} restored ${restored.length} file(s) to upstream ${upstreamShort}`);
|
|
3091
|
+
console.info(colors_default.dim(" the codemod changed them, and they were identical to upstream before the run:"));
|
|
3092
|
+
for (const path of restored.slice(0, RESTORED_LIST_MAX)) console.info(colors_default.dim(` ${path}`));
|
|
3093
|
+
if (restored.length > RESTORED_LIST_MAX) {
|
|
3094
|
+
console.info(colors_default.dim(` ... + ${restored.length - RESTORED_LIST_MAX} more`));
|
|
3095
|
+
}
|
|
3096
|
+
} else {
|
|
3097
|
+
console.info(colors_default.dim(`no file identical to upstream ${upstreamShort} was changed by the codemod.`));
|
|
3098
|
+
}
|
|
3099
|
+
if (child.status !== 0) throw new Error(`the codemod exited with code ${child.status ?? "unknown"}`);
|
|
3100
|
+
if (!config.runArgs?.length) {
|
|
3101
|
+
const apply = ["rewrite", ...note.roots].join(" ");
|
|
3102
|
+
console.info(colors_default.dim(`report only. to apply: pnpm cella migrate --run ${id} -- ${apply}`));
|
|
3103
|
+
}
|
|
2814
3104
|
}
|
|
2815
3105
|
async function markNotes(config, ref, pending, ids) {
|
|
2816
3106
|
const open = new Set(pending);
|
|
@@ -2818,7 +3108,7 @@ async function markNotes(config, ref, pending, ids) {
|
|
|
2818
3108
|
for (const id of ids) open.delete(id);
|
|
2819
3109
|
await writePending(config.forkPath, [...open]);
|
|
2820
3110
|
const marked = ids.length - unknown.length;
|
|
2821
|
-
if (marked > 0) console.info(`${
|
|
3111
|
+
if (marked > 0) console.info(`${checkMark} recorded ${marked} note(s) as handled`);
|
|
2822
3112
|
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2823
3113
|
console.info(colors_default.dim(formatNotesLine((await listNoteIds(config.forkPath, ref)).length, open.size)));
|
|
2824
3114
|
}
|
|
@@ -2833,6 +3123,7 @@ async function runMigrate(config) {
|
|
|
2833
3123
|
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2834
3124
|
const urlOf = (id) => noteUrl(githubUrl, ref, id);
|
|
2835
3125
|
if (config.mark?.length) return markNotes(config, ref, pending, config.mark);
|
|
3126
|
+
if (config.run) return runNote(config, ref, config.run);
|
|
2836
3127
|
if (config.extract) return extractNote(config, ref, config.extract);
|
|
2837
3128
|
if (config.show) {
|
|
2838
3129
|
const source = await git(["show", `${ref}:${NOTES_DIR}/${config.show}/README.md`], forkPath, {
|
|
@@ -2906,16 +3197,7 @@ function printUnifiedDiff(config, filePath) {
|
|
|
2906
3197
|
}
|
|
2907
3198
|
async function runAnalyze(config) {
|
|
2908
3199
|
createSpinner("starting analysis...");
|
|
2909
|
-
const result = await
|
|
2910
|
-
apply: false,
|
|
2911
|
-
onProgress: (message) => {
|
|
2912
|
-
spinnerText(message);
|
|
2913
|
-
},
|
|
2914
|
-
onStep: (label, detail) => {
|
|
2915
|
-
spinnerSuccess(label, detail);
|
|
2916
|
-
createSpinner("...");
|
|
2917
|
-
}
|
|
2918
|
-
});
|
|
3200
|
+
const result = await runEngineWithSpinner(config, false);
|
|
2919
3201
|
spinnerSuccess();
|
|
2920
3202
|
const scopedFiles = filterByScope(result, config.scope ?? "all");
|
|
2921
3203
|
if (config.diff) {
|
|
@@ -2937,15 +3219,15 @@ async function runAnalyze(config) {
|
|
|
2937
3219
|
upstreamChangedLines: f.upstreamChangedLines ?? null,
|
|
2938
3220
|
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null,
|
|
2939
3221
|
upstreamOnly: f.upstreamOnly ?? false,
|
|
2940
|
-
// Only on the sync config: upstream override entries the fork config does not follow
|
|
2941
|
-
upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored } : null
|
|
3222
|
+
// Only on the sync config: upstream override entries and packageJsonSync keys the fork config does not follow
|
|
3223
|
+
upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored, packageJsonSync: overrides.packageJsonSync } : null
|
|
2942
3224
|
}));
|
|
2943
3225
|
writeStdout(JSON.stringify(out, null, 2));
|
|
2944
3226
|
return result;
|
|
2945
3227
|
}
|
|
2946
3228
|
if (config.list) {
|
|
2947
3229
|
for (const file of scopedFiles) {
|
|
2948
|
-
|
|
3230
|
+
writeStdout(file.path);
|
|
2949
3231
|
}
|
|
2950
3232
|
return result;
|
|
2951
3233
|
}
|
|
@@ -2954,7 +3236,7 @@ async function runAnalyze(config) {
|
|
|
2954
3236
|
if (!file) throw new Error(`file not found in analysis results: ${config.openDiff}`);
|
|
2955
3237
|
const patch = gitDiffFile(config.forkPath, config.upstreamRef, file.path);
|
|
2956
3238
|
if (patch.length === 0) {
|
|
2957
|
-
console.info(`${
|
|
3239
|
+
console.info(`${checkMark} ${file.path} is identical to upstream`);
|
|
2958
3240
|
return result;
|
|
2959
3241
|
}
|
|
2960
3242
|
const pagePath = await openDiffInBrowser(
|
|
@@ -2967,7 +3249,7 @@ async function runAnalyze(config) {
|
|
|
2967
3249
|
},
|
|
2968
3250
|
config.forkPath
|
|
2969
3251
|
);
|
|
2970
|
-
console.info(`${
|
|
3252
|
+
console.info(`${checkMark} opened browser diff for ${file.path}`);
|
|
2971
3253
|
console.info(colors_default.dim(` ${pagePath}`));
|
|
2972
3254
|
return result;
|
|
2973
3255
|
}
|
|
@@ -2977,41 +3259,34 @@ async function runAnalyze(config) {
|
|
|
2977
3259
|
fileLinkMode: config.settings.fileLinkMode,
|
|
2978
3260
|
forkPath: config.forkPath
|
|
2979
3261
|
};
|
|
2980
|
-
printAnalysisFileGroups(result.files, linkOptions);
|
|
2981
|
-
|
|
2982
|
-
printIgnoredUpstreamChanges(result);
|
|
2983
|
-
printUpstreamOverrideChanges(result);
|
|
2984
|
-
printMaskingPinWarning(result.files);
|
|
3262
|
+
printAnalysisFileGroups(result.files, linkOptions, result);
|
|
3263
|
+
printEngineReports(result, "analysis summary");
|
|
2985
3264
|
await printMigrationNotesLine(config, result);
|
|
2986
|
-
|
|
2987
|
-
const logPath = writeLogFile(config.forkPath, result.files);
|
|
2988
|
-
console.info();
|
|
2989
|
-
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
2990
|
-
}
|
|
3265
|
+
printLogFileReport(config, result.files);
|
|
2991
3266
|
console.info();
|
|
2992
3267
|
return result;
|
|
2993
3268
|
}
|
|
2994
3269
|
|
|
2995
3270
|
// src/services/audit.ts
|
|
2996
|
-
import { spawnSync } from "child_process";
|
|
2997
|
-
import
|
|
2998
|
-
import
|
|
3271
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3272
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3273
|
+
import { basename as basename5, join as join13, relative as relative2 } from "path";
|
|
2999
3274
|
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
3000
3275
|
|
|
3001
3276
|
// src/utils/audit-utils.ts
|
|
3002
3277
|
import { execFile as execFile2 } from "child_process";
|
|
3003
|
-
import
|
|
3004
|
-
import
|
|
3278
|
+
import { existsSync as existsSync8, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3279
|
+
import { join as join12 } from "path";
|
|
3005
3280
|
import { promisify as promisify2 } from "util";
|
|
3006
3281
|
var execFileAsync2 = promisify2(execFile2);
|
|
3007
|
-
var CACHE_FILE =
|
|
3282
|
+
var CACHE_FILE = join12(import.meta.dirname, "..", "..", ".audit.cache.json");
|
|
3008
3283
|
var CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
|
|
3009
3284
|
var CHANGELOG_PATHS = ["CHANGELOG.md", "CHANGELOG", "changelog.md", "HISTORY.md", "CHANGES.md", "NEWS.md"];
|
|
3010
3285
|
var DEFAULT_BRANCHES = ["main", "master"];
|
|
3011
3286
|
function loadCache() {
|
|
3012
3287
|
try {
|
|
3013
|
-
if (
|
|
3014
|
-
return JSON.parse(
|
|
3288
|
+
if (existsSync8(CACHE_FILE)) {
|
|
3289
|
+
return JSON.parse(readFileSync2(CACHE_FILE, "utf8"));
|
|
3015
3290
|
}
|
|
3016
3291
|
} catch {
|
|
3017
3292
|
}
|
|
@@ -3019,15 +3294,15 @@ function loadCache() {
|
|
|
3019
3294
|
}
|
|
3020
3295
|
function saveCache(cache) {
|
|
3021
3296
|
try {
|
|
3022
|
-
|
|
3297
|
+
writeFileSync3(CACHE_FILE, `${JSON.stringify(cache, null, 2)}
|
|
3023
3298
|
`);
|
|
3024
3299
|
} catch {
|
|
3025
3300
|
}
|
|
3026
3301
|
}
|
|
3027
3302
|
function clearCache() {
|
|
3028
3303
|
try {
|
|
3029
|
-
if (
|
|
3030
|
-
|
|
3304
|
+
if (existsSync8(CACHE_FILE)) {
|
|
3305
|
+
unlinkSync(CACHE_FILE);
|
|
3031
3306
|
console.info(colors_default.green("\u2713 cache cleared successfully"));
|
|
3032
3307
|
} else {
|
|
3033
3308
|
console.info(colors_default.yellow("no cache file found"));
|
|
@@ -3115,56 +3390,32 @@ function isMajorVersionChange(current, latest) {
|
|
|
3115
3390
|
if (!currentMajor || !latestMajor) return false;
|
|
3116
3391
|
return Number.parseInt(latestMajor, 10) > Number.parseInt(currentMajor, 10);
|
|
3117
3392
|
}
|
|
3118
|
-
async function
|
|
3393
|
+
async function runPnpmJson(args, cwd) {
|
|
3394
|
+
const parse = (stdout) => {
|
|
3395
|
+
if (!stdout || stdout.trim() === "") return null;
|
|
3396
|
+
try {
|
|
3397
|
+
return JSON.parse(stdout);
|
|
3398
|
+
} catch {
|
|
3399
|
+
return null;
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3119
3402
|
try {
|
|
3120
|
-
const { stdout } = await execFileAsync2("pnpm",
|
|
3121
|
-
encoding: "
|
|
3403
|
+
const { stdout } = await execFileAsync2("pnpm", args, {
|
|
3404
|
+
encoding: "utf8",
|
|
3122
3405
|
maxBuffer: 10 * 1024 * 1024,
|
|
3123
3406
|
// 10MB buffer for large outputs
|
|
3124
3407
|
cwd
|
|
3125
3408
|
});
|
|
3126
|
-
|
|
3127
|
-
return {};
|
|
3128
|
-
}
|
|
3129
|
-
return JSON.parse(stdout);
|
|
3409
|
+
return parse(stdout);
|
|
3130
3410
|
} catch (error) {
|
|
3131
|
-
|
|
3132
|
-
const stdout = error.stdout;
|
|
3133
|
-
if (stdout?.trim()) {
|
|
3134
|
-
try {
|
|
3135
|
-
return JSON.parse(stdout);
|
|
3136
|
-
} catch {
|
|
3137
|
-
return {};
|
|
3138
|
-
}
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
return {};
|
|
3411
|
+
return parse(error instanceof Error && "stdout" in error ? error.stdout : void 0);
|
|
3142
3412
|
}
|
|
3143
3413
|
}
|
|
3414
|
+
async function getOutdatedPackages(cwd) {
|
|
3415
|
+
return await runPnpmJson(["-r", "outdated", "--json"], cwd) ?? {};
|
|
3416
|
+
}
|
|
3144
3417
|
async function runPnpmAudit(cwd) {
|
|
3145
|
-
|
|
3146
|
-
const { stdout } = await execFileAsync2("pnpm", ["audit", "--json"], {
|
|
3147
|
-
encoding: "utf-8",
|
|
3148
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
3149
|
-
cwd
|
|
3150
|
-
});
|
|
3151
|
-
if (!stdout || stdout.trim() === "") {
|
|
3152
|
-
return null;
|
|
3153
|
-
}
|
|
3154
|
-
return JSON.parse(stdout);
|
|
3155
|
-
} catch (error) {
|
|
3156
|
-
if (error instanceof Error && "stdout" in error) {
|
|
3157
|
-
const stdout = error.stdout;
|
|
3158
|
-
if (stdout?.trim()) {
|
|
3159
|
-
try {
|
|
3160
|
-
return JSON.parse(stdout);
|
|
3161
|
-
} catch {
|
|
3162
|
-
return null;
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
}
|
|
3166
|
-
return null;
|
|
3167
|
-
}
|
|
3418
|
+
return await runPnpmJson(["audit", "--json"], cwd);
|
|
3168
3419
|
}
|
|
3169
3420
|
function parseDependencyPath(paths) {
|
|
3170
3421
|
for (const pathStr of paths) {
|
|
@@ -3186,12 +3437,9 @@ function buildVulnerabilityMap(auditResult) {
|
|
|
3186
3437
|
const { workspace, directDependency } = parseDependencyPath(allPaths);
|
|
3187
3438
|
const existing = map.get(advisory.module_name) || [];
|
|
3188
3439
|
existing.push({
|
|
3189
|
-
id: advisory.id,
|
|
3190
3440
|
title: advisory.title,
|
|
3191
3441
|
severity: advisory.severity,
|
|
3192
|
-
url: advisory.url,
|
|
3193
3442
|
vulnerableVersions: advisory.vulnerable_versions,
|
|
3194
|
-
patchedVersions: advisory.patched_versions,
|
|
3195
3443
|
cves: advisory.cves || [],
|
|
3196
3444
|
workspace,
|
|
3197
3445
|
directDependency
|
|
@@ -3200,19 +3448,15 @@ function buildVulnerabilityMap(auditResult) {
|
|
|
3200
3448
|
}
|
|
3201
3449
|
return map;
|
|
3202
3450
|
}
|
|
3451
|
+
var vulnIcons = {
|
|
3452
|
+
critical: colors_default.red("\u25CF"),
|
|
3453
|
+
high: colors_default.red("\u25CF"),
|
|
3454
|
+
moderate: colors_default.yellow("\u25CF"),
|
|
3455
|
+
low: colors_default.blue("\u25CF"),
|
|
3456
|
+
info: colors_default.gray("\u25CF")
|
|
3457
|
+
};
|
|
3203
3458
|
function getVulnIcon(severity) {
|
|
3204
|
-
|
|
3205
|
-
case "critical":
|
|
3206
|
-
return colors_default.red("\u25CF");
|
|
3207
|
-
case "high":
|
|
3208
|
-
return colors_default.red("\u25CF");
|
|
3209
|
-
case "moderate":
|
|
3210
|
-
return colors_default.yellow("\u25CF");
|
|
3211
|
-
case "low":
|
|
3212
|
-
return colors_default.blue("\u25CF");
|
|
3213
|
-
default:
|
|
3214
|
-
return colors_default.gray("\u25CF");
|
|
3215
|
-
}
|
|
3459
|
+
return vulnIcons[severity] ?? colors_default.gray("\u25CF");
|
|
3216
3460
|
}
|
|
3217
3461
|
function getHighestSeverity(vulns) {
|
|
3218
3462
|
if (vulns.length === 0) return null;
|
|
@@ -3222,13 +3466,13 @@ function getHighestSeverity(vulns) {
|
|
|
3222
3466
|
}
|
|
3223
3467
|
return null;
|
|
3224
3468
|
}
|
|
3225
|
-
function middleTruncate(
|
|
3226
|
-
if (
|
|
3469
|
+
function middleTruncate(str2, maxLen) {
|
|
3470
|
+
if (str2.length <= maxLen) return str2;
|
|
3227
3471
|
const ellipsis = "\u2026";
|
|
3228
3472
|
const charsToShow = maxLen - ellipsis.length;
|
|
3229
3473
|
const frontChars = Math.ceil(charsToShow / 2);
|
|
3230
3474
|
const backChars = Math.floor(charsToShow / 2);
|
|
3231
|
-
return
|
|
3475
|
+
return str2.slice(0, frontChars) + ellipsis + str2.slice(-backChars);
|
|
3232
3476
|
}
|
|
3233
3477
|
function formatDependents(dependents) {
|
|
3234
3478
|
if (dependents.length === 0) return "";
|
|
@@ -3273,18 +3517,42 @@ function parseYamlBlockList(content, key) {
|
|
|
3273
3517
|
}
|
|
3274
3518
|
|
|
3275
3519
|
// src/services/audit.ts
|
|
3276
|
-
|
|
3520
|
+
var AUDIT_FIX_VALUE = "__audit_fix__";
|
|
3521
|
+
function createDependentPkgReader() {
|
|
3522
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3523
|
+
return (location) => {
|
|
3524
|
+
let cached = cache.get(location);
|
|
3525
|
+
if (cached === void 0) {
|
|
3526
|
+
try {
|
|
3527
|
+
cached = JSON.parse(readFileSync3(join13(location, "package.json"), "utf8"));
|
|
3528
|
+
} catch {
|
|
3529
|
+
cached = null;
|
|
3530
|
+
}
|
|
3531
|
+
cache.set(location, cached ?? null);
|
|
3532
|
+
}
|
|
3533
|
+
return cached ?? null;
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
function formatSeverityCounts(counts, suffix = "") {
|
|
3537
|
+
const parts = [];
|
|
3538
|
+
if (counts.critical) parts.push(colors_default.red(`${counts.critical} critical${suffix}`));
|
|
3539
|
+
if (counts.high) parts.push(colors_default.red(`${counts.high} high${suffix}`));
|
|
3540
|
+
if (counts.moderate) parts.push(colors_default.yellow(`${counts.moderate} moderate${suffix}`));
|
|
3541
|
+
if (counts.low) parts.push(colors_default.blue(`${counts.low} low`));
|
|
3542
|
+
return parts;
|
|
3543
|
+
}
|
|
3544
|
+
async function runAudit(config) {
|
|
3277
3545
|
const { forkPath } = config;
|
|
3278
|
-
if (
|
|
3546
|
+
if (config.checkOverrides) {
|
|
3279
3547
|
await checkOverrides(forkPath);
|
|
3280
3548
|
return;
|
|
3281
3549
|
}
|
|
3282
3550
|
const spinner = createSpinner("checking for outdated packages & vulnerabilities...");
|
|
3283
3551
|
try {
|
|
3284
|
-
if (
|
|
3552
|
+
if (config.force) {
|
|
3285
3553
|
spinnerText("clearing pnpm metadata cache...");
|
|
3286
3554
|
clearCache();
|
|
3287
|
-
|
|
3555
|
+
spawnSync2("pnpm", ["cache", "delete", "*"], {
|
|
3288
3556
|
cwd: forkPath,
|
|
3289
3557
|
stdio: "ignore"
|
|
3290
3558
|
});
|
|
@@ -3298,25 +3566,13 @@ async function runAudit(config, options = {}) {
|
|
|
3298
3566
|
return;
|
|
3299
3567
|
}
|
|
3300
3568
|
const cache = loadCache();
|
|
3301
|
-
spinnerText(`found ${packageNames.length} outdated package(s)
|
|
3302
|
-
const
|
|
3303
|
-
const readDependentPkg = (location) => {
|
|
3304
|
-
let cached = pkgJsonCache.get(location);
|
|
3305
|
-
if (cached === void 0) {
|
|
3306
|
-
try {
|
|
3307
|
-
cached = JSON.parse(fs2.readFileSync(path2.join(location, "package.json"), "utf-8"));
|
|
3308
|
-
} catch {
|
|
3309
|
-
cached = null;
|
|
3310
|
-
}
|
|
3311
|
-
pkgJsonCache.set(location, cached ?? null);
|
|
3312
|
-
}
|
|
3313
|
-
return cached ?? null;
|
|
3314
|
-
};
|
|
3569
|
+
spinnerText(`found ${packageNames.length} outdated package(s), fetching metadata...`);
|
|
3570
|
+
const readDependentPkg = createDependentPkgReader();
|
|
3315
3571
|
const dependentNameCache = /* @__PURE__ */ new Map();
|
|
3316
3572
|
for (const name of packageNames) {
|
|
3317
3573
|
for (const dep of outdatedPackages[name].dependentPackages) {
|
|
3318
3574
|
if (!dependentNameCache.has(dep.location)) {
|
|
3319
|
-
dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name ||
|
|
3575
|
+
dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name || basename5(dep.location));
|
|
3320
3576
|
}
|
|
3321
3577
|
}
|
|
3322
3578
|
}
|
|
@@ -3427,7 +3683,7 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3427
3683
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
3428
3684
|
for (const pkg of enhancedPackages) {
|
|
3429
3685
|
for (const loc of pkg.dependentLocations) {
|
|
3430
|
-
const dependentName = dependentNameCache.get(loc) ||
|
|
3686
|
+
const dependentName = dependentNameCache.get(loc) || basename5(loc);
|
|
3431
3687
|
const existing = dependentMap.get(dependentName);
|
|
3432
3688
|
if (existing) {
|
|
3433
3689
|
existing.count++;
|
|
@@ -3439,34 +3695,15 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3439
3695
|
const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
3440
3696
|
const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
|
|
3441
3697
|
for (const [dependentName, { location, count }] of sortedDependents) {
|
|
3442
|
-
const packageJsonPath2 =
|
|
3443
|
-
const relativePath =
|
|
3698
|
+
const packageJsonPath2 = join13(location, "package.json");
|
|
3699
|
+
const relativePath = relative2(forkPath, packageJsonPath2);
|
|
3444
3700
|
const paddedName = dependentName.padEnd(maxDependentNameLen);
|
|
3445
3701
|
console.info(
|
|
3446
3702
|
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count}`)}`
|
|
3447
3703
|
);
|
|
3448
3704
|
}
|
|
3449
3705
|
}
|
|
3450
|
-
|
|
3451
|
-
const hasVulnerabilities = auditResult !== null && Object.values(auditResult.metadata?.vulnerabilities || {}).some((c) => c > 0);
|
|
3452
|
-
const hasPackages = packages.length > 0;
|
|
3453
|
-
let confirmMessage = "update packages?";
|
|
3454
|
-
if (hasVulnerabilities && hasPackages) {
|
|
3455
|
-
confirmMessage = "update packages and fix vulnerabilities?";
|
|
3456
|
-
} else if (hasVulnerabilities) {
|
|
3457
|
-
confirmMessage = "fix vulnerabilities?";
|
|
3458
|
-
}
|
|
3459
|
-
let wantsUpdate;
|
|
3460
|
-
try {
|
|
3461
|
-
wantsUpdate = await confirm({
|
|
3462
|
-
message: confirmMessage,
|
|
3463
|
-
default: true
|
|
3464
|
-
});
|
|
3465
|
-
} catch {
|
|
3466
|
-
return;
|
|
3467
|
-
}
|
|
3468
|
-
if (!wantsUpdate) return;
|
|
3469
|
-
console.info();
|
|
3706
|
+
function buildUpdateChoices(packages, auditResult, hasVulnerabilities) {
|
|
3470
3707
|
const makeChoice = (pkg) => {
|
|
3471
3708
|
const devTag = pkg.isDev ? colors_default.dim(" (dev)") : "";
|
|
3472
3709
|
const version = `${colors_default.red(pkg.current)} \u2192 ${colors_default.green(pkg.latest)}`;
|
|
@@ -3477,17 +3714,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3477
3714
|
checked: false
|
|
3478
3715
|
};
|
|
3479
3716
|
};
|
|
3480
|
-
const AUDIT_FIX_VALUE = "__audit_fix__";
|
|
3481
3717
|
const pinned = packages.filter((p) => p.pinnedIn.length > 0);
|
|
3482
3718
|
const regular = packages.filter((p) => p.pinnedIn.length === 0);
|
|
3483
3719
|
const choices = [];
|
|
3484
3720
|
if (hasVulnerabilities) {
|
|
3485
|
-
const
|
|
3486
|
-
const parts = [];
|
|
3487
|
-
if (vulnMeta.critical) parts.push(colors_default.red(`${vulnMeta.critical} critical`));
|
|
3488
|
-
if (vulnMeta.high) parts.push(colors_default.red(`${vulnMeta.high} high`));
|
|
3489
|
-
if (vulnMeta.moderate) parts.push(colors_default.yellow(`${vulnMeta.moderate} moderate`));
|
|
3490
|
-
if (vulnMeta.low) parts.push(colors_default.blue(`${vulnMeta.low} low`));
|
|
3721
|
+
const parts = formatSeverityCounts(auditResult?.metadata?.vulnerabilities || {});
|
|
3491
3722
|
const vulnSummary = parts.length > 0 ? ` ${colors_default.dim("\xB7")} ${parts.join(" ")}` : "";
|
|
3492
3723
|
choices.push(new Separator(colors_default.red(`\u2500\u2500 vulnerabilities${vulnSummary} \u2500\u2500`)));
|
|
3493
3724
|
choices.push({
|
|
@@ -3506,6 +3737,29 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3506
3737
|
}
|
|
3507
3738
|
choices.push(...regular.map(makeChoice));
|
|
3508
3739
|
}
|
|
3740
|
+
return choices;
|
|
3741
|
+
}
|
|
3742
|
+
async function promptForUpdates(packages, forkPath, auditResult) {
|
|
3743
|
+
const hasVulnerabilities = auditResult !== null && Object.values(auditResult.metadata?.vulnerabilities || {}).some((c) => c > 0);
|
|
3744
|
+
const hasPackages = packages.length > 0;
|
|
3745
|
+
let confirmMessage = "update packages?";
|
|
3746
|
+
if (hasVulnerabilities && hasPackages) {
|
|
3747
|
+
confirmMessage = "update packages and fix vulnerabilities?";
|
|
3748
|
+
} else if (hasVulnerabilities) {
|
|
3749
|
+
confirmMessage = "fix vulnerabilities?";
|
|
3750
|
+
}
|
|
3751
|
+
let wantsUpdate;
|
|
3752
|
+
try {
|
|
3753
|
+
wantsUpdate = await confirm({
|
|
3754
|
+
message: confirmMessage,
|
|
3755
|
+
default: true
|
|
3756
|
+
});
|
|
3757
|
+
} catch {
|
|
3758
|
+
return;
|
|
3759
|
+
}
|
|
3760
|
+
if (!wantsUpdate) return;
|
|
3761
|
+
console.info();
|
|
3762
|
+
const choices = buildUpdateChoices(packages, auditResult, hasVulnerabilities);
|
|
3509
3763
|
let selected;
|
|
3510
3764
|
try {
|
|
3511
3765
|
selected = await checkbox({
|
|
@@ -3533,6 +3787,9 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3533
3787
|
}
|
|
3534
3788
|
const runAuditFix = selected.includes(AUDIT_FIX_VALUE);
|
|
3535
3789
|
const selectedPackages = selected.filter((s) => s !== AUDIT_FIX_VALUE);
|
|
3790
|
+
await executeUpdates(packages, forkPath, selectedPackages, runAuditFix);
|
|
3791
|
+
}
|
|
3792
|
+
async function executeUpdates(packages, forkPath, selectedPackages, runAuditFix) {
|
|
3536
3793
|
const summaryParts = [];
|
|
3537
3794
|
if (selectedPackages.length > 0) {
|
|
3538
3795
|
summaryParts.push(`updating ${selectedPackages.length} package${selectedPackages.length !== 1 ? "s" : ""}`);
|
|
@@ -3564,17 +3821,15 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3564
3821
|
for (const pkgName of selectedPackages) {
|
|
3565
3822
|
const pkg = packages.find((p) => p.name === pkgName);
|
|
3566
3823
|
if (!pkg) continue;
|
|
3567
|
-
for (
|
|
3568
|
-
const
|
|
3569
|
-
if (
|
|
3570
|
-
|
|
3571
|
-
}
|
|
3572
|
-
workspacePackages.get(dependent)?.packages.push(pkgName);
|
|
3824
|
+
for (const dependent of pkg.dependents) {
|
|
3825
|
+
const group = workspacePackages.get(dependent);
|
|
3826
|
+
if (group) group.push(pkgName);
|
|
3827
|
+
else workspacePackages.set(dependent, [pkgName]);
|
|
3573
3828
|
}
|
|
3574
3829
|
}
|
|
3575
3830
|
if (runAuditFix) {
|
|
3576
3831
|
console.info(colors_default.dim("running pnpm audit --fix override..."));
|
|
3577
|
-
const auditFixResult =
|
|
3832
|
+
const auditFixResult = spawnSync2("pnpm", ["audit", "--fix", "override"], {
|
|
3578
3833
|
cwd: forkPath,
|
|
3579
3834
|
stdio: "inherit"
|
|
3580
3835
|
});
|
|
@@ -3585,7 +3840,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3585
3840
|
console.error(colors_default.red(`\u2717 pnpm audit --fix override failed (exit code ${auditFixResult.status})`));
|
|
3586
3841
|
}
|
|
3587
3842
|
}
|
|
3588
|
-
for (const [workspace,
|
|
3843
|
+
for (const [workspace, pkgs] of workspacePackages) {
|
|
3589
3844
|
const pkgList = pkgs.join(", ");
|
|
3590
3845
|
console.info(`${colors_default.cyan("\u2191")} ${colors_default.bold(workspace)}: ${colors_default.dim(pkgList)}`);
|
|
3591
3846
|
}
|
|
@@ -3594,7 +3849,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3594
3849
|
const selectedCatalogPackages = selectedPackages.filter((pkgName) => catalogPackages.has(pkgName));
|
|
3595
3850
|
if (selectedCatalogPackages.length > 0) {
|
|
3596
3851
|
console.info(colors_default.dim(`normalizing catalog consumers: ${selectedCatalogPackages.join(", ")}`));
|
|
3597
|
-
const normalizeResult =
|
|
3852
|
+
const normalizeResult = spawnSync2("pnpm", ["-r", "up", ...selectedCatalogPackages], {
|
|
3598
3853
|
cwd: forkPath,
|
|
3599
3854
|
stdio: "inherit"
|
|
3600
3855
|
});
|
|
@@ -3604,7 +3859,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3604
3859
|
return;
|
|
3605
3860
|
}
|
|
3606
3861
|
}
|
|
3607
|
-
const result =
|
|
3862
|
+
const result = spawnSync2("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
|
|
3608
3863
|
cwd: forkPath,
|
|
3609
3864
|
stdio: "inherit"
|
|
3610
3865
|
});
|
|
@@ -3619,15 +3874,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3619
3874
|
function printVulnerabilityResults(auditResult, vulnMap) {
|
|
3620
3875
|
if (auditResult && vulnMap.size > 0) {
|
|
3621
3876
|
const vulnMeta = auditResult.metadata?.vulnerabilities || {};
|
|
3622
|
-
const criticalCount = vulnMeta.critical || 0;
|
|
3623
|
-
const highCount = vulnMeta.high || 0;
|
|
3624
|
-
const moderateCount = vulnMeta.moderate || 0;
|
|
3625
|
-
const lowCount = vulnMeta.low || 0;
|
|
3626
3877
|
console.info();
|
|
3627
3878
|
console.info(
|
|
3628
|
-
`${colors_default.red("\u26A0")} ${colors_default.bold("vulnerabilities")} ${colors_default.dim("\xB7")}
|
|
3879
|
+
`${colors_default.red("\u26A0")} ${colors_default.bold("vulnerabilities")} ${colors_default.dim("\xB7")} ${formatSeverityCounts(vulnMeta, " ").join("")}`
|
|
3629
3880
|
);
|
|
3630
|
-
console.info(
|
|
3881
|
+
console.info(DIVIDER);
|
|
3631
3882
|
for (const [pkgName, vulns] of vulnMap.entries()) {
|
|
3632
3883
|
for (const vuln of vulns) {
|
|
3633
3884
|
const severityIcon = getVulnIcon(vuln.severity);
|
|
@@ -3647,14 +3898,14 @@ function printVulnerabilityResults(auditResult, vulnMap) {
|
|
|
3647
3898
|
}
|
|
3648
3899
|
} else {
|
|
3649
3900
|
console.info();
|
|
3650
|
-
console.info(`${
|
|
3901
|
+
console.info(`${checkMark} no vulnerabilities found`);
|
|
3651
3902
|
}
|
|
3652
3903
|
console.info();
|
|
3653
3904
|
}
|
|
3654
3905
|
function readCatalogPackageNames(forkPath) {
|
|
3655
|
-
const workspacePath =
|
|
3906
|
+
const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
|
|
3656
3907
|
try {
|
|
3657
|
-
const content =
|
|
3908
|
+
const content = readFileSync3(workspacePath, "utf8");
|
|
3658
3909
|
return new Set(Object.keys(parseYamlBlockMap(content, "catalog")));
|
|
3659
3910
|
} catch {
|
|
3660
3911
|
return /* @__PURE__ */ new Set();
|
|
@@ -3662,18 +3913,18 @@ function readCatalogPackageNames(forkPath) {
|
|
|
3662
3913
|
}
|
|
3663
3914
|
function readOverrides(forkPath) {
|
|
3664
3915
|
const overrides = /* @__PURE__ */ new Map();
|
|
3665
|
-
const pkgJsonPath =
|
|
3916
|
+
const pkgJsonPath = join13(forkPath, "package.json");
|
|
3666
3917
|
try {
|
|
3667
|
-
const pkgJson = JSON.parse(
|
|
3918
|
+
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
3668
3919
|
const pnpmOverrides = pkgJson.pnpm?.overrides || {};
|
|
3669
3920
|
for (const [key, value] of Object.entries(pnpmOverrides)) {
|
|
3670
3921
|
overrides.set(key, { target: value, source: "package.json" });
|
|
3671
3922
|
}
|
|
3672
3923
|
} catch {
|
|
3673
3924
|
}
|
|
3674
|
-
const workspacePath =
|
|
3925
|
+
const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
|
|
3675
3926
|
try {
|
|
3676
|
-
const content =
|
|
3927
|
+
const content = readFileSync3(workspacePath, "utf8");
|
|
3677
3928
|
const wsOverrides = parseYamlBlockMap(content, "overrides");
|
|
3678
3929
|
for (const [key, value] of Object.entries(wsOverrides)) {
|
|
3679
3930
|
overrides.set(key, { target: value, source: "pnpm-workspace.yaml" });
|
|
@@ -3716,7 +3967,7 @@ async function checkOverrides(forkPath) {
|
|
|
3716
3967
|
const maxTargetLen = Math.max(6, ...allValues.map((v) => v.target.length));
|
|
3717
3968
|
if (securityOverrides.length > 0) {
|
|
3718
3969
|
console.info(colors_default.bold(`security overrides (${securityOverrides.length})`));
|
|
3719
|
-
console.info(
|
|
3970
|
+
console.info(DIVIDER);
|
|
3720
3971
|
for (const o of securityOverrides) {
|
|
3721
3972
|
const icon = o.status === "active" ? colors_default.red("\u25CF") : colors_default.green("\u25CB");
|
|
3722
3973
|
const statusText = o.status === "active" ? colors_default.red("active") : colors_default.green("likely stale");
|
|
@@ -3729,7 +3980,7 @@ async function checkOverrides(forkPath) {
|
|
|
3729
3980
|
}
|
|
3730
3981
|
if (pinOverrides.length > 0) {
|
|
3731
3982
|
console.info(colors_default.bold(`version pins (${pinOverrides.length})`));
|
|
3732
|
-
console.info(
|
|
3983
|
+
console.info(DIVIDER);
|
|
3733
3984
|
for (const o of pinOverrides) {
|
|
3734
3985
|
const keyText = colors_default.white(o.key.padEnd(maxKeyLen));
|
|
3735
3986
|
const targetText = colors_default.dim(o.target.padEnd(maxTargetLen));
|
|
@@ -3742,26 +3993,24 @@ async function checkOverrides(forkPath) {
|
|
|
3742
3993
|
const activeCount = securityOverrides.filter((o) => o.status === "active").length;
|
|
3743
3994
|
if (staleCount > 0) {
|
|
3744
3995
|
console.info(
|
|
3745
|
-
`${colors_default.green("\u25CB")} ${staleCount} override${staleCount !== 1 ? "s" : ""} likely stale
|
|
3996
|
+
`${colors_default.green("\u25CB")} ${staleCount} override${staleCount !== 1 ? "s" : ""} likely stale: no matching advisory found in current audit`
|
|
3746
3997
|
);
|
|
3747
|
-
console.info(colors_default.dim(" overrides mask vulnerabilities
|
|
3998
|
+
console.info(colors_default.dim(" overrides mask vulnerabilities: verify by temporarily removing and running pnpm audit"));
|
|
3748
3999
|
}
|
|
3749
4000
|
if (activeCount > 0) {
|
|
3750
4001
|
console.info(
|
|
3751
|
-
`${colors_default.red("\u25CF")} ${activeCount} override${activeCount !== 1 ? "s" : ""} still active
|
|
4002
|
+
`${colors_default.red("\u25CF")} ${activeCount} override${activeCount !== 1 ? "s" : ""} still active: advisory present without override`
|
|
3752
4003
|
);
|
|
3753
4004
|
}
|
|
3754
4005
|
if (pinOverrides.length > 0) {
|
|
3755
4006
|
console.info(
|
|
3756
|
-
`${colors_default.blue("\u25C6")} ${pinOverrides.length} version pin${pinOverrides.length !== 1 ? "s" : ""}
|
|
4007
|
+
`${colors_default.blue("\u25C6")} ${pinOverrides.length} version pin${pinOverrides.length !== 1 ? "s" : ""}: not security-related, review manually`
|
|
3757
4008
|
);
|
|
3758
4009
|
}
|
|
3759
4010
|
console.info();
|
|
3760
4011
|
}
|
|
3761
4012
|
|
|
3762
4013
|
// src/services/contributions.ts
|
|
3763
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
3764
|
-
import { join as join16 } from "path";
|
|
3765
4014
|
import {
|
|
3766
4015
|
createPrompt,
|
|
3767
4016
|
isDownKey,
|
|
@@ -3769,14 +4018,13 @@ import {
|
|
|
3769
4018
|
isSpaceKey,
|
|
3770
4019
|
isUpKey,
|
|
3771
4020
|
useKeypress,
|
|
3772
|
-
useMemo,
|
|
3773
4021
|
usePagination,
|
|
3774
4022
|
useState
|
|
3775
4023
|
} from "@inquirer/core";
|
|
3776
4024
|
import { select as select2 } from "@inquirer/prompts";
|
|
3777
4025
|
|
|
3778
4026
|
// src/services/contrib-core.ts
|
|
3779
|
-
import { dirname as dirname5, join as
|
|
4027
|
+
import { dirname as dirname5, join as join14, resolve as resolve5 } from "path";
|
|
3780
4028
|
async function listFilesAtRef(repoPath, ref) {
|
|
3781
4029
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
3782
4030
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -3784,8 +4032,8 @@ async function listFilesAtRef(repoPath, ref) {
|
|
|
3784
4032
|
async function detectContributableFiles(repoPath, baseRef, forkRef, config, forkTerritory = []) {
|
|
3785
4033
|
const mergeBase = (await git(["merge-base", baseRef, forkRef], repoPath, { ignoreErrors: true })).trim() || baseRef;
|
|
3786
4034
|
const predicates = {
|
|
3787
|
-
isIgnored: (
|
|
3788
|
-
isPinned: (
|
|
4035
|
+
isIgnored: (path) => isIgnored(path, config) || isUnderAnyFolder(path, forkTerritory),
|
|
4036
|
+
isPinned: (path) => isPinned(path, config)
|
|
3789
4037
|
};
|
|
3790
4038
|
const analyzed = await analyzeRefs(repoPath, baseRef, forkRef, mergeBase, predicates);
|
|
3791
4039
|
await enrichChangeInfo(repoPath, analyzed, mergeBase, baseRef, forkRef);
|
|
@@ -3839,8 +4087,8 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
3839
4087
|
}
|
|
3840
4088
|
async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
|
|
3841
4089
|
const branch = `contrib/${forkName}`;
|
|
3842
|
-
const gitDir =
|
|
3843
|
-
const tmpIndex =
|
|
4090
|
+
const gitDir = resolve5(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
4091
|
+
const tmpIndex = join14(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
3844
4092
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
3845
4093
|
try {
|
|
3846
4094
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -3884,14 +4132,14 @@ ${commitBody}`;
|
|
|
3884
4132
|
|
|
3885
4133
|
// src/services/fork-utils.ts
|
|
3886
4134
|
import { existsSync as existsSync9 } from "fs";
|
|
3887
|
-
import { dirname as dirname6, resolve as
|
|
4135
|
+
import { dirname as dirname6, resolve as resolve6 } from "path";
|
|
3888
4136
|
async function resolveForkBasePath(repoPath) {
|
|
3889
4137
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
3890
|
-
return commonDir ? dirname6(
|
|
4138
|
+
return commonDir ? dirname6(resolve6(repoPath, commonDir)) : repoPath;
|
|
3891
4139
|
}
|
|
3892
4140
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
3893
|
-
const resolvedPath =
|
|
3894
|
-
if (!resolvedPath.startsWith(
|
|
4141
|
+
const resolvedPath = resolve6(basePath, fork.localPath);
|
|
4142
|
+
if (!resolvedPath.startsWith(resolve6(basePath, ".."))) {
|
|
3895
4143
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
3896
4144
|
}
|
|
3897
4145
|
if (!existsSync9(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
@@ -3913,20 +4161,14 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
|
3913
4161
|
if (patch.length === 0) return null;
|
|
3914
4162
|
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
3915
4163
|
}
|
|
3916
|
-
function shouldApplyContributionsUnstaged(repoPath) {
|
|
3917
|
-
return JSON.parse(readFileSync4(join16(repoPath, "package.json"), "utf8")).name === "cella";
|
|
3918
|
-
}
|
|
3919
4164
|
var contribPrompt = createPrompt((config, done) => {
|
|
3920
4165
|
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
3921
4166
|
const [items, setItems] = useState(config.items);
|
|
3922
4167
|
const [active, setActive] = useState(0);
|
|
3923
4168
|
const [statusMsg, setStatusMsg] = useState("");
|
|
3924
4169
|
const [promptStatus, setPromptStatus] = useState("idle");
|
|
3925
|
-
const bounds = useMemo(() => {
|
|
3926
|
-
if (items.length === 0) return { first: 0, last: 0 };
|
|
3927
|
-
return { first: 0, last: items.length - 1 };
|
|
3928
|
-
}, [items]);
|
|
3929
4170
|
useKeypress(async (key) => {
|
|
4171
|
+
const last = items.length - 1;
|
|
3930
4172
|
if (items.length === 0) {
|
|
3931
4173
|
if (isEnterKey(key) || key.name === "q") {
|
|
3932
4174
|
setPromptStatus("done");
|
|
@@ -3950,22 +4192,22 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
3950
4192
|
return;
|
|
3951
4193
|
}
|
|
3952
4194
|
if (key.ctrl && isUpKey(key)) {
|
|
3953
|
-
setActive(
|
|
4195
|
+
setActive(0);
|
|
3954
4196
|
setStatusMsg("");
|
|
3955
4197
|
return;
|
|
3956
4198
|
}
|
|
3957
4199
|
if (key.ctrl && isDownKey(key)) {
|
|
3958
|
-
setActive(
|
|
4200
|
+
setActive(last);
|
|
3959
4201
|
setStatusMsg("");
|
|
3960
4202
|
return;
|
|
3961
4203
|
}
|
|
3962
4204
|
if (isUpKey(key)) {
|
|
3963
|
-
setActive(active <=
|
|
4205
|
+
setActive(active <= 0 ? 0 : active - 1);
|
|
3964
4206
|
setStatusMsg("");
|
|
3965
4207
|
return;
|
|
3966
4208
|
}
|
|
3967
4209
|
if (isDownKey(key)) {
|
|
3968
|
-
setActive(active >=
|
|
4210
|
+
setActive(active >= last ? last : active + 1);
|
|
3969
4211
|
setStatusMsg("");
|
|
3970
4212
|
return;
|
|
3971
4213
|
}
|
|
@@ -3978,7 +4220,7 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
3978
4220
|
const pagePath = await showContribDiff(items[active], baseRef, cwd, forkName);
|
|
3979
4221
|
setStatusMsg(pagePath ? `opened ${items[active].path} in browser` : `no changes for ${items[active].path}`);
|
|
3980
4222
|
} catch (error) {
|
|
3981
|
-
setStatusMsg(`diff failed: ${error
|
|
4223
|
+
setStatusMsg(`diff failed: ${errorMessage(error)}`);
|
|
3982
4224
|
}
|
|
3983
4225
|
return;
|
|
3984
4226
|
}
|
|
@@ -3992,12 +4234,12 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
3992
4234
|
if (promptStatus === "done") {
|
|
3993
4235
|
const checked = items.filter((i) => i.checked);
|
|
3994
4236
|
if (checked.length > 0) {
|
|
3995
|
-
return `${
|
|
4237
|
+
return `${checkMark} ${checked.length} files accepted`;
|
|
3996
4238
|
}
|
|
3997
|
-
return `${
|
|
4239
|
+
return `${checkMark} done`;
|
|
3998
4240
|
}
|
|
3999
4241
|
if (items.length === 0) {
|
|
4000
|
-
return `${
|
|
4242
|
+
return `${checkMark} no contributions: press enter`;
|
|
4001
4243
|
}
|
|
4002
4244
|
const checkedCount = items.filter((i) => i.checked).length;
|
|
4003
4245
|
const page = usePagination({
|
|
@@ -4031,6 +4273,38 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4031
4273
|
const lines = [header, page, statusLine, helpLine].filter(Boolean).join("\n").trimEnd();
|
|
4032
4274
|
return `${lines}\x1B[?25l`;
|
|
4033
4275
|
});
|
|
4276
|
+
async function selectFork(config, validated) {
|
|
4277
|
+
if (config.fork) {
|
|
4278
|
+
const match = validated.find((v) => v.fork.name === config.fork);
|
|
4279
|
+
if (!match?.valid) {
|
|
4280
|
+
throw new Error(`fork '${config.fork}' not found or invalid in config`);
|
|
4281
|
+
}
|
|
4282
|
+
return match;
|
|
4283
|
+
}
|
|
4284
|
+
const validForks = validated.filter((v) => v.valid);
|
|
4285
|
+
if (config.list || config.json || config.diff) {
|
|
4286
|
+
if (validForks.length === 0) {
|
|
4287
|
+
throw new Error("no valid forks configured");
|
|
4288
|
+
}
|
|
4289
|
+
if (validForks.length > 1) {
|
|
4290
|
+
throw new Error("multiple forks configured; pass --fork <name> to choose one");
|
|
4291
|
+
}
|
|
4292
|
+
return validForks[0];
|
|
4293
|
+
}
|
|
4294
|
+
const choices = validated.map((v) => ({
|
|
4295
|
+
value: v.fork.name,
|
|
4296
|
+
name: v.valid ? `${v.fork.name} ${colors_default.dim(`[${v.fork.pullBranch}] ${v.fork.localPath}`)}` : `${v.fork.name} ${colors_default.dim(v.fork.localPath)}`,
|
|
4297
|
+
disabled: v.valid ? false : v.error ?? "invalid"
|
|
4298
|
+
}));
|
|
4299
|
+
const picked = await select2({
|
|
4300
|
+
message: "select fork to pull contributions from:",
|
|
4301
|
+
choices,
|
|
4302
|
+
loop: false
|
|
4303
|
+
});
|
|
4304
|
+
const selected = validated.find((v) => v.fork.name === picked);
|
|
4305
|
+
if (!selected) console.info(colors_default.dim("no fork selected."));
|
|
4306
|
+
return selected;
|
|
4307
|
+
}
|
|
4034
4308
|
async function fetchForkBranch(cellaPath, fetchSource, pullBranch) {
|
|
4035
4309
|
await git(["fetch", fetchSource, pullBranch], cellaPath);
|
|
4036
4310
|
return git(["rev-parse", "FETCH_HEAD"], cellaPath);
|
|
@@ -4058,42 +4332,8 @@ async function runContributions(config) {
|
|
|
4058
4332
|
}
|
|
4059
4333
|
const forkBasePath = await resolveForkBasePath(config.forkPath);
|
|
4060
4334
|
const validated = forks.map((fork2) => validateForkPath(fork2, forkBasePath));
|
|
4061
|
-
const
|
|
4062
|
-
|
|
4063
|
-
if (config.fork) {
|
|
4064
|
-
const match = validated.find((v) => v.fork.name === config.fork);
|
|
4065
|
-
if (!match?.valid) {
|
|
4066
|
-
console.error(colors_default.red(`fork '${config.fork}' not found or invalid in config`));
|
|
4067
|
-
return;
|
|
4068
|
-
}
|
|
4069
|
-
selectedFork = match;
|
|
4070
|
-
} else if (config.list || config.json || config.diff) {
|
|
4071
|
-
if (validForks.length === 0) {
|
|
4072
|
-
console.error(colors_default.red("no valid forks configured"));
|
|
4073
|
-
return;
|
|
4074
|
-
}
|
|
4075
|
-
if (validForks.length > 1) {
|
|
4076
|
-
console.error(colors_default.red("multiple forks configured; pass --fork <name> to choose one"));
|
|
4077
|
-
return;
|
|
4078
|
-
}
|
|
4079
|
-
selectedFork = validForks[0];
|
|
4080
|
-
} else {
|
|
4081
|
-
const choices = validated.map((v) => ({
|
|
4082
|
-
value: v.fork.name,
|
|
4083
|
-
name: v.valid ? `${v.fork.name} ${colors_default.dim(`[${v.fork.pullBranch}] ${v.fork.localPath}`)}` : `${v.fork.name} ${colors_default.dim(v.fork.localPath)}`,
|
|
4084
|
-
disabled: v.valid ? false : v.error ?? "invalid"
|
|
4085
|
-
}));
|
|
4086
|
-
const picked = await select2({
|
|
4087
|
-
message: "select fork to pull contributions from:",
|
|
4088
|
-
choices,
|
|
4089
|
-
loop: false
|
|
4090
|
-
});
|
|
4091
|
-
selectedFork = validated.find((v) => v.fork.name === picked);
|
|
4092
|
-
}
|
|
4093
|
-
if (!selectedFork) {
|
|
4094
|
-
console.info(colors_default.dim("no fork selected."));
|
|
4095
|
-
return;
|
|
4096
|
-
}
|
|
4335
|
+
const selectedFork = await selectFork(config, validated);
|
|
4336
|
+
if (!selectedFork) return;
|
|
4097
4337
|
const { fork, resolvedPath } = selectedFork;
|
|
4098
4338
|
const forkName = fork.name;
|
|
4099
4339
|
createSpinner("pulling fork contributions...");
|
|
@@ -4116,40 +4356,28 @@ async function runContributions(config) {
|
|
|
4116
4356
|
if (appliedFiles.length > 0) {
|
|
4117
4357
|
const stat = await getDiffStat(config.forkPath, baseRef, branch);
|
|
4118
4358
|
const metaByPath = new Map(detection.files.map((f) => [f.path, f]));
|
|
4119
|
-
|
|
4120
|
-
const fileMeta = metaByPath.get(
|
|
4121
|
-
|
|
4122
|
-
path
|
|
4123
|
-
ref: branch,
|
|
4124
|
-
deleted: false,
|
|
4125
|
-
created: fileMeta?.kind === "created",
|
|
4126
|
-
status: fileMeta?.status,
|
|
4127
|
-
changedAt: fileMeta?.changedAt,
|
|
4128
|
-
changedTs: fileMeta?.changedTs,
|
|
4129
|
-
additions: stat.get(path4)?.additions ?? null,
|
|
4130
|
-
deletions: stat.get(path4)?.deletions ?? null,
|
|
4131
|
-
checked: false
|
|
4132
|
-
});
|
|
4133
|
-
}
|
|
4134
|
-
for (const path4 of detection.deleted) {
|
|
4135
|
-
const fileMeta = metaByPath.get(path4);
|
|
4136
|
-
allItems.push({
|
|
4137
|
-
path: path4,
|
|
4359
|
+
const toItem = (path, deleted) => {
|
|
4360
|
+
const fileMeta = metaByPath.get(path);
|
|
4361
|
+
return {
|
|
4362
|
+
path,
|
|
4138
4363
|
ref: branch,
|
|
4139
|
-
deleted
|
|
4364
|
+
deleted,
|
|
4365
|
+
...deleted ? {} : { created: fileMeta?.kind === "created" },
|
|
4140
4366
|
status: fileMeta?.status,
|
|
4141
4367
|
changedAt: fileMeta?.changedAt,
|
|
4142
4368
|
changedTs: fileMeta?.changedTs,
|
|
4143
|
-
additions: stat.get(
|
|
4144
|
-
deletions: stat.get(
|
|
4369
|
+
additions: stat.get(path)?.additions ?? null,
|
|
4370
|
+
deletions: stat.get(path)?.deletions ?? null,
|
|
4145
4371
|
checked: false
|
|
4146
|
-
}
|
|
4147
|
-
}
|
|
4372
|
+
};
|
|
4373
|
+
};
|
|
4374
|
+
for (const path of [...detection.modified, ...detection.created]) allItems.push(toItem(path, false));
|
|
4375
|
+
for (const path of detection.deleted) allItems.push(toItem(path, true));
|
|
4148
4376
|
}
|
|
4149
4377
|
}
|
|
4150
4378
|
} catch (error) {
|
|
4151
4379
|
spinnerFail(`failed to pull ${forkName}`);
|
|
4152
|
-
console.info(colors_default.red(` \u2717 ${error
|
|
4380
|
+
console.info(colors_default.red(` \u2717 ${errorMessage(error)}`));
|
|
4153
4381
|
return;
|
|
4154
4382
|
}
|
|
4155
4383
|
if (allItems.length === 0) {
|
|
@@ -4160,15 +4388,13 @@ async function runContributions(config) {
|
|
|
4160
4388
|
spinnerSuccess(`${allItems.length} files from ${forkName}`);
|
|
4161
4389
|
if (!config.list && !config.diff && forkBanner) {
|
|
4162
4390
|
console.info(
|
|
4163
|
-
` ${colors_default.dim(`${forkName}: comparing committed '${forkBanner.pullBranch}' @ ${forkBanner.sha} (${forkBanner.date})
|
|
4391
|
+
` ${colors_default.dim(`${forkName}: comparing committed '${forkBanner.pullBranch}' @ ${forkBanner.sha} (${forkBanner.date}); uncommitted fork changes are not included`)}`
|
|
4164
4392
|
);
|
|
4165
4393
|
}
|
|
4166
4394
|
if (config.diff) {
|
|
4167
4395
|
const matches = allItems.filter((i) => i.path === config.diff);
|
|
4168
4396
|
if (matches.length === 0) {
|
|
4169
|
-
|
|
4170
|
-
process.exitCode = 1;
|
|
4171
|
-
return;
|
|
4397
|
+
throw new Error(`no contribution found for path: ${config.diff}`);
|
|
4172
4398
|
}
|
|
4173
4399
|
for (const item of matches) {
|
|
4174
4400
|
const diff = gitDiffFile(config.forkPath, `${baseRef}..${item.ref}`, item.path, { dstPrefix: forkName });
|
|
@@ -4195,7 +4421,7 @@ async function runContributions(config) {
|
|
|
4195
4421
|
const status = item.status ?? "behind";
|
|
4196
4422
|
const kind = item.deleted ? "deleted" : "modified";
|
|
4197
4423
|
const changedAt = item.changedAt ?? "-";
|
|
4198
|
-
|
|
4424
|
+
writeStdout(`${forkName} ${status} ${kind} ${changedAt} ${item.path}`);
|
|
4199
4425
|
}
|
|
4200
4426
|
return;
|
|
4201
4427
|
}
|
|
@@ -4214,7 +4440,7 @@ async function runContributions(config) {
|
|
|
4214
4440
|
return;
|
|
4215
4441
|
}
|
|
4216
4442
|
createSpinner(`applying ${selected.length} files...`);
|
|
4217
|
-
const applyUnstaged =
|
|
4443
|
+
const applyUnstaged = isUpstreamRepo(config.forkPath);
|
|
4218
4444
|
let applied = 0;
|
|
4219
4445
|
const errors = [];
|
|
4220
4446
|
for (const item of selected) {
|
|
@@ -4238,7 +4464,7 @@ async function runContributions(config) {
|
|
|
4238
4464
|
}
|
|
4239
4465
|
applied++;
|
|
4240
4466
|
} catch (error) {
|
|
4241
|
-
errors.push(`${item.path}: ${error
|
|
4467
|
+
errors.push(`${item.path}: ${errorMessage(error)}`);
|
|
4242
4468
|
}
|
|
4243
4469
|
}
|
|
4244
4470
|
if (errors.length > 0) {
|
|
@@ -4252,28 +4478,28 @@ async function runContributions(config) {
|
|
|
4252
4478
|
console.info();
|
|
4253
4479
|
console.info(
|
|
4254
4480
|
colors_default.dim(
|
|
4255
|
-
applyUnstaged ? " files are unstaged
|
|
4481
|
+
applyUnstaged ? " files are unstaged: review, stage what you want, and commit when ready" : " files are staged: review and commit when ready"
|
|
4256
4482
|
)
|
|
4257
4483
|
);
|
|
4258
4484
|
console.info();
|
|
4259
4485
|
}
|
|
4260
4486
|
|
|
4261
4487
|
// src/services/forks.ts
|
|
4262
|
-
import { resolve as
|
|
4488
|
+
import { resolve as resolve7 } from "path";
|
|
4263
4489
|
import process8 from "process";
|
|
4264
4490
|
import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
|
|
4265
4491
|
|
|
4266
4492
|
// src/services/sync.ts
|
|
4267
|
-
import { spawnSync as
|
|
4493
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4268
4494
|
import { select as select3 } from "@inquirer/prompts";
|
|
4269
4495
|
|
|
4270
4496
|
// src/utils/gh.ts
|
|
4271
|
-
import { spawnSync as
|
|
4497
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4272
4498
|
function ghAvailable() {
|
|
4273
|
-
return
|
|
4499
|
+
return spawnSync3("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
4274
4500
|
}
|
|
4275
4501
|
function runGh(cwd, args) {
|
|
4276
|
-
const result =
|
|
4502
|
+
const result = spawnSync3("gh", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4277
4503
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
4278
4504
|
return { ok: result.status === 0, output };
|
|
4279
4505
|
}
|
|
@@ -4313,12 +4539,12 @@ function closePr(cwd, ref) {
|
|
|
4313
4539
|
}
|
|
4314
4540
|
|
|
4315
4541
|
// src/services/packages.ts
|
|
4316
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4317
|
-
import { join as
|
|
4542
|
+
import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
4543
|
+
import { join as join16 } from "path";
|
|
4318
4544
|
|
|
4319
4545
|
// src/services/package-usage.ts
|
|
4320
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4321
|
-
import { join as
|
|
4546
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4, statSync } from "fs";
|
|
4547
|
+
import { join as join15 } from "path";
|
|
4322
4548
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4323
4549
|
function escapeRegExp(value) {
|
|
4324
4550
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4326,41 +4552,41 @@ function escapeRegExp(value) {
|
|
|
4326
4552
|
function readSearchable(filePath) {
|
|
4327
4553
|
try {
|
|
4328
4554
|
if (statSync(filePath).size > MAX_FILE_BYTES) return "";
|
|
4329
|
-
const text =
|
|
4555
|
+
const text = readFileSync4(filePath, "utf8");
|
|
4330
4556
|
return text.includes("\0") ? "" : text;
|
|
4331
4557
|
} catch {
|
|
4332
4558
|
return "";
|
|
4333
4559
|
}
|
|
4334
4560
|
}
|
|
4335
|
-
function fileSource(forkPath,
|
|
4561
|
+
function fileSource(forkPath, path) {
|
|
4336
4562
|
let text;
|
|
4337
|
-
return { path
|
|
4563
|
+
return { path, label: path, text: () => text ??= readSearchable(join15(forkPath, path)) };
|
|
4338
4564
|
}
|
|
4339
|
-
async function readScriptsAtRef(forkPath, ref,
|
|
4565
|
+
async function readScriptsAtRef(forkPath, ref, path) {
|
|
4340
4566
|
try {
|
|
4341
|
-
return JSON.parse(await git(["show", `${ref}:${
|
|
4567
|
+
return JSON.parse(await git(["show", `${ref}:${path}`], forkPath)).scripts ?? {};
|
|
4342
4568
|
} catch {
|
|
4343
4569
|
return {};
|
|
4344
4570
|
}
|
|
4345
4571
|
}
|
|
4346
|
-
async function scriptSources(forkPath, upstreamRef, baseRef,
|
|
4572
|
+
async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
4347
4573
|
let forkScripts;
|
|
4348
4574
|
try {
|
|
4349
|
-
forkScripts = JSON.parse(
|
|
4575
|
+
forkScripts = JSON.parse(readFileSync4(join15(forkPath, path), "utf8")).scripts ?? {};
|
|
4350
4576
|
} catch {
|
|
4351
4577
|
return [];
|
|
4352
4578
|
}
|
|
4353
|
-
const baseScripts = await readScriptsAtRef(forkPath, baseRef,
|
|
4354
|
-
const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef,
|
|
4355
|
-
return Object.entries(forkScripts).filter(([name, value]) => value !== baseScripts[name] && value !== upstreamScripts[name]).map(([name, value]) => ({ path
|
|
4579
|
+
const baseScripts = await readScriptsAtRef(forkPath, baseRef, path);
|
|
4580
|
+
const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef, path);
|
|
4581
|
+
return Object.entries(forkScripts).filter(([name, value]) => value !== baseScripts[name] && value !== upstreamScripts[name]).map(([name, value]) => ({ path, label: `${path} (scripts.${name})`, text: () => value }));
|
|
4356
4582
|
}
|
|
4357
4583
|
function binNames(forkPath, location, name) {
|
|
4358
4584
|
const bareName = name.split("/").pop();
|
|
4359
|
-
for (const dir of [
|
|
4360
|
-
const manifest =
|
|
4585
|
+
for (const dir of [join15(forkPath, location), forkPath]) {
|
|
4586
|
+
const manifest = join15(dir, "node_modules", name, "package.json");
|
|
4361
4587
|
if (!existsSync10(manifest)) continue;
|
|
4362
4588
|
try {
|
|
4363
|
-
const { bin } = JSON.parse(
|
|
4589
|
+
const { bin } = JSON.parse(readFileSync4(manifest, "utf8"));
|
|
4364
4590
|
if (typeof bin === "string") return [bareName];
|
|
4365
4591
|
return bin && typeof bin === "object" ? Object.keys(bin) : [];
|
|
4366
4592
|
} catch {
|
|
@@ -4385,9 +4611,9 @@ function scriptPattern(name) {
|
|
|
4385
4611
|
async function loadForkUsage(forkPath, upstreamRef, baseRef) {
|
|
4386
4612
|
const diff = await git(["diff", "--name-only", "-z", "--diff-filter=d", upstreamRef], forkPath);
|
|
4387
4613
|
const sources = [];
|
|
4388
|
-
for (const
|
|
4389
|
-
if (isPackageJson(
|
|
4390
|
-
else if (!isManagedFile(
|
|
4614
|
+
for (const path of diff.split("\0").filter(Boolean)) {
|
|
4615
|
+
if (isPackageJson(path)) sources.push(...await scriptSources(forkPath, upstreamRef, baseRef, path));
|
|
4616
|
+
else if (!isManagedFile(path)) sources.push(fileSource(forkPath, path));
|
|
4391
4617
|
}
|
|
4392
4618
|
const findIn = (scoped, patterns) => scoped.find((source) => patterns.some((pattern) => pattern.test(source.text())))?.label;
|
|
4393
4619
|
return {
|
|
@@ -4517,17 +4743,37 @@ function safeMergePnpm(forkPnpm, upstreamPnpm, basePnpm) {
|
|
|
4517
4743
|
}
|
|
4518
4744
|
return { merged, changed };
|
|
4519
4745
|
}
|
|
4746
|
+
function mergeModuleType(forkType, upstreamType, baseType) {
|
|
4747
|
+
const asString = (value) => typeof value === "string" ? value : void 0;
|
|
4748
|
+
const fork = asString(forkType);
|
|
4749
|
+
const upstream = asString(upstreamType);
|
|
4750
|
+
if (fork === upstream) return null;
|
|
4751
|
+
if (fork === void 0) return { value: upstream, change: "added" };
|
|
4752
|
+
if (fork !== asString(baseType)) return null;
|
|
4753
|
+
return { value: upstream, change: upstream === void 0 ? "removed" : "updated" };
|
|
4754
|
+
}
|
|
4755
|
+
function setTopLevelKey(pkg, key, value) {
|
|
4756
|
+
if (key in pkg) {
|
|
4757
|
+
pkg[key] = value;
|
|
4758
|
+
return;
|
|
4759
|
+
}
|
|
4760
|
+
const entries = Object.entries(pkg);
|
|
4761
|
+
const after = Math.max(...["name", "version", "private"].map((name) => entries.findIndex(([k]) => k === name)));
|
|
4762
|
+
entries.splice(after + 1, 0, [key, value]);
|
|
4763
|
+
for (const name of Object.keys(pkg)) delete pkg[name];
|
|
4764
|
+
Object.assign(pkg, Object.fromEntries(entries));
|
|
4765
|
+
}
|
|
4520
4766
|
function readPackageJson(filePath) {
|
|
4521
4767
|
if (!existsSync11(filePath)) return null;
|
|
4522
4768
|
try {
|
|
4523
|
-
return JSON.parse(
|
|
4769
|
+
return JSON.parse(readFileSync5(filePath, "utf8"));
|
|
4524
4770
|
} catch {
|
|
4525
4771
|
return null;
|
|
4526
4772
|
}
|
|
4527
4773
|
}
|
|
4528
4774
|
function writePackageJson(filePath, data) {
|
|
4529
4775
|
writeFileSync4(filePath, `${JSON.stringify(data, null, 2)}
|
|
4530
|
-
`, "
|
|
4776
|
+
`, "utf8");
|
|
4531
4777
|
}
|
|
4532
4778
|
function packageJsonPath(relativePath) {
|
|
4533
4779
|
return relativePath ? `${relativePath}/package.json` : "package.json";
|
|
@@ -4556,20 +4802,27 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4556
4802
|
const changes = [];
|
|
4557
4803
|
const kept = [];
|
|
4558
4804
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4559
|
-
const pkgPath =
|
|
4805
|
+
const pkgPath = join16(forkPath, pkgRelPath);
|
|
4560
4806
|
const forkPkg = readPackageJson(pkgPath);
|
|
4561
4807
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4562
4808
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4563
4809
|
if (!upstream) return { updated: false, changes, kept };
|
|
4564
4810
|
if (!forkPkg) {
|
|
4565
|
-
const workspaceArrived = relativePath !== "" && existsSync11(
|
|
4811
|
+
const workspaceArrived = relativePath !== "" && existsSync11(join16(forkPath, relativePath));
|
|
4566
4812
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4567
4813
|
writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4568
|
-
`, "
|
|
4814
|
+
`, "utf8");
|
|
4569
4815
|
return { updated: true, changes: ["copied from upstream (new workspace)"], kept };
|
|
4570
4816
|
}
|
|
4571
4817
|
const upstreamPkg = upstream.data;
|
|
4572
4818
|
let updated = false;
|
|
4819
|
+
const type = mergeModuleType(forkPkg.type, upstreamPkg.type, basePkg?.type);
|
|
4820
|
+
if (type) {
|
|
4821
|
+
if (type.value === void 0) delete forkPkg.type;
|
|
4822
|
+
else setTopLevelKey(forkPkg, "type", type.value);
|
|
4823
|
+
updated = true;
|
|
4824
|
+
changes.push(`type: ${type.change}`);
|
|
4825
|
+
}
|
|
4573
4826
|
for (const key of keysToSync) {
|
|
4574
4827
|
if (key === "pnpm") {
|
|
4575
4828
|
const result2 = safeMergePnpm(forkPkg.pnpm, upstreamPkg.pnpm, basePkg?.pnpm);
|
|
@@ -4630,7 +4883,7 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4630
4883
|
}
|
|
4631
4884
|
async function runPackages(config, options = {}) {
|
|
4632
4885
|
createSpinner("syncing package.json files...");
|
|
4633
|
-
const keysToSync = config.settings.packageJsonSync ||
|
|
4886
|
+
const keysToSync = config.settings.packageJsonSync || DEFAULT_PACKAGE_JSON_SYNC;
|
|
4634
4887
|
const conflictedSet = new Set(options.conflictedFiles ?? []);
|
|
4635
4888
|
const baseRef = await resolveMergeBase(config.forkPath, config.upstreamRef);
|
|
4636
4889
|
const usage = baseRef ? await loadForkUsage(config.forkPath, config.upstreamRef, baseRef) : null;
|
|
@@ -4652,36 +4905,42 @@ async function runPackages(config, options = {}) {
|
|
|
4652
4905
|
"package sync complete",
|
|
4653
4906
|
`${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
|
|
4654
4907
|
);
|
|
4655
|
-
for (const { path
|
|
4908
|
+
for (const { path, changes } of reports) {
|
|
4656
4909
|
if (changes.length === 0) continue;
|
|
4657
|
-
console.info(` ${
|
|
4910
|
+
console.info(` ${path}`);
|
|
4658
4911
|
for (const change of changes) console.info(` ${colors_default.dim(change)}`);
|
|
4659
4912
|
}
|
|
4660
|
-
const kept = reports.flatMap(({ path
|
|
4913
|
+
const kept = reports.flatMap(({ path, kept: kept2 }) => kept2.map((entry) => `${path} ${entry}`));
|
|
4661
4914
|
if (kept.length > 0) {
|
|
4662
4915
|
console.info();
|
|
4663
|
-
console.
|
|
4664
|
-
`${warningMark} kept ${kept.length} ${kept.length === 1 ? "entry" : "entries"} upstream dropped, the fork still uses ${kept.length === 1 ? "it" : "them"}:`
|
|
4916
|
+
console.info(
|
|
4917
|
+
`${warningMark} ${colors_default.yellow(`kept ${kept.length} ${kept.length === 1 ? "entry" : "entries"} upstream dropped, the fork still uses ${kept.length === 1 ? "it" : "them"}:`)}`
|
|
4665
4918
|
);
|
|
4666
|
-
for (const entry of kept) console.
|
|
4919
|
+
for (const entry of kept) console.info(` ${colors_default.dim("\u2192")} ${entry}`);
|
|
4667
4920
|
}
|
|
4668
4921
|
if (skipped.length > 0) {
|
|
4669
4922
|
console.info();
|
|
4670
|
-
console.
|
|
4671
|
-
`${warningMark} skipped ${skipped.length} conflicted package.json file(s)
|
|
4923
|
+
console.info(
|
|
4924
|
+
`${warningMark} ${colors_default.yellow(`skipped ${skipped.length} conflicted package.json file(s): resolve the conflicts, commit the merge, then rerun \`pnpm cella sync\`:`)}`
|
|
4672
4925
|
);
|
|
4673
|
-
for (const
|
|
4674
|
-
console.
|
|
4926
|
+
for (const path of skipped) {
|
|
4927
|
+
console.info(` ${colors_default.dim("\u2192")} ${path}`);
|
|
4675
4928
|
}
|
|
4676
4929
|
}
|
|
4677
4930
|
}
|
|
4678
4931
|
|
|
4679
4932
|
// src/services/sync.ts
|
|
4680
|
-
async function
|
|
4681
|
-
|
|
4933
|
+
async function resolveCycleStart(forkPath, base, currentBranch) {
|
|
4934
|
+
if (!await branchExists(forkPath, base)) {
|
|
4935
|
+
await fetchRemote(forkPath, "origin").catch(() => {
|
|
4936
|
+
});
|
|
4937
|
+
console.info(colors_default.dim(`'${base}' has no local branch: cutting from 'origin/${base}'.`));
|
|
4938
|
+
return `origin/${base}`;
|
|
4939
|
+
}
|
|
4940
|
+
const { upstream, ahead, behind } = await getUpstreamStatus(forkPath, base);
|
|
4682
4941
|
if (!upstream) {
|
|
4683
|
-
console.info(colors_default.dim(`'${base}' has no upstream
|
|
4684
|
-
return;
|
|
4942
|
+
console.info(colors_default.dim(`'${base}' has no upstream: skipping the up-to-date check.`));
|
|
4943
|
+
return base;
|
|
4685
4944
|
}
|
|
4686
4945
|
if (ahead > 0 && behind > 0) {
|
|
4687
4946
|
throw new Error(
|
|
@@ -4690,53 +4949,74 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
|
|
|
4690
4949
|
);
|
|
4691
4950
|
}
|
|
4692
4951
|
if (behind > 0) {
|
|
4952
|
+
const worktree = currentBranch === base ? null : await getBranchWorktree(forkPath, base);
|
|
4953
|
+
if (worktree) {
|
|
4954
|
+
console.info(
|
|
4955
|
+
colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree}; leaving it as it is.`)
|
|
4956
|
+
);
|
|
4957
|
+
return upstream;
|
|
4958
|
+
}
|
|
4693
4959
|
console.info(colors_default.dim(`fast-forwarding '${base}' to '${upstream}' (${behind} behind)...`));
|
|
4694
|
-
await pullFastForward(forkPath);
|
|
4695
|
-
|
|
4960
|
+
if (currentBranch === base) await pullFastForward(forkPath);
|
|
4961
|
+
else await fastForwardBranch(forkPath, base, upstream);
|
|
4962
|
+
return base;
|
|
4696
4963
|
}
|
|
4697
4964
|
if (ahead > 0) {
|
|
4698
|
-
console.info(colors_default.dim(`'${base}' is ${ahead} commit(s) ahead of '${upstream}' (unpushed)
|
|
4965
|
+
console.info(colors_default.dim(`'${base}' is ${ahead} commit(s) ahead of '${upstream}' (unpushed); continuing.`));
|
|
4699
4966
|
}
|
|
4967
|
+
return base;
|
|
4700
4968
|
}
|
|
4701
4969
|
async function setupTemporarySyncBranch(config) {
|
|
4702
4970
|
const { forkPath, settings } = config;
|
|
4703
4971
|
const base = resolveReleaseBase(settings);
|
|
4704
4972
|
const startBranch = await getCurrentBranch(forkPath);
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
await
|
|
4973
|
+
const startCommit = (await getCommitInfo(forkPath, "HEAD")).hash;
|
|
4974
|
+
console.info(colors_default.dim(`updating '${base}'...`));
|
|
4975
|
+
const startPoint = await resolveCycleStart(forkPath, base, startBranch);
|
|
4708
4976
|
const temporaryBranch = buildTemporarySyncBranch();
|
|
4709
|
-
console.info(colors_default.dim(`creating temporary sync branch '${temporaryBranch}' from '${
|
|
4977
|
+
console.info(colors_default.dim(`creating temporary sync branch '${temporaryBranch}' from '${startPoint}'...`));
|
|
4710
4978
|
console.info();
|
|
4711
|
-
await createBranchFrom(forkPath, temporaryBranch,
|
|
4712
|
-
return { temporaryBranch, base, startBranch };
|
|
4979
|
+
await createBranchFrom(forkPath, temporaryBranch, startPoint);
|
|
4980
|
+
return { temporaryBranch, base, startBranch, startCommit };
|
|
4981
|
+
}
|
|
4982
|
+
async function resolveCutBase(forkPath, base) {
|
|
4983
|
+
if (!await branchExists(forkPath, base)) return `origin/${base}`;
|
|
4984
|
+
const upstream = await getBranchUpstream(forkPath, base);
|
|
4985
|
+
return upstream && await isAncestor(forkPath, base, upstream) ? upstream : base;
|
|
4986
|
+
}
|
|
4987
|
+
async function returnToBase(forkPath, base) {
|
|
4988
|
+
const worktree = await getBranchWorktree(forkPath, base);
|
|
4989
|
+
if (worktree) {
|
|
4990
|
+
console.info(colors_default.dim(`'${base}' is checked out at ${worktree}; detaching at '${base}' here instead...`));
|
|
4991
|
+
await switchDetached(forkPath, base);
|
|
4992
|
+
return `detached at '${base}'`;
|
|
4993
|
+
}
|
|
4994
|
+
console.info(colors_default.dim(`switching back to '${base}'...`));
|
|
4995
|
+
await switchBranch(forkPath, base);
|
|
4996
|
+
await pullFastForward(forkPath).catch(() => {
|
|
4997
|
+
});
|
|
4998
|
+
return `back on '${base}'`;
|
|
4713
4999
|
}
|
|
4714
5000
|
async function runSync(config, options) {
|
|
4715
5001
|
createSpinner("starting sync...");
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
5002
|
+
let result;
|
|
5003
|
+
try {
|
|
5004
|
+
result = await runEngineWithSpinner(config, true);
|
|
5005
|
+
} catch (error) {
|
|
5006
|
+
spinnerSuccess();
|
|
5007
|
+
if (error instanceof UpstreamConfigChangedError) {
|
|
5008
|
+
printUpstreamOverrideChanges({ upstreamOverrides: error.report });
|
|
5009
|
+
console.info();
|
|
4724
5010
|
}
|
|
4725
|
-
|
|
5011
|
+
throw error;
|
|
5012
|
+
}
|
|
4726
5013
|
if (result.success) {
|
|
4727
5014
|
spinnerSuccess();
|
|
4728
5015
|
} else {
|
|
4729
5016
|
spinnerFail("sync completed with conflicts");
|
|
4730
5017
|
}
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
printUpstreamOverrideChanges(result);
|
|
4734
|
-
printMaskingPinWarning(result.files);
|
|
4735
|
-
if (config.logFile) {
|
|
4736
|
-
const logPath = writeLogFile(config.forkPath, result.files);
|
|
4737
|
-
console.info();
|
|
4738
|
-
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
4739
|
-
}
|
|
5018
|
+
printEngineReports(result, "merge summary");
|
|
5019
|
+
printLogFileReport(config, result.files);
|
|
4740
5020
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
|
|
4741
5021
|
printSyncComplete(result, { stagedBranch });
|
|
4742
5022
|
printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
|
|
@@ -4744,7 +5024,6 @@ async function runSync(config, options) {
|
|
|
4744
5024
|
return result;
|
|
4745
5025
|
}
|
|
4746
5026
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
4747
|
-
var PR_BODY_COMMIT_MAX = 50;
|
|
4748
5027
|
async function buildSyncCommitMessage(forkPath, upstreamRef) {
|
|
4749
5028
|
const shortSha = await getShortSha(forkPath, upstreamRef).catch(() => "");
|
|
4750
5029
|
if (!shortSha) return SYNC_PR_TITLE;
|
|
@@ -4796,9 +5075,8 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
4796
5075
|
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
4797
5076
|
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
4798
5077
|
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, {
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
limit: PR_BODY_COMMIT_MAX
|
|
5078
|
+
skip: totalCount > COMMIT_LIST_MAX ? totalCount - COMMIT_LIST_MAX : 0,
|
|
5079
|
+
limit: COMMIT_LIST_MAX
|
|
4802
5080
|
}) : [];
|
|
4803
5081
|
const notes = await readPrBodyNotes(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
4804
5082
|
return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount, notes });
|
|
@@ -4822,11 +5100,8 @@ function printShipSteps(temporaryBranch, base, title) {
|
|
|
4822
5100
|
console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
|
|
4823
5101
|
printPrCreateStep(temporaryBranch, base, title);
|
|
4824
5102
|
}
|
|
4825
|
-
function printFinishSteps() {
|
|
4826
|
-
console.info(colors_default.dim(" pnpm cella sync"));
|
|
4827
|
-
}
|
|
4828
5103
|
function hasStagedSyncChanges(result) {
|
|
4829
|
-
return result.files.some((file) =>
|
|
5104
|
+
return result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
4830
5105
|
}
|
|
4831
5106
|
function extractFirstUrl(output) {
|
|
4832
5107
|
return output.match(/https?:\/\/\S+/)?.[0];
|
|
@@ -4837,7 +5112,7 @@ async function flattenSyncBranch(forkPath, branch, base) {
|
|
|
4837
5112
|
const message = await buildSyncCommitMessage(forkPath, `${mergeCommits[0]}^2`);
|
|
4838
5113
|
console.info(
|
|
4839
5114
|
colors_default.yellow(
|
|
4840
|
-
`'${branch}' contains ${mergeCommits.length} merge commit(s)
|
|
5115
|
+
`'${branch}' contains ${mergeCommits.length} merge commit(s): the PR would list the entire upstream history.`
|
|
4841
5116
|
)
|
|
4842
5117
|
);
|
|
4843
5118
|
console.info(colors_default.dim(`flattening '${branch}' to a single commit (same content)...`));
|
|
@@ -4850,7 +5125,8 @@ function indentLines(text) {
|
|
|
4850
5125
|
async function shipSyncBranch(config, branch) {
|
|
4851
5126
|
const { forkPath, settings } = config;
|
|
4852
5127
|
const base = resolveReleaseBase(settings);
|
|
4853
|
-
const
|
|
5128
|
+
const cutBase = await resolveCutBase(forkPath, base);
|
|
5129
|
+
const flattened = await flattenSyncBranch(forkPath, branch, cutBase);
|
|
4854
5130
|
let prUrl;
|
|
4855
5131
|
let prOpened = false;
|
|
4856
5132
|
const headSubject = (await getCommitInfo(forkPath, "HEAD").catch(() => null))?.message;
|
|
@@ -4859,18 +5135,17 @@ async function shipSyncBranch(config, branch) {
|
|
|
4859
5135
|
try {
|
|
4860
5136
|
await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
|
|
4861
5137
|
} catch (error) {
|
|
4862
|
-
|
|
4863
|
-
console.info(colors_default.yellow(`push failed (${detail.split("\n")[0]}). finish manually:`));
|
|
5138
|
+
console.info(colors_default.yellow(`push failed (${errorMessage(error).split("\n")[0]}). finish manually:`));
|
|
4864
5139
|
printShipSteps(branch, base, prTitle);
|
|
4865
5140
|
return;
|
|
4866
5141
|
}
|
|
4867
5142
|
if (ghAvailable()) {
|
|
4868
5143
|
console.info(colors_default.dim("opening a pull request..."));
|
|
4869
|
-
const prBody = await buildSyncPrBodyForBranch(forkPath,
|
|
5144
|
+
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
4870
5145
|
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
4871
|
-
const pr =
|
|
5146
|
+
const pr = spawnSync4("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
4872
5147
|
cwd: forkPath,
|
|
4873
|
-
encoding: "
|
|
5148
|
+
encoding: "utf8",
|
|
4874
5149
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4875
5150
|
});
|
|
4876
5151
|
prUrl = extractFirstUrl(`${pr.stdout ?? ""}
|
|
@@ -4882,42 +5157,60 @@ ${pr.stderr ?? ""}`);
|
|
|
4882
5157
|
printPrCreateStep(branch, base, prTitle);
|
|
4883
5158
|
}
|
|
4884
5159
|
} else {
|
|
4885
|
-
console.info(colors_default.yellow("`gh` not found
|
|
5160
|
+
console.info(colors_default.yellow("`gh` not found, open the PR manually:"));
|
|
4886
5161
|
printPrCreateStep(branch, base, prTitle);
|
|
4887
5162
|
}
|
|
4888
|
-
|
|
4889
|
-
await switchBranch(forkPath, base);
|
|
4890
|
-
await pullFastForward(forkPath).catch(() => {
|
|
4891
|
-
});
|
|
5163
|
+
const position = await returnToBase(forkPath, base);
|
|
4892
5164
|
console.info();
|
|
4893
5165
|
if (prUrl) {
|
|
4894
|
-
console.info(`${
|
|
4895
|
-
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed,
|
|
5166
|
+
console.info(`${checkMark} sync pull request ${prOpened ? "opened" : "ready"}`);
|
|
5167
|
+
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, ${position}`));
|
|
4896
5168
|
} else {
|
|
4897
|
-
console.info(`${
|
|
4898
|
-
console.info(colors_default.dim(` '${branch}' is on origin,
|
|
5169
|
+
console.info(`${checkMark} sync branch pushed`);
|
|
5170
|
+
console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
|
|
4899
5171
|
}
|
|
4900
5172
|
await printMigrationNotesLine(config);
|
|
4901
5173
|
}
|
|
4902
5174
|
function finalizeWorkspace(forkPath) {
|
|
4903
5175
|
for (const args of [["install"], ["check"]]) {
|
|
4904
5176
|
console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
|
|
4905
|
-
const result =
|
|
5177
|
+
const result = spawnSync4("pnpm", args, { cwd: forkPath, stdio: "inherit" });
|
|
4906
5178
|
if (result.status !== 0) return false;
|
|
4907
5179
|
}
|
|
4908
5180
|
return true;
|
|
4909
5181
|
}
|
|
5182
|
+
async function discardTemporarySyncBranch(forkPath, branch) {
|
|
5183
|
+
if (branch.startBranch === "HEAD") await switchDetached(forkPath, branch.startCommit);
|
|
5184
|
+
else await switchBranch(forkPath, branch.startBranch);
|
|
5185
|
+
await deleteBranch(forkPath, branch.temporaryBranch);
|
|
5186
|
+
}
|
|
5187
|
+
async function discardUnusedSyncBranch(forkPath, branch) {
|
|
5188
|
+
const unused = await getCurrentBranch(forkPath) === branch.temporaryBranch && !await mergeInProgress(forkPath) && await isClean(forkPath);
|
|
5189
|
+
if (!unused) return false;
|
|
5190
|
+
await discardTemporarySyncBranch(forkPath, branch);
|
|
5191
|
+
const start = branch.startBranch === "HEAD" ? `at ${branch.startCommit.slice(0, 7)}` : `on '${branch.startBranch}'`;
|
|
5192
|
+
console.info(colors_default.dim(`removed the unused '${branch.temporaryBranch}', back ${start}.`));
|
|
5193
|
+
return true;
|
|
5194
|
+
}
|
|
4910
5195
|
async function runSyncCycle(config) {
|
|
4911
5196
|
const { forkPath } = config;
|
|
4912
5197
|
const branch = await setupTemporarySyncBranch(config);
|
|
4913
|
-
|
|
5198
|
+
let result;
|
|
5199
|
+
try {
|
|
5200
|
+
result = await runSync(config, { stagedBranch: branch.temporaryBranch });
|
|
5201
|
+
} catch (error) {
|
|
5202
|
+
const discarded = await discardUnusedSyncBranch(forkPath, branch).catch(() => false);
|
|
5203
|
+
if (discarded && error instanceof BehindSyncPointError && !config.ref) {
|
|
5204
|
+
return { status: "noop", behind: error.message };
|
|
5205
|
+
}
|
|
5206
|
+
throw error;
|
|
5207
|
+
}
|
|
4914
5208
|
if (config.settings.syncWithPackages !== false) {
|
|
4915
5209
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
4916
5210
|
}
|
|
4917
5211
|
if (result.conflicts.length > 0) return { status: "conflicts", branch };
|
|
4918
|
-
if (!mergeInProgress(forkPath)) {
|
|
4919
|
-
await
|
|
4920
|
-
await deleteBranch(forkPath, branch.temporaryBranch);
|
|
5212
|
+
if (!await mergeInProgress(forkPath)) {
|
|
5213
|
+
await discardTemporarySyncBranch(forkPath, branch);
|
|
4921
5214
|
return { status: "noop" };
|
|
4922
5215
|
}
|
|
4923
5216
|
return { status: "staged", branch };
|
|
@@ -4962,21 +5255,18 @@ async function mergeOpenSyncPrs(config, open) {
|
|
|
4962
5255
|
const merged = mergePrSquash(forkPath, newest.number, { deleteBranch: true });
|
|
4963
5256
|
if (!merged.ok) {
|
|
4964
5257
|
console.info();
|
|
4965
|
-
console.info(colors_default.yellow(`could not merge #${newest.number}
|
|
5258
|
+
console.info(colors_default.yellow(`could not merge #${newest.number}: resolve it first, then re-run \`pnpm cella sync\`:`));
|
|
4966
5259
|
if (merged.output) console.info(colors_default.dim(indentLines(merged.output)));
|
|
4967
5260
|
console.info(colors_default.dim(` ${newest.url}`));
|
|
4968
5261
|
return "cancel";
|
|
4969
5262
|
}
|
|
4970
|
-
console.info(`${
|
|
5263
|
+
console.info(`${checkMark} merged #${newest.number}`);
|
|
4971
5264
|
await deleteBranch(forkPath, newest.headRefName);
|
|
4972
5265
|
for (const pr of superseded) {
|
|
4973
|
-
console.info(colors_default.dim(`closing #${pr.number}
|
|
5266
|
+
console.info(colors_default.dim(`closing #${pr.number}: its changes are included in #${newest.number}...`));
|
|
4974
5267
|
closePr(forkPath, pr.number);
|
|
4975
5268
|
await deleteBranch(forkPath, pr.headRefName);
|
|
4976
5269
|
}
|
|
4977
|
-
console.info(colors_default.dim(`switching to '${base}' and fast-forwarding...`));
|
|
4978
|
-
await switchBranch(forkPath, base);
|
|
4979
|
-
await pullFastForward(forkPath);
|
|
4980
5270
|
return "continue";
|
|
4981
5271
|
}
|
|
4982
5272
|
async function guardAgainstOpenSyncPr(config) {
|
|
@@ -5015,7 +5305,7 @@ async function runSyncCommand(config) {
|
|
|
5015
5305
|
const { forkPath } = config;
|
|
5016
5306
|
const currentBranch = await getCurrentBranch(forkPath);
|
|
5017
5307
|
const onSyncBranch = isTemporarySyncBranch(currentBranch);
|
|
5018
|
-
if (onSyncBranch && mergeInProgress(forkPath)) {
|
|
5308
|
+
if (onSyncBranch && await mergeInProgress(forkPath)) {
|
|
5019
5309
|
await commitSyncMerge(config, currentBranch);
|
|
5020
5310
|
return;
|
|
5021
5311
|
}
|
|
@@ -5038,15 +5328,20 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5038
5328
|
const outcome = await runSyncCycle(config);
|
|
5039
5329
|
console.info();
|
|
5040
5330
|
if (outcome.status === "noop") {
|
|
5041
|
-
|
|
5331
|
+
if (outcome.behind) {
|
|
5332
|
+
console.info(colors_default.green("nothing to sync: the last sync already went past this upstream point."));
|
|
5333
|
+
for (const line of outcome.behind.split("\n")) console.info(colors_default.dim(` ${line}`));
|
|
5334
|
+
} else {
|
|
5335
|
+
console.info(colors_default.green("already up to date with upstream, nothing to sync."));
|
|
5336
|
+
}
|
|
5042
5337
|
return;
|
|
5043
5338
|
}
|
|
5044
5339
|
const { temporaryBranch } = outcome.branch;
|
|
5045
5340
|
if (outcome.status === "conflicts") {
|
|
5046
5341
|
console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve and stage them, then:`)}`);
|
|
5047
|
-
|
|
5342
|
+
console.info(colors_default.dim(" pnpm cella sync"));
|
|
5048
5343
|
console.info(colors_default.dim(" rerun commits the sync and stops for drift triage; a further rerun ships (push + PR)."));
|
|
5049
|
-
console.info(colors_default.dim(" let the rerun commit
|
|
5344
|
+
console.info(colors_default.dim(" let the rerun commit: a manual `git commit` records a merge commit that bloats the PR."));
|
|
5050
5345
|
return;
|
|
5051
5346
|
}
|
|
5052
5347
|
await commitSyncMerge(config, temporaryBranch);
|
|
@@ -5054,12 +5349,11 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5054
5349
|
|
|
5055
5350
|
// src/services/forks.ts
|
|
5056
5351
|
async function gatherForkStatus(forkPath) {
|
|
5057
|
-
const [branch,
|
|
5352
|
+
const [branch, dirty, syncRef] = await Promise.all([
|
|
5058
5353
|
getCurrentBranch(forkPath).catch(() => "unknown"),
|
|
5059
|
-
|
|
5354
|
+
getWorkingTreeChangeCount(forkPath).catch(() => 0),
|
|
5060
5355
|
getStoredSyncRef(forkPath)
|
|
5061
5356
|
]);
|
|
5062
|
-
const dirty = porcelain ? porcelain.split("\n").filter(Boolean).length : 0;
|
|
5063
5357
|
let lastSync = null;
|
|
5064
5358
|
if (syncRef) {
|
|
5065
5359
|
const commitInfo = await getCommitInfo(forkPath, syncRef).catch(() => null);
|
|
@@ -5122,7 +5416,8 @@ async function syncFork(config, fork, forkPath) {
|
|
|
5122
5416
|
list: false,
|
|
5123
5417
|
json: false,
|
|
5124
5418
|
verbose: config.verbose,
|
|
5125
|
-
hard: config.hard
|
|
5419
|
+
hard: config.hard,
|
|
5420
|
+
keepConfig: config.keepConfig
|
|
5126
5421
|
};
|
|
5127
5422
|
await runSyncCommand(forkRuntimeConfig);
|
|
5128
5423
|
}
|
|
@@ -5135,17 +5430,16 @@ async function runForks(config) {
|
|
|
5135
5430
|
if (config.fork) {
|
|
5136
5431
|
const match = forks.find((f) => f.name === config.fork);
|
|
5137
5432
|
if (!match) {
|
|
5138
|
-
|
|
5139
|
-
return;
|
|
5433
|
+
throw new Error(`fork '${config.fork}' not found in config`);
|
|
5140
5434
|
}
|
|
5141
|
-
const resolvedPath =
|
|
5435
|
+
const resolvedPath = resolve7(await resolveForkBasePath(config.forkPath), match.localPath);
|
|
5142
5436
|
await syncFork(config, match, resolvedPath);
|
|
5143
5437
|
return;
|
|
5144
5438
|
}
|
|
5145
5439
|
const forkBasePath = await resolveForkBasePath(config.forkPath);
|
|
5146
5440
|
while (true) {
|
|
5147
5441
|
const choices = await buildForkChoices(forks, forkBasePath);
|
|
5148
|
-
const forkChoices = [...choices, new Separator2(
|
|
5442
|
+
const forkChoices = [...choices, new Separator2(MENU_DIVIDER), { value: "_exit", name: colors_default.dim("exit") }];
|
|
5149
5443
|
const selectedPath = await select4({
|
|
5150
5444
|
message: "select fork to sync:",
|
|
5151
5445
|
choices: forkChoices,
|
|
@@ -5154,7 +5448,7 @@ async function runForks(config) {
|
|
|
5154
5448
|
if (selectedPath === "_exit") {
|
|
5155
5449
|
process8.exit(0);
|
|
5156
5450
|
}
|
|
5157
|
-
const resolvedForkPath =
|
|
5451
|
+
const resolvedForkPath = resolve7(forkBasePath, selectedPath);
|
|
5158
5452
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
5159
5453
|
if (!selectedFork) {
|
|
5160
5454
|
console.error(colors_default.red(`fork '${selectedPath}' not found in config`));
|
|
@@ -5167,15 +5461,15 @@ async function runForks(config) {
|
|
|
5167
5461
|
|
|
5168
5462
|
// src/services/stats.ts
|
|
5169
5463
|
import { readFile as readFile3 } from "fs/promises";
|
|
5170
|
-
import { join as
|
|
5464
|
+
import { join as join18 } from "path";
|
|
5171
5465
|
|
|
5172
5466
|
// src/utils/coverage-utils.ts
|
|
5173
|
-
import { spawnSync as
|
|
5174
|
-
import
|
|
5175
|
-
import
|
|
5467
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
5468
|
+
import { existsSync as existsSync12, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
5469
|
+
import { join as join17, relative as relative3, sep as sep2 } from "path";
|
|
5176
5470
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5177
5471
|
function summaryPath(forkPath) {
|
|
5178
|
-
return
|
|
5472
|
+
return join17(forkPath, ".coverage", "coverage-summary.json");
|
|
5179
5473
|
}
|
|
5180
5474
|
function colorPct(pct) {
|
|
5181
5475
|
if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
|
|
@@ -5194,7 +5488,7 @@ function formatAge(ms) {
|
|
|
5194
5488
|
}
|
|
5195
5489
|
function refreshCoverage(forkPath) {
|
|
5196
5490
|
console.info(colors_default.dim("running `pnpm test` (this spins up Docker and runs the full suite)..."));
|
|
5197
|
-
const result =
|
|
5491
|
+
const result = spawnSync5("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
|
|
5198
5492
|
return result.status === 0;
|
|
5199
5493
|
}
|
|
5200
5494
|
function printCoverageSummary(forkPath, options = {}) {
|
|
@@ -5202,24 +5496,24 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5202
5496
|
if (options.refresh) {
|
|
5203
5497
|
const ok = refreshCoverage(forkPath);
|
|
5204
5498
|
if (!ok) {
|
|
5205
|
-
console.info(colors_default.red("coverage run failed
|
|
5499
|
+
console.info(colors_default.red("coverage run failed: showing last cached results (if any)"));
|
|
5206
5500
|
}
|
|
5207
5501
|
console.info();
|
|
5208
5502
|
}
|
|
5209
|
-
if (!
|
|
5503
|
+
if (!existsSync12(file)) {
|
|
5210
5504
|
console.info(colors_default.bold("coverage"));
|
|
5211
|
-
console.info(colors_default.yellow(" no coverage data found
|
|
5505
|
+
console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to generate it"));
|
|
5212
5506
|
console.info();
|
|
5213
5507
|
return;
|
|
5214
5508
|
}
|
|
5215
5509
|
let summary;
|
|
5216
5510
|
let mtimeMs;
|
|
5217
5511
|
try {
|
|
5218
|
-
summary = JSON.parse(
|
|
5219
|
-
mtimeMs =
|
|
5512
|
+
summary = JSON.parse(readFileSync6(file, "utf8"));
|
|
5513
|
+
mtimeMs = statSync2(file).mtimeMs;
|
|
5220
5514
|
} catch {
|
|
5221
5515
|
console.info(colors_default.bold("coverage"));
|
|
5222
|
-
console.info(colors_default.yellow(" could not read coverage-summary.json
|
|
5516
|
+
console.info(colors_default.yellow(" could not read coverage-summary.json: run `pnpm test` to regenerate"));
|
|
5223
5517
|
console.info();
|
|
5224
5518
|
return;
|
|
5225
5519
|
}
|
|
@@ -5228,9 +5522,9 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5228
5522
|
const perPackage = /* @__PURE__ */ new Map();
|
|
5229
5523
|
for (const [key, entry] of Object.entries(summary)) {
|
|
5230
5524
|
if (key === "total") continue;
|
|
5231
|
-
const rel =
|
|
5525
|
+
const rel = relative3(forkPath, key);
|
|
5232
5526
|
if (rel.startsWith("..")) continue;
|
|
5233
|
-
const pkg = rel.split(
|
|
5527
|
+
const pkg = rel.split(sep2)[0];
|
|
5234
5528
|
const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
|
|
5235
5529
|
acc.covered += entry.lines.covered;
|
|
5236
5530
|
acc.total += entry.lines.total;
|
|
@@ -5239,10 +5533,10 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5239
5533
|
const ageLabel = formatAge(ageMs);
|
|
5240
5534
|
console.info(colors_default.bold("coverage"), colors_default.dim(`(updated ${ageLabel})`));
|
|
5241
5535
|
if (stale) {
|
|
5242
|
-
console.info(colors_default.yellow(" \u26A0 data is older than 7 days
|
|
5536
|
+
console.info(colors_default.yellow(" \u26A0 data is older than 7 days: run `pnpm test` to refresh"));
|
|
5243
5537
|
}
|
|
5244
5538
|
if (!Object.keys(summary).some((key) => key !== "total")) {
|
|
5245
|
-
console.info(colors_default.yellow(" no coverage data found
|
|
5539
|
+
console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to regenerate"));
|
|
5246
5540
|
console.info();
|
|
5247
5541
|
return;
|
|
5248
5542
|
}
|
|
@@ -5261,7 +5555,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5261
5555
|
// src/services/stats.ts
|
|
5262
5556
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
5263
5557
|
async function getWorkspacePackages(forkPath) {
|
|
5264
|
-
const content = await readFile3(
|
|
5558
|
+
const content = await readFile3(join18(forkPath, "pnpm-workspace.yaml"), "utf8");
|
|
5265
5559
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
5266
5560
|
if (pattern.endsWith("/*")) {
|
|
5267
5561
|
const base = pattern.slice(0, -2);
|
|
@@ -5325,12 +5619,12 @@ async function collectStats(forkPath) {
|
|
|
5325
5619
|
const categories = emptyCat();
|
|
5326
5620
|
const pkgStats = /* @__PURE__ */ new Map();
|
|
5327
5621
|
let totalLoc = 0;
|
|
5328
|
-
for (const { path
|
|
5329
|
-
const category = classifyFile(
|
|
5622
|
+
for (const { path, loc } of entries) {
|
|
5623
|
+
const category = classifyFile(path);
|
|
5330
5624
|
categories[category].files++;
|
|
5331
5625
|
categories[category].loc += loc;
|
|
5332
5626
|
totalLoc += loc;
|
|
5333
|
-
const pkg = matchPackage(
|
|
5627
|
+
const pkg = matchPackage(path, packages);
|
|
5334
5628
|
let entry = pkgStats.get(pkg);
|
|
5335
5629
|
if (!entry) {
|
|
5336
5630
|
entry = { total: 0, loc: 0, categories: emptyCat() };
|
|
@@ -5407,24 +5701,244 @@ function printStats(stats, verbose) {
|
|
|
5407
5701
|
}
|
|
5408
5702
|
console.info();
|
|
5409
5703
|
}
|
|
5410
|
-
async function runStats(
|
|
5704
|
+
async function runStats(config) {
|
|
5705
|
+
const { forkPath } = config;
|
|
5411
5706
|
createSpinner("counting files...");
|
|
5412
5707
|
const stats = await collectStats(forkPath);
|
|
5413
|
-
spinnerSuccess("
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5708
|
+
spinnerSuccess("counted raw lines of code");
|
|
5709
|
+
printStats(stats, config.verbose);
|
|
5710
|
+
console.info();
|
|
5711
|
+
printCoverageSummary(forkPath, { refresh: config.coverage });
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
// src/services/stats-since.ts
|
|
5715
|
+
var kindLabels = {
|
|
5716
|
+
source: "source",
|
|
5717
|
+
test: "tests",
|
|
5718
|
+
stories: "stories",
|
|
5719
|
+
generated: "generated",
|
|
5720
|
+
json: "json",
|
|
5721
|
+
docs: "docs",
|
|
5722
|
+
other: "other"
|
|
5723
|
+
};
|
|
5724
|
+
var docExtensions = /* @__PURE__ */ new Set(["md", "mdx"]);
|
|
5725
|
+
var lockFiles = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"]);
|
|
5726
|
+
var codeExtensions = [...sourceExtensions].filter((ext) => !ext.startsWith("json"));
|
|
5727
|
+
function classifyChange(filePath) {
|
|
5728
|
+
const name = filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
5729
|
+
if (lockFiles.has(name)) return "generated";
|
|
5730
|
+
const category = classifyFile(filePath);
|
|
5731
|
+
if (category !== "other") return category;
|
|
5732
|
+
const ext = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : "";
|
|
5733
|
+
if (docExtensions.has(ext)) return "docs";
|
|
5734
|
+
return sourceExtensions.has(ext) ? "source" : "other";
|
|
5735
|
+
}
|
|
5736
|
+
function classifyLine(text) {
|
|
5737
|
+
const line = text.trim();
|
|
5738
|
+
if (!line) return "blank";
|
|
5739
|
+
if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*") || line.startsWith("{/*")) {
|
|
5740
|
+
return "comments";
|
|
5741
|
+
}
|
|
5742
|
+
return "code";
|
|
5743
|
+
}
|
|
5744
|
+
function parseNumstat(output) {
|
|
5745
|
+
const files = [];
|
|
5746
|
+
for (const record of output.split("\0")) {
|
|
5747
|
+
if (!record) continue;
|
|
5748
|
+
const [added, removed, ...rest] = record.split(" ");
|
|
5749
|
+
const path = rest.join(" ");
|
|
5750
|
+
if (!path) continue;
|
|
5751
|
+
files.push({
|
|
5752
|
+
path,
|
|
5753
|
+
kind: classifyChange(path),
|
|
5754
|
+
added: Number.parseInt(added, 10) || 0,
|
|
5755
|
+
removed: Number.parseInt(removed, 10) || 0
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
return files;
|
|
5759
|
+
}
|
|
5760
|
+
function countLineContent(patch, isCounted) {
|
|
5761
|
+
const content = {
|
|
5762
|
+
code: { added: 0, removed: 0 },
|
|
5763
|
+
comments: { added: 0, removed: 0 },
|
|
5764
|
+
blank: { added: 0, removed: 0 }
|
|
5765
|
+
};
|
|
5766
|
+
let counted = false;
|
|
5767
|
+
let remaining = 0;
|
|
5768
|
+
for (const line of patch.split("\n")) {
|
|
5769
|
+
if (remaining > 0) {
|
|
5770
|
+
if (line.startsWith("\\")) continue;
|
|
5771
|
+
remaining--;
|
|
5772
|
+
if (!counted) continue;
|
|
5773
|
+
const change = line.startsWith("+") ? "added" : "removed";
|
|
5774
|
+
content[classifyLine(line.slice(1))][change]++;
|
|
5775
|
+
continue;
|
|
5776
|
+
}
|
|
5777
|
+
if (line.startsWith("diff --git ")) {
|
|
5778
|
+
counted = false;
|
|
5779
|
+
continue;
|
|
5780
|
+
}
|
|
5781
|
+
if (line.startsWith("--- a/") || line.startsWith("+++ b/")) {
|
|
5782
|
+
counted = isCounted(line.slice(6));
|
|
5783
|
+
continue;
|
|
5784
|
+
}
|
|
5785
|
+
const hunk = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line);
|
|
5786
|
+
if (hunk) remaining = Number(hunk[1] ?? 1) + Number(hunk[2] ?? 1);
|
|
5787
|
+
}
|
|
5788
|
+
return content;
|
|
5789
|
+
}
|
|
5790
|
+
async function collectBranchStats(forkPath, since) {
|
|
5791
|
+
const sinceSha = await git(["rev-parse", "--verify", "--quiet", `${since}^{commit}`], forkPath, {
|
|
5792
|
+
ignoreErrors: true
|
|
5793
|
+
});
|
|
5794
|
+
if (!sinceSha) throw new Error(`unknown ref '${since}'. pass a branch, tag or commit, such as origin/main`);
|
|
5795
|
+
const mergeBase = await git(["merge-base", sinceSha, "HEAD"], forkPath, { ignoreErrors: true });
|
|
5796
|
+
if (!mergeBase) throw new Error(`the current branch shares no history with '${since}'`);
|
|
5797
|
+
const range = [mergeBase, "HEAD"];
|
|
5798
|
+
const files = parseNumstat(await git(["diff", "--numstat", "-z", "--no-renames", ...range], forkPath));
|
|
5799
|
+
const kindOf = new Map(files.map((file) => [file.path, file.kind]));
|
|
5800
|
+
const emptyKind = () => ({ files: 0, added: 0, removed: 0 });
|
|
5801
|
+
const kinds = {
|
|
5802
|
+
source: emptyKind(),
|
|
5803
|
+
test: emptyKind(),
|
|
5804
|
+
stories: emptyKind(),
|
|
5805
|
+
generated: emptyKind(),
|
|
5806
|
+
json: emptyKind(),
|
|
5807
|
+
docs: emptyKind(),
|
|
5808
|
+
other: emptyKind()
|
|
5809
|
+
};
|
|
5810
|
+
const total = emptyKind();
|
|
5811
|
+
const packages = await getWorkspacePackages(forkPath).catch(() => []);
|
|
5812
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
5813
|
+
const add = (target, file) => {
|
|
5814
|
+
target.files++;
|
|
5815
|
+
target.added += file.added;
|
|
5816
|
+
target.removed += file.removed;
|
|
5817
|
+
};
|
|
5818
|
+
for (const file of files) {
|
|
5819
|
+
add(total, file);
|
|
5820
|
+
add(kinds[file.kind], file);
|
|
5821
|
+
if (file.kind !== "source") continue;
|
|
5822
|
+
const pkg = matchPackage(file.path, packages);
|
|
5823
|
+
const entry = byPackage.get(pkg) ?? emptyKind();
|
|
5824
|
+
add(entry, file);
|
|
5825
|
+
byPackage.set(pkg, entry);
|
|
5826
|
+
}
|
|
5827
|
+
const patch = await git(
|
|
5828
|
+
["diff", "-U0", "--no-renames", "--no-color", ...range, "--", ...codeExtensions.map((ext) => `*.${ext}`)],
|
|
5829
|
+
forkPath,
|
|
5830
|
+
{ ignoreErrors: true }
|
|
5831
|
+
);
|
|
5832
|
+
const sourceContent = kinds.source.files > 0 && !patch ? null : countLineContent(patch, (path) => kindOf.get(path) === "source");
|
|
5833
|
+
const sourceByPackage = Object.fromEntries(
|
|
5834
|
+
[...byPackage.entries()].sort((a, b) => net(b[1]) - net(a[1]) || a[0].localeCompare(b[0]))
|
|
5835
|
+
);
|
|
5836
|
+
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], forkPath);
|
|
5837
|
+
const status = await git(["status", "--porcelain"], forkPath, { ignoreErrors: true });
|
|
5838
|
+
return {
|
|
5839
|
+
since,
|
|
5840
|
+
mergeBase: mergeBase.slice(0, 7),
|
|
5841
|
+
head: branch === "HEAD" ? await git(["rev-parse", "--short", "HEAD"], forkPath) : branch,
|
|
5842
|
+
total,
|
|
5843
|
+
kinds,
|
|
5844
|
+
sourceContent,
|
|
5845
|
+
sourceByPackage,
|
|
5846
|
+
files: files.sort((a, b) => net(b) - net(a) || a.path.localeCompare(b.path)),
|
|
5847
|
+
uncommitted: status.split("\n").filter(Boolean).length
|
|
5848
|
+
};
|
|
5849
|
+
}
|
|
5850
|
+
function net(change) {
|
|
5851
|
+
return change.added - change.removed;
|
|
5852
|
+
}
|
|
5853
|
+
function signed(n) {
|
|
5854
|
+
return n > 0 ? `+${n}` : String(n);
|
|
5855
|
+
}
|
|
5856
|
+
function changedKinds(stats) {
|
|
5857
|
+
return Object.entries(stats.kinds).filter(([, change]) => change.files > 0).sort((a, b) => net(b[1]) - net(a[1]));
|
|
5858
|
+
}
|
|
5859
|
+
function formatNetList(parts) {
|
|
5860
|
+
return parts.filter(([, change]) => change.added > 0 || change.removed > 0).map(([label, change]) => `${label} ${signed(net(change))}`).join(", ");
|
|
5861
|
+
}
|
|
5862
|
+
function formatMarkdown(stats) {
|
|
5863
|
+
const row = (label, change) => `| ${label} | ${change.files} | ${signed(change.added)} | ${signed(-change.removed)} | ${signed(net(change))} |`;
|
|
5864
|
+
const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
5865
|
+
const lines = [
|
|
5866
|
+
"| Kind | Files | Added | Removed | Net |",
|
|
5867
|
+
"| --- | ---: | ---: | ---: | ---: |",
|
|
5868
|
+
...changedKinds(stats).map(
|
|
5869
|
+
([kind, change]) => row(kind === "json" ? "JSON" : capitalize(kindLabels[kind]), change)
|
|
5870
|
+
),
|
|
5871
|
+
`| **Total** | **${stats.total.files}** | **${signed(stats.total.added)}** | **${signed(-stats.total.removed)}** | **${signed(net(stats.total))}** |`
|
|
5872
|
+
];
|
|
5873
|
+
const notes = [];
|
|
5874
|
+
if (stats.sourceContent) {
|
|
5875
|
+
const content = formatNetList(Object.entries(stats.sourceContent));
|
|
5876
|
+
if (content) notes.push(`Source lines by content: ${content}.`);
|
|
5877
|
+
}
|
|
5878
|
+
const byPackage = formatNetList(Object.entries(stats.sourceByPackage));
|
|
5879
|
+
if (byPackage) notes.push(`Source lines by package: ${byPackage}.`);
|
|
5880
|
+
return notes.length > 0 ? [...lines, "", ...notes].join("\n") : lines.join("\n");
|
|
5881
|
+
}
|
|
5882
|
+
function printBranchStats(stats, verbose) {
|
|
5883
|
+
const columns = (files, change, label) => ` ${files.padStart(5)} ${signed(change.added).padStart(8)} ${signed(-change.removed).padStart(8)} ${colors_default.cyan(signed(net(change)).padStart(8))} ${label}`;
|
|
5884
|
+
console.info();
|
|
5885
|
+
console.info(colors_default.dim(` ${"files".padStart(5)} ${"added".padStart(8)} ${"removed".padStart(8)} ${"net".padStart(8)}`));
|
|
5886
|
+
for (const [kind, change] of changedKinds(stats)) {
|
|
5887
|
+
console.info(columns(String(change.files), change, kindLabels[kind]));
|
|
5888
|
+
}
|
|
5889
|
+
console.info(columns(String(stats.total.files), stats.total, colors_default.bold("total")));
|
|
5890
|
+
if (stats.sourceContent && stats.kinds.source.files > 0) {
|
|
5891
|
+
console.info();
|
|
5892
|
+
console.info(colors_default.dim(" source lines by content"));
|
|
5893
|
+
for (const [content, change] of Object.entries(stats.sourceContent)) {
|
|
5894
|
+
console.info(columns("", change, content));
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
const packages = Object.entries(stats.sourceByPackage);
|
|
5898
|
+
if (packages.length > 0) {
|
|
5899
|
+
console.info();
|
|
5900
|
+
console.info(colors_default.dim(" source lines by package"));
|
|
5901
|
+
for (const [name, change] of packages) {
|
|
5902
|
+
console.info(columns(String(change.files), change, name));
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
if (verbose) {
|
|
5418
5906
|
console.info();
|
|
5419
|
-
|
|
5907
|
+
console.info(DIVIDER);
|
|
5908
|
+
console.info();
|
|
5909
|
+
for (const file of stats.files) {
|
|
5910
|
+
console.info(columns("", file, `${file.path} ${colors_default.dim(kindLabels[file.kind])}`));
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
if (stats.uncommitted > 0) {
|
|
5914
|
+
console.info();
|
|
5915
|
+
console.info(
|
|
5916
|
+
colors_default.dim(` ${stats.uncommitted} uncommitted file${stats.uncommitted === 1 ? " is" : "s are"} not counted`)
|
|
5917
|
+
);
|
|
5918
|
+
}
|
|
5919
|
+
console.info();
|
|
5920
|
+
}
|
|
5921
|
+
async function runBranchStats(forkPath, options) {
|
|
5922
|
+
createSpinner("counting changed lines...");
|
|
5923
|
+
const stats = await collectBranchStats(forkPath, options.since);
|
|
5924
|
+
spinnerSuccess(`Counted what ${stats.head} changed since ${stats.since} (${stats.mergeBase})`);
|
|
5925
|
+
if (stats.total.files === 0) {
|
|
5926
|
+
console.info(colors_default.dim(` nothing committed since ${stats.since}`));
|
|
5927
|
+
return;
|
|
5928
|
+
}
|
|
5929
|
+
if (options.markdown) {
|
|
5930
|
+
writeStdout(formatMarkdown(stats));
|
|
5931
|
+
if (stats.uncommitted > 0) console.info(colors_default.dim(`${stats.uncommitted} uncommitted file(s) are not counted`));
|
|
5932
|
+
return;
|
|
5420
5933
|
}
|
|
5934
|
+
printBranchStats(stats, options.verbose ?? false);
|
|
5421
5935
|
}
|
|
5422
5936
|
|
|
5423
5937
|
// src/cella-cli.ts
|
|
5424
5938
|
function getForkPath() {
|
|
5425
5939
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5426
5940
|
if (envPath) {
|
|
5427
|
-
const resolved =
|
|
5941
|
+
const resolved = resolve8(envPath);
|
|
5428
5942
|
if (!existsSync13(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5429
5943
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5430
5944
|
}
|
|
@@ -5432,12 +5946,12 @@ function getForkPath() {
|
|
|
5432
5946
|
}
|
|
5433
5947
|
let cwd = process9.cwd();
|
|
5434
5948
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
5435
|
-
cwd =
|
|
5949
|
+
cwd = resolve8(cwd, "../..");
|
|
5436
5950
|
}
|
|
5437
5951
|
return cwd;
|
|
5438
5952
|
}
|
|
5439
|
-
|
|
5440
|
-
if (!existsSync13(
|
|
5953
|
+
function preflight(forkPath) {
|
|
5954
|
+
if (!existsSync13(join19(forkPath, ".git"))) {
|
|
5441
5955
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5442
5956
|
}
|
|
5443
5957
|
}
|
|
@@ -5455,7 +5969,7 @@ async function main() {
|
|
|
5455
5969
|
const userConfig = await loadConfig(forkPath);
|
|
5456
5970
|
const config = await parseCli(userConfig, forkPath);
|
|
5457
5971
|
if (!["audit", "forks", "contributions", "stats"].includes(config.service)) {
|
|
5458
|
-
|
|
5972
|
+
preflight(forkPath);
|
|
5459
5973
|
}
|
|
5460
5974
|
switch (config.service) {
|
|
5461
5975
|
case "analyze": {
|
|
@@ -5466,26 +5980,36 @@ async function main() {
|
|
|
5466
5980
|
await runSyncCommand(config);
|
|
5467
5981
|
break;
|
|
5468
5982
|
}
|
|
5469
|
-
case "migrate":
|
|
5983
|
+
case "migrate": {
|
|
5470
5984
|
await runMigrate(config);
|
|
5471
5985
|
break;
|
|
5472
|
-
|
|
5473
|
-
|
|
5986
|
+
}
|
|
5987
|
+
case "audit": {
|
|
5988
|
+
await runAudit(config);
|
|
5474
5989
|
break;
|
|
5475
|
-
|
|
5990
|
+
}
|
|
5991
|
+
case "forks": {
|
|
5476
5992
|
await runForks(config);
|
|
5477
5993
|
break;
|
|
5478
|
-
|
|
5994
|
+
}
|
|
5995
|
+
case "contributions": {
|
|
5479
5996
|
await runContributions(config);
|
|
5480
5997
|
break;
|
|
5481
|
-
|
|
5482
|
-
|
|
5998
|
+
}
|
|
5999
|
+
case "stats": {
|
|
6000
|
+
if (config.md && !config.since) throw new Error("--md goes with --since <ref>");
|
|
6001
|
+
if (config.since) {
|
|
6002
|
+
await runBranchStats(config.forkPath, { since: config.since, markdown: config.md, verbose: config.verbose });
|
|
6003
|
+
} else {
|
|
6004
|
+
await runStats(config);
|
|
6005
|
+
}
|
|
5483
6006
|
break;
|
|
6007
|
+
}
|
|
5484
6008
|
}
|
|
5485
6009
|
console.info();
|
|
5486
6010
|
} catch (error) {
|
|
5487
6011
|
console.error();
|
|
5488
|
-
console.error(`${colors_default.red("\u2717")} ${error
|
|
6012
|
+
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
5489
6013
|
process9.exit(1);
|
|
5490
6014
|
}
|
|
5491
6015
|
}
|