@cellajs/cli 0.3.1 → 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 +128 -31
- package/dist/config.d.ts +13 -21
- package/dist/config.js +13 -14
- package/dist/config.js.map +1 -1
- package/dist/index.js +1063 -686
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.",
|
|
@@ -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,19 +804,26 @@ 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."));
|
|
799
808
|
console.info();
|
|
800
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);
|
|
819
|
+
console.info();
|
|
820
|
+
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
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";
|
|
@@ -819,9 +835,9 @@ import { promisify } from "util";
|
|
|
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
|
}
|
|
@@ -932,8 +956,8 @@ async function flattenBranch(cwd, baseRef, message) {
|
|
|
932
956
|
}
|
|
933
957
|
async function commitSquash(cwd, message) {
|
|
934
958
|
for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
|
|
935
|
-
const
|
|
936
|
-
if (existsSync3(
|
|
959
|
+
const path = await gitPath(cwd, file);
|
|
960
|
+
if (existsSync3(path)) await unlink(path);
|
|
937
961
|
}
|
|
938
962
|
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
939
963
|
}
|
|
@@ -965,7 +989,7 @@ async function ensureRemote(cwd, remoteName, url) {
|
|
|
965
989
|
await git(["remote", "add", remoteName, url], cwd);
|
|
966
990
|
}
|
|
967
991
|
}
|
|
968
|
-
async function
|
|
992
|
+
async function fetchRemote(cwd, remoteName) {
|
|
969
993
|
await git(["fetch", remoteName], cwd);
|
|
970
994
|
}
|
|
971
995
|
async function fetchUpstreamTags(cwd, remoteName) {
|
|
@@ -1023,10 +1047,8 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
|
1023
1047
|
return Number.parseInt(output.trim(), 10) || 0;
|
|
1024
1048
|
}
|
|
1025
1049
|
async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
1026
|
-
const {
|
|
1027
|
-
const args = ["log", "--format=%H%x1f%s%x1f%ar"];
|
|
1028
|
-
if (oldestFirst) args.push("--reverse");
|
|
1029
|
-
args.push(`${fromRef}..${toRef}`);
|
|
1050
|
+
const { skip, limit } = options;
|
|
1051
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
|
|
1030
1052
|
const output = await git(args, cwd, { ignoreErrors: true });
|
|
1031
1053
|
if (!output) return [];
|
|
1032
1054
|
const commits = [];
|
|
@@ -1036,14 +1058,8 @@ async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
|
1036
1058
|
if (!hash || !message || !date) continue;
|
|
1037
1059
|
commits.push({ hash, message, date });
|
|
1038
1060
|
}
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
if (normalizedSkip === 0 && normalizedLimit === void 0) {
|
|
1042
|
-
return commits;
|
|
1043
|
-
}
|
|
1044
|
-
const start = Math.min(normalizedSkip, commits.length);
|
|
1045
|
-
const end = normalizedLimit === void 0 ? commits.length : Math.min(start + normalizedLimit, commits.length);
|
|
1046
|
-
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);
|
|
1047
1063
|
}
|
|
1048
1064
|
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
1049
1065
|
const info = /* @__PURE__ */ new Map();
|
|
@@ -1098,12 +1114,9 @@ async function listWorktrees(cwd) {
|
|
|
1098
1114
|
}
|
|
1099
1115
|
return paths;
|
|
1100
1116
|
}
|
|
1101
|
-
async function merge(cwd, ref
|
|
1102
|
-
const args = ["merge", "--no-ff", ref];
|
|
1103
|
-
if (options.noCommit) args.push("--no-commit");
|
|
1104
|
-
if (options.noEdit) args.push("--no-edit");
|
|
1117
|
+
async function merge(cwd, ref) {
|
|
1105
1118
|
try {
|
|
1106
|
-
await git(
|
|
1119
|
+
await git(["merge", "--no-ff", "--no-commit", "--no-edit", ref], cwd, { skipEditor: true });
|
|
1107
1120
|
return { success: true, conflicts: [] };
|
|
1108
1121
|
} catch (error) {
|
|
1109
1122
|
const conflicts = await getConflictedFiles(cwd);
|
|
@@ -1131,6 +1144,22 @@ async function checkoutFromRef(cwd, ref, filePath) {
|
|
|
1131
1144
|
async function restoreWorktreeFromRef(cwd, ref, filePath) {
|
|
1132
1145
|
await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
|
|
1133
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
|
+
}
|
|
1134
1163
|
async function getMergeBase(cwd, ref1, ref2) {
|
|
1135
1164
|
return git(["merge-base", ref1, ref2], cwd);
|
|
1136
1165
|
}
|
|
@@ -1140,6 +1169,7 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1140
1169
|
});
|
|
1141
1170
|
const changes = /* @__PURE__ */ new Map();
|
|
1142
1171
|
if (!output) return changes;
|
|
1172
|
+
const hashOrNull = (hash) => hash === "0".repeat(40) ? null : hash;
|
|
1143
1173
|
for (const line of output.split("\n")) {
|
|
1144
1174
|
if (!line) continue;
|
|
1145
1175
|
const renameMatch = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) R\d*\s+(.+)\t(.+)$/);
|
|
@@ -1147,10 +1177,9 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1147
1177
|
const [, baseHash, targetHash, oldPath, newPath] = renameMatch;
|
|
1148
1178
|
changes.set(newPath, {
|
|
1149
1179
|
status: "R",
|
|
1150
|
-
baseHash: baseHash
|
|
1151
|
-
targetHash: targetHash
|
|
1152
|
-
oldPath
|
|
1153
|
-
newPath
|
|
1180
|
+
baseHash: hashOrNull(baseHash),
|
|
1181
|
+
targetHash: hashOrNull(targetHash),
|
|
1182
|
+
oldPath
|
|
1154
1183
|
});
|
|
1155
1184
|
continue;
|
|
1156
1185
|
}
|
|
@@ -1159,8 +1188,8 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1159
1188
|
const [, baseHash, targetHash, status, filePath] = match;
|
|
1160
1189
|
changes.set(filePath, {
|
|
1161
1190
|
status,
|
|
1162
|
-
baseHash: baseHash
|
|
1163
|
-
targetHash: targetHash
|
|
1191
|
+
baseHash: hashOrNull(baseHash),
|
|
1192
|
+
targetHash: hashOrNull(targetHash)
|
|
1164
1193
|
});
|
|
1165
1194
|
}
|
|
1166
1195
|
}
|
|
@@ -1174,9 +1203,9 @@ async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
|
1174
1203
|
for (const line of out.split("\n")) {
|
|
1175
1204
|
if (!line.trim()) continue;
|
|
1176
1205
|
const [a, d, ...rest] = line.split(" ");
|
|
1177
|
-
const
|
|
1178
|
-
if (!
|
|
1179
|
-
stat.set(
|
|
1206
|
+
const path = rest.join(" ");
|
|
1207
|
+
if (!path) continue;
|
|
1208
|
+
stat.set(path, {
|
|
1180
1209
|
additions: a === "-" ? null : Number(a),
|
|
1181
1210
|
deletions: d === "-" ? null : Number(d)
|
|
1182
1211
|
});
|
|
@@ -1258,8 +1287,8 @@ async function storeLastSyncRef(cwd, upstreamHash) {
|
|
|
1258
1287
|
await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
|
|
1259
1288
|
}
|
|
1260
1289
|
}
|
|
1261
|
-
async function stagePath(cwd,
|
|
1262
|
-
await git(["add", "--",
|
|
1290
|
+
async function stagePath(cwd, path) {
|
|
1291
|
+
await git(["add", "--", path], cwd, { ignoreErrors: true });
|
|
1263
1292
|
}
|
|
1264
1293
|
async function getStoredSyncRef(cwd) {
|
|
1265
1294
|
const ref = await git(["rev-parse", "refs/cella/last-sync"], cwd, { ignoreErrors: true });
|
|
@@ -1272,14 +1301,7 @@ async function getStoredSyncHead(cwd) {
|
|
|
1272
1301
|
async function readManifestAtRef(cwd, ref) {
|
|
1273
1302
|
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
1274
1303
|
if (!raw) return null;
|
|
1275
|
-
|
|
1276
|
-
const parsed = JSON.parse(raw);
|
|
1277
|
-
const commit = parsed?.upstream?.commit;
|
|
1278
|
-
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1279
|
-
return parsed;
|
|
1280
|
-
} catch {
|
|
1281
|
-
return null;
|
|
1282
|
-
}
|
|
1304
|
+
return parseSyncManifest(raw);
|
|
1283
1305
|
}
|
|
1284
1306
|
async function readManifestBaseAtRef(cwd, ref) {
|
|
1285
1307
|
const manifest = await readManifestAtRef(cwd, ref);
|
|
@@ -1400,8 +1422,8 @@ async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
|
1400
1422
|
throw new Error(
|
|
1401
1423
|
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1402
1424
|
|
|
1403
|
-
This fork has unrelated history with upstream
|
|
1404
|
-
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
|
|
1405
1427
|
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1406
1428
|
|
|
1407
1429
|
git update-ref refs/cella/last-sync <upstream-sha>
|
|
@@ -1460,9 +1482,15 @@ async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
|
1460
1482
|
}
|
|
1461
1483
|
|
|
1462
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";
|
|
1463
1488
|
var NOTES_DIR = "cella/migrations";
|
|
1464
1489
|
var PENDING_FILE = "cella/cella.migrations.json";
|
|
1465
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
|
+
}
|
|
1466
1494
|
function isUpstreamOnly(filePath) {
|
|
1467
1495
|
return filePath === NOTES_DIR || filePath.startsWith(`${NOTES_DIR}/`);
|
|
1468
1496
|
}
|
|
@@ -1493,7 +1521,7 @@ function parseNote(id, source, fileNames) {
|
|
|
1493
1521
|
syncBreaking: meta.get("syncBreaking") === "true",
|
|
1494
1522
|
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1495
1523
|
roots: (meta.get("roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean),
|
|
1496
|
-
codemod: fileNames.find(
|
|
1524
|
+
codemod: fileNames.find(isCodemodFile) ?? null
|
|
1497
1525
|
};
|
|
1498
1526
|
}
|
|
1499
1527
|
async function listNoteIds(cwd, ref) {
|
|
@@ -1512,23 +1540,23 @@ async function arrivedNoteIds(cwd, from, to) {
|
|
|
1512
1540
|
return after.filter((id) => !known.has(id));
|
|
1513
1541
|
}
|
|
1514
1542
|
async function readPending(cwd, syncRef) {
|
|
1515
|
-
const
|
|
1516
|
-
if (!existsSync4(
|
|
1517
|
-
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"));
|
|
1518
1546
|
if (Array.isArray(parsed.pending)) return parsed.pending.filter((id) => typeof id === "string");
|
|
1519
1547
|
if (!Array.isArray(parsed.applied) || !syncRef) return [];
|
|
1520
1548
|
const applied = new Set(parsed.applied);
|
|
1521
1549
|
return (await listNoteIds(cwd, syncRef)).filter((id) => !applied.has(id));
|
|
1522
1550
|
}
|
|
1523
1551
|
async function writePending(cwd, ids) {
|
|
1524
|
-
const
|
|
1552
|
+
const path = join6(cwd, PENDING_FILE);
|
|
1525
1553
|
const pending = [...new Set(ids)].sort();
|
|
1526
1554
|
if (pending.length === 0) {
|
|
1527
|
-
await rm(
|
|
1555
|
+
await rm(path, { force: true });
|
|
1528
1556
|
return;
|
|
1529
1557
|
}
|
|
1530
|
-
await mkdir3(dirname3(
|
|
1531
|
-
await writeFile2(
|
|
1558
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1559
|
+
await writeFile2(path, `${JSON.stringify({ pending }, null, 2)}
|
|
1532
1560
|
`, "utf8");
|
|
1533
1561
|
}
|
|
1534
1562
|
function noteUrl(upstreamGitHubUrl, commit, id) {
|
|
@@ -1572,6 +1600,19 @@ function groupIgnoredUpstreamChanges(files, ignored) {
|
|
|
1572
1600
|
for (const group of groups.values()) group.paths.sort();
|
|
1573
1601
|
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
1574
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
|
+
}
|
|
1575
1616
|
function isPinnedForSync(filePath, config, unpinned) {
|
|
1576
1617
|
if (isManagedFile(filePath)) return true;
|
|
1577
1618
|
if (unpinned) return false;
|
|
@@ -1580,7 +1621,7 @@ function isPinnedForSync(filePath, config, unpinned) {
|
|
|
1580
1621
|
function hasGlobChars(entry) {
|
|
1581
1622
|
return entry.includes("*") || entry.includes("?");
|
|
1582
1623
|
}
|
|
1583
|
-
function validateOverrides(config, forkPath) {
|
|
1624
|
+
async function validateOverrides(config, forkPath, upstreamRef) {
|
|
1584
1625
|
const warnings = [];
|
|
1585
1626
|
const pinned = config.overrides?.pinned || [];
|
|
1586
1627
|
for (const entry of pinned) {
|
|
@@ -1607,6 +1648,7 @@ function validateOverrides(config, forkPath) {
|
|
|
1607
1648
|
message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
|
|
1608
1649
|
});
|
|
1609
1650
|
} else if (!existsSync5(join7(forkPath, entry.replace(/\/+$/, "")))) {
|
|
1651
|
+
if (upstreamRef && await fileExistsAtRef(forkPath, upstreamRef, entry.replace(/\/+$/, ""))) continue;
|
|
1610
1652
|
warnings.push({
|
|
1611
1653
|
type: "ignored-not-found",
|
|
1612
1654
|
pattern: entry,
|
|
@@ -1623,78 +1665,124 @@ function printWarnings(warnings) {
|
|
|
1623
1665
|
}
|
|
1624
1666
|
|
|
1625
1667
|
// src/cli.ts
|
|
1626
|
-
|
|
1627
|
-
|
|
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);
|
|
1628
1672
|
if (scope && scope !== "all" && scope !== "risk" && scope !== "protected") {
|
|
1629
1673
|
throw new Error(`invalid --scope '${scope}'. expected one of: all, risk, protected`);
|
|
1630
1674
|
}
|
|
1631
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
|
+
}
|
|
1632
1680
|
return {
|
|
1633
|
-
logFile: opts.log
|
|
1634
|
-
verbose: opts.verbose
|
|
1635
|
-
list: opts.list
|
|
1636
|
-
json: opts.json
|
|
1637
|
-
diff:
|
|
1638
|
-
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),
|
|
1639
1687
|
scope: normalizedScope,
|
|
1640
|
-
fork:
|
|
1641
|
-
hard: opts.hard
|
|
1642
|
-
unpinned: opts.unpinned
|
|
1688
|
+
fork: str(opts.fork),
|
|
1689
|
+
hard: flag(opts.hard),
|
|
1690
|
+
unpinned: flag(opts.unpinned),
|
|
1643
1691
|
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
1644
|
-
ref:
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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,
|
|
1651
1705
|
mark: Array.isArray(opts.mark) ? opts.mark.filter((id) => typeof id === "string") : void 0
|
|
1652
1706
|
};
|
|
1653
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
|
+
};
|
|
1654
1736
|
var serviceDefinitions = [
|
|
1655
1737
|
{
|
|
1656
1738
|
name: "analyze",
|
|
1657
1739
|
description: "dry run to see what would change on sync",
|
|
1658
1740
|
options: [
|
|
1659
|
-
|
|
1741
|
+
logOption,
|
|
1660
1742
|
{ flags: "--list", description: "non-interactive output for tooling (one file per line)" },
|
|
1661
|
-
|
|
1743
|
+
jsonOption,
|
|
1662
1744
|
{ flags: "--scope <scope>", description: "analyze scope for --list/--json: all|risk|protected" },
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
{ flags: "--diff <path>", description: "print unified diff for one file, then exit" },
|
|
1745
|
+
trackOption,
|
|
1746
|
+
refOption,
|
|
1747
|
+
{ flags: "--diff <path>", description: "print the unified diff for one file, then exit" },
|
|
1666
1748
|
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
1667
1749
|
],
|
|
1668
1750
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1669
1751
|
},
|
|
1670
1752
|
{
|
|
1671
1753
|
name: "sync",
|
|
1672
|
-
description: "merge upstream changes
|
|
1754
|
+
description: "merge upstream changes onto a fresh branch, sync package.json and open a squash-merge PR",
|
|
1673
1755
|
options: [
|
|
1674
|
-
|
|
1675
|
-
|
|
1756
|
+
logOption,
|
|
1757
|
+
hardOption,
|
|
1676
1758
|
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
1677
|
-
|
|
1678
|
-
|
|
1759
|
+
trackOption,
|
|
1760
|
+
refOption,
|
|
1761
|
+
{ flags: "--keep-config", description: "merge with your sync config as it stands when upstream changed its own" }
|
|
1679
1762
|
],
|
|
1680
|
-
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1681
|
-
menuDescription: () => "merge upstream changes + sync package.json"
|
|
1763
|
+
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1682
1764
|
},
|
|
1683
1765
|
{
|
|
1684
1766
|
name: "migrate",
|
|
1685
1767
|
description: "list the upstream migration notes this app has not handled yet",
|
|
1686
1768
|
options: [
|
|
1687
1769
|
{ flags: "--all", description: "list every upstream note, handled or not" },
|
|
1688
|
-
|
|
1770
|
+
jsonOption,
|
|
1689
1771
|
{ flags: "--show <id>", description: "print one note's README" },
|
|
1690
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" },
|
|
1691
1775
|
{ flags: "--mark <ids...>", description: "record notes as handled" }
|
|
1692
1776
|
],
|
|
1777
|
+
operands: {
|
|
1778
|
+
flags: "[codemodArgs...]",
|
|
1779
|
+
description: "with --run, after `--`: arguments for the codemod (default: inventory and the note's roots)"
|
|
1780
|
+
},
|
|
1693
1781
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1694
1782
|
},
|
|
1695
1783
|
{
|
|
1696
1784
|
name: "audit",
|
|
1697
|
-
description: "check for outdated packages
|
|
1785
|
+
description: "check for outdated packages and vulnerabilities",
|
|
1698
1786
|
options: [
|
|
1699
1787
|
{ flags: "--list", description: "skip interactive update prompts after printing audit results" },
|
|
1700
1788
|
{ flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
|
|
@@ -1703,23 +1791,27 @@ var serviceDefinitions = [
|
|
|
1703
1791
|
},
|
|
1704
1792
|
{
|
|
1705
1793
|
name: "forks",
|
|
1706
|
-
description: "sync
|
|
1794
|
+
description: "run normal sync inside local fork repositories",
|
|
1707
1795
|
options: [
|
|
1708
|
-
|
|
1796
|
+
forkOption,
|
|
1709
1797
|
{ flags: "--log", description: "write complete file list to cella-sync.log for each synced fork" },
|
|
1710
|
-
|
|
1711
|
-
|
|
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
|
+
}
|
|
1712
1804
|
],
|
|
1713
1805
|
includeInMenu: (context) => context.hasForks
|
|
1714
1806
|
},
|
|
1715
1807
|
{
|
|
1716
1808
|
name: "contributions",
|
|
1717
|
-
description: "pull and adopt changes from forks",
|
|
1809
|
+
description: "pull and adopt changes from local forks",
|
|
1718
1810
|
options: [
|
|
1719
|
-
|
|
1720
|
-
{ flags: "--list", description: "non-interactive output
|
|
1721
|
-
|
|
1722
|
-
{ 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" }
|
|
1723
1815
|
],
|
|
1724
1816
|
includeInMenu: (context) => context.hasForks
|
|
1725
1817
|
},
|
|
@@ -1727,26 +1819,31 @@ var serviceDefinitions = [
|
|
|
1727
1819
|
name: "stats",
|
|
1728
1820
|
description: "count files by category and workspace package",
|
|
1729
1821
|
options: [
|
|
1730
|
-
|
|
1731
|
-
{ 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" }
|
|
1732
1829
|
]
|
|
1733
1830
|
}
|
|
1734
1831
|
];
|
|
1735
|
-
|
|
1832
|
+
function getMenuContext(userConfig, forkPath) {
|
|
1736
1833
|
return {
|
|
1737
1834
|
hasForks: (userConfig.forks?.length ?? 0) > 0,
|
|
1738
|
-
isUpstreamRepo:
|
|
1835
|
+
isUpstreamRepo: isUpstreamRepo(forkPath)
|
|
1739
1836
|
};
|
|
1740
1837
|
}
|
|
1741
1838
|
function buildServiceChoices(context) {
|
|
1742
1839
|
const label = (name) => name.padEnd(14);
|
|
1743
1840
|
const baseChoices = serviceDefinitions.filter((service) => service.includeInMenu?.(context) ?? true).map((service) => ({
|
|
1744
1841
|
value: service.name,
|
|
1745
|
-
name: `${label(service.name)}${colors_default.dim(service.
|
|
1842
|
+
name: `${label(service.name)}${colors_default.dim(service.description)}`
|
|
1746
1843
|
}));
|
|
1747
1844
|
return [
|
|
1748
1845
|
...baseChoices,
|
|
1749
|
-
{ type: "separator", separator:
|
|
1846
|
+
{ type: "separator", separator: MENU_DIVIDER },
|
|
1750
1847
|
{ value: "exit", name: colors_default.red(`${label("exit")}${colors_default.dim("quit without doing anything")}`) }
|
|
1751
1848
|
];
|
|
1752
1849
|
}
|
|
@@ -1755,10 +1852,13 @@ function addServiceCommand(program, definition, setSelection) {
|
|
|
1755
1852
|
for (const option of definition.options ?? []) {
|
|
1756
1853
|
command.option(option.flags, option.description);
|
|
1757
1854
|
}
|
|
1758
|
-
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];
|
|
1759
1859
|
setSelection({
|
|
1760
1860
|
service: definition.name,
|
|
1761
|
-
options: readOptions(opts)
|
|
1861
|
+
options: readOptions(opts, operands)
|
|
1762
1862
|
});
|
|
1763
1863
|
});
|
|
1764
1864
|
}
|
|
@@ -1775,8 +1875,10 @@ function buildProgram(setSelection) {
|
|
|
1775
1875
|
" $ cella sync --unpinned",
|
|
1776
1876
|
" $ cella sync --track branch",
|
|
1777
1877
|
" $ cella sync --ref 4f7d87c",
|
|
1878
|
+
" $ cella migrate --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
|
|
1778
1879
|
" $ cella migrate --mark 20261002T0614-config-switch",
|
|
1779
1880
|
" $ cella audit --check-overrides",
|
|
1881
|
+
" $ cella stats --since origin/main --md",
|
|
1780
1882
|
" $ cella contributions --fork raak --json"
|
|
1781
1883
|
].join("\n")
|
|
1782
1884
|
);
|
|
@@ -1799,7 +1901,7 @@ function parseCommandLine(argv) {
|
|
|
1799
1901
|
async function promptForService(userConfig, forkPath) {
|
|
1800
1902
|
const selected = await select({
|
|
1801
1903
|
message: "choose a service:",
|
|
1802
|
-
choices: buildServiceChoices(
|
|
1904
|
+
choices: buildServiceChoices(getMenuContext(userConfig, forkPath)),
|
|
1803
1905
|
loop: false
|
|
1804
1906
|
});
|
|
1805
1907
|
if (selected === "exit") {
|
|
@@ -1822,9 +1924,10 @@ function buildRuntimeConfig(userConfig, forkPath, selection) {
|
|
|
1822
1924
|
}
|
|
1823
1925
|
async function parseCli(userConfig, forkPath) {
|
|
1824
1926
|
const selection = parseCommandLine(process5.argv);
|
|
1825
|
-
|
|
1927
|
+
const machineList = selection.options.list && selection.service !== "audit";
|
|
1928
|
+
if (selection.options.json || selection.options.diff || selection.options.md || machineList) setJsonMode(true);
|
|
1826
1929
|
printHeader();
|
|
1827
|
-
const warnings = validateOverrides(userConfig, forkPath);
|
|
1930
|
+
const warnings = await validateOverrides(userConfig, forkPath, resolveUpstream(userConfig.settings).branchRef);
|
|
1828
1931
|
if (warnings.length > 0) {
|
|
1829
1932
|
printWarnings(warnings);
|
|
1830
1933
|
console.info();
|
|
@@ -1842,14 +1945,14 @@ import { basename as basename4 } from "path";
|
|
|
1842
1945
|
import { execFileSync } from "child_process";
|
|
1843
1946
|
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1844
1947
|
import { tmpdir } from "os";
|
|
1845
|
-
import { basename as basename2, join as
|
|
1948
|
+
import { basename as basename2, join as join8 } from "path";
|
|
1846
1949
|
import process6 from "process";
|
|
1847
1950
|
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
1848
1951
|
const args = ["diff", "--no-color"];
|
|
1849
1952
|
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
1850
1953
|
args.push(range, "--", filePath);
|
|
1851
1954
|
try {
|
|
1852
|
-
return execFileSync("git", args, { cwd, maxBuffer:
|
|
1955
|
+
return execFileSync("git", args, { cwd, maxBuffer: MAX_BUFFER });
|
|
1853
1956
|
} catch (error) {
|
|
1854
1957
|
const stderr = error.stderr?.toString().trim();
|
|
1855
1958
|
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
@@ -1936,9 +2039,9 @@ function openWithDefaultApp(target) {
|
|
|
1936
2039
|
}
|
|
1937
2040
|
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
1938
2041
|
const html = await renderDiffPage(patch, meta);
|
|
1939
|
-
const dir =
|
|
2042
|
+
const dir = join8(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1940
2043
|
mkdirSync(dir, { recursive: true });
|
|
1941
|
-
const outPath =
|
|
2044
|
+
const outPath = join8(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
1942
2045
|
writeFileSync2(outPath, html);
|
|
1943
2046
|
openWithDefaultApp(outPath);
|
|
1944
2047
|
return outPath;
|
|
@@ -1947,13 +2050,13 @@ async function openDiffInBrowser(patch, meta, repoPath) {
|
|
|
1947
2050
|
// src/services/merge-engine.ts
|
|
1948
2051
|
import { existsSync as existsSync7 } from "fs";
|
|
1949
2052
|
import { rm as rm2 } from "fs/promises";
|
|
1950
|
-
import { join as
|
|
2053
|
+
import { join as join10 } from "path";
|
|
1951
2054
|
|
|
1952
2055
|
// src/utils/cleanup.ts
|
|
1953
2056
|
import { createHash } from "crypto";
|
|
1954
2057
|
import { existsSync as existsSync6, rmSync } from "fs";
|
|
1955
2058
|
import { tmpdir as tmpdir2 } from "os";
|
|
1956
|
-
import { basename as basename3, join as
|
|
2059
|
+
import { basename as basename3, join as join9, resolve as resolve4 } from "path";
|
|
1957
2060
|
import process7 from "process";
|
|
1958
2061
|
var WORKTREE_PREFIXES = {
|
|
1959
2062
|
sync: "cella-sync-",
|
|
@@ -1961,10 +2064,10 @@ var WORKTREE_PREFIXES = {
|
|
|
1961
2064
|
};
|
|
1962
2065
|
function buildWorktreePath(kind, repoPath) {
|
|
1963
2066
|
const key = createHash("sha256").update(resolve4(repoPath)).digest("hex").slice(0, 8);
|
|
1964
|
-
return
|
|
2067
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
|
|
1965
2068
|
}
|
|
1966
2069
|
function legacyWorktreePath(kind, repoPath) {
|
|
1967
|
-
return
|
|
2070
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
1968
2071
|
}
|
|
1969
2072
|
function getWorktreePath(repoPath) {
|
|
1970
2073
|
return buildWorktreePath("sync", repoPath);
|
|
@@ -2003,7 +2106,7 @@ async function cleanupLeftoverWorktrees(repoPath) {
|
|
|
2003
2106
|
}
|
|
2004
2107
|
async function handleAbort(signal) {
|
|
2005
2108
|
console.info();
|
|
2006
|
-
console.info(`${warningMark}
|
|
2109
|
+
console.info(`${warningMark} interrupted (${signal}): cleaning up...`);
|
|
2007
2110
|
if (currentRepoPath && currentWorktreePath) {
|
|
2008
2111
|
try {
|
|
2009
2112
|
await mergeAbort(currentWorktreePath);
|
|
@@ -2011,11 +2114,9 @@ async function handleAbort(signal) {
|
|
|
2011
2114
|
}
|
|
2012
2115
|
try {
|
|
2013
2116
|
await cleanupWorktree(currentRepoPath, currentWorktreePath);
|
|
2014
|
-
console.info(`${
|
|
2117
|
+
console.info(`${checkMark} no changes were made to your repository.`);
|
|
2015
2118
|
} catch (error) {
|
|
2016
|
-
console.error(
|
|
2017
|
-
`${colors_default.red("\u2717")} failed to clean up worktree: ${error instanceof Error ? error.message : "unknown error"}`
|
|
2018
|
-
);
|
|
2119
|
+
console.error(`${colors_default.red("\u2717")} failed to clean up worktree: ${errorMessage(error)}`);
|
|
2019
2120
|
}
|
|
2020
2121
|
}
|
|
2021
2122
|
process7.exit(1);
|
|
@@ -2111,25 +2212,44 @@ function readProperty(object, name, sourceFile) {
|
|
|
2111
2212
|
}
|
|
2112
2213
|
return void 0;
|
|
2113
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
|
+
}
|
|
2114
2233
|
function parseOverrideLists(source) {
|
|
2115
2234
|
try {
|
|
2116
2235
|
const project = new Project2({ useInMemoryFileSystem: true, skipLoadingLibFiles: true });
|
|
2117
2236
|
const sourceFile = project.createSourceFile("cella.config.ts", source);
|
|
2118
2237
|
const config = findConfigObject(sourceFile);
|
|
2119
2238
|
if (!config) return null;
|
|
2239
|
+
const lists = {
|
|
2240
|
+
pinned: [],
|
|
2241
|
+
ignored: [],
|
|
2242
|
+
packageJsonSync: readPackageJsonSync(config, sourceFile)
|
|
2243
|
+
};
|
|
2120
2244
|
const overrides = readProperty(config, "overrides", sourceFile);
|
|
2121
|
-
if (overrides === void 0) return
|
|
2245
|
+
if (overrides === void 0) return lists;
|
|
2122
2246
|
if (!overrides || !Node.isObjectLiteralExpression(overrides)) return null;
|
|
2123
|
-
const lists = { pinned: [], ignored: [] };
|
|
2124
2247
|
for (const key of LIST_KEYS) {
|
|
2125
2248
|
const list = readProperty(overrides, key, sourceFile);
|
|
2126
2249
|
if (list === void 0) continue;
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
lists[key].push(element.getLiteralValue());
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2250
|
+
const entries = list ? readStringEntries(list) : null;
|
|
2251
|
+
if (!entries) return null;
|
|
2252
|
+
lists[key] = entries;
|
|
2133
2253
|
}
|
|
2134
2254
|
return lists;
|
|
2135
2255
|
} catch {
|
|
@@ -2149,11 +2269,19 @@ function diffOverrideLists(base, incoming, fork) {
|
|
|
2149
2269
|
removed: [...baseSet].filter((entry) => !incomingSet.has(entry) && forkEntries.includes(entry))
|
|
2150
2270
|
};
|
|
2151
2271
|
};
|
|
2152
|
-
|
|
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() };
|
|
2153
2281
|
}
|
|
2154
2282
|
async function readConfigSourceAtRef(repoPath, ref) {
|
|
2155
|
-
for (const
|
|
2156
|
-
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 });
|
|
2157
2285
|
if (source) return source;
|
|
2158
2286
|
}
|
|
2159
2287
|
return null;
|
|
@@ -2170,10 +2298,11 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
|
|
|
2170
2298
|
if (!incoming) return { kind: "unreadable", side: "incoming" };
|
|
2171
2299
|
const fork = {
|
|
2172
2300
|
pinned: forkConfig.overrides?.pinned ?? [],
|
|
2173
|
-
ignored: forkConfig.overrides?.ignored ?? []
|
|
2301
|
+
ignored: forkConfig.overrides?.ignored ?? [],
|
|
2302
|
+
packageJsonSync: forkConfig.settings.packageJsonSync ?? DEFAULT_PACKAGE_JSON_SYNC
|
|
2174
2303
|
};
|
|
2175
2304
|
const changes = diffOverrideLists(base, incoming, fork);
|
|
2176
|
-
const count =
|
|
2305
|
+
const count = Object.values(changes).reduce((n, list) => n + list.added.length + list.removed.length, 0);
|
|
2177
2306
|
return count > 0 ? { kind: "changes", ...changes } : void 0;
|
|
2178
2307
|
}
|
|
2179
2308
|
|
|
@@ -2297,10 +2426,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
2297
2426
|
status = fileIsPinned ? "pinned" : "diverged";
|
|
2298
2427
|
} else if (forkChanged && !upstreamChanged2) {
|
|
2299
2428
|
status = fileIsPinned ? "ahead" : predicates.hard ? "behind" : "drifted";
|
|
2300
|
-
} else if (!forkChanged && upstreamChanged2) {
|
|
2301
|
-
status = "behind";
|
|
2302
2429
|
} else {
|
|
2303
|
-
status = "
|
|
2430
|
+
status = "behind";
|
|
2304
2431
|
}
|
|
2305
2432
|
}
|
|
2306
2433
|
const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
|
|
@@ -2385,6 +2512,21 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
2385
2512
|
}
|
|
2386
2513
|
|
|
2387
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
|
+
};
|
|
2388
2530
|
function getGitHubBaseUrl(remoteUrl) {
|
|
2389
2531
|
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^.]+)(?:\.git)?$/);
|
|
2390
2532
|
if (sshMatch) {
|
|
@@ -2396,10 +2538,13 @@ function getGitHubBaseUrl(remoteUrl) {
|
|
|
2396
2538
|
}
|
|
2397
2539
|
return null;
|
|
2398
2540
|
}
|
|
2541
|
+
function isProtectedFile(file) {
|
|
2542
|
+
return file.isIgnored || file.isPinned;
|
|
2543
|
+
}
|
|
2399
2544
|
function syncPredicates(config) {
|
|
2400
2545
|
return {
|
|
2401
|
-
isIgnored: (
|
|
2402
|
-
isPinned: (
|
|
2546
|
+
isIgnored: (path) => isIgnored(path, config),
|
|
2547
|
+
isPinned: (path) => isPinnedForSync(path, config, config.unpinned),
|
|
2403
2548
|
hard: config.hard
|
|
2404
2549
|
};
|
|
2405
2550
|
}
|
|
@@ -2418,6 +2563,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2418
2563
|
for (const file of analyzedFiles) {
|
|
2419
2564
|
const { path: filePath, status, isPinned: pinned, isIgnored: ignored, existsInFork } = file;
|
|
2420
2565
|
if (status === "identical" || status === "ahead") continue;
|
|
2566
|
+
if (!isProtectedFile(file)) continue;
|
|
2421
2567
|
if (ignored && existsInFork) {
|
|
2422
2568
|
batchRestorePaths.push(filePath);
|
|
2423
2569
|
} else if (ignored && !existsInFork) {
|
|
@@ -2429,12 +2575,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2429
2575
|
}
|
|
2430
2576
|
}
|
|
2431
2577
|
onProgress?.("starting merge in fork...");
|
|
2432
|
-
await withTemporarySyncBaseGraft(
|
|
2433
|
-
forkPath,
|
|
2434
|
-
"HEAD",
|
|
2435
|
-
mergeBaseRef,
|
|
2436
|
-
() => merge(forkPath, upstreamRef, { noCommit: true, noEdit: true })
|
|
2437
|
-
);
|
|
2578
|
+
await withTemporarySyncBaseGraft(forkPath, "HEAD", mergeBaseRef, () => merge(forkPath, upstreamRef));
|
|
2438
2579
|
if (batchRestorePaths.length > 0) {
|
|
2439
2580
|
onProgress?.(`batch restoring ${batchRestorePaths.length} pinned/ignored files...`);
|
|
2440
2581
|
await batchRestoreToHead(forkPath, batchRestorePaths);
|
|
@@ -2447,29 +2588,11 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2447
2588
|
}
|
|
2448
2589
|
}
|
|
2449
2590
|
for (const file of analyzedFiles) {
|
|
2450
|
-
const { path: filePath, status,
|
|
2591
|
+
const { path: filePath, status, existsInFork } = file;
|
|
2451
2592
|
if (status === "identical" || status === "ahead") {
|
|
2452
2593
|
continue;
|
|
2453
2594
|
}
|
|
2454
|
-
if (
|
|
2455
|
-
if (pinned && !file.renamedFrom) continue;
|
|
2456
|
-
if (pinned && file.renamedFrom) {
|
|
2457
|
-
const oldPathExists = await fileExistsInWorktree(forkPath, file.renamedFrom);
|
|
2458
|
-
if (oldPathExists) {
|
|
2459
|
-
onProgress?.(`\u2192 ${file.renamedFrom} \u2192 ${filePath}: moving fork content (pinned rename)`);
|
|
2460
|
-
try {
|
|
2461
|
-
await gitMv(forkPath, file.renamedFrom, filePath);
|
|
2462
|
-
} catch {
|
|
2463
|
-
await removeFileFully(forkPath, file.renamedFrom);
|
|
2464
|
-
await checkoutFromRef(forkPath, "HEAD", file.renamedFrom).catch(() => {
|
|
2465
|
-
});
|
|
2466
|
-
}
|
|
2467
|
-
} else {
|
|
2468
|
-
onProgress?.(`\u2192 ${filePath}: keeping fork content (pinned rename, old path removed)`);
|
|
2469
|
-
await restoreToHead(forkPath, filePath);
|
|
2470
|
-
}
|
|
2471
|
-
continue;
|
|
2472
|
-
}
|
|
2595
|
+
if (isProtectedFile(file)) continue;
|
|
2473
2596
|
if (status === "diverged") {
|
|
2474
2597
|
onProgress?.(`\u2192 ${filePath}: using git merge result (diverged)`);
|
|
2475
2598
|
continue;
|
|
@@ -2514,6 +2637,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2514
2637
|
}
|
|
2515
2638
|
}
|
|
2516
2639
|
const protectedConflicts = analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path);
|
|
2640
|
+
const protectedConflictSet = new Set(protectedConflicts);
|
|
2517
2641
|
const gitConflicts = await getConflictedFiles(forkPath);
|
|
2518
2642
|
for (const filePath of gitConflicts) {
|
|
2519
2643
|
const isProtected = isIgnored(filePath, config) || isPinnedForSync(filePath, config, config.unpinned);
|
|
@@ -2521,7 +2645,10 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2521
2645
|
if (await fileExistsAtRef(forkPath, "HEAD", filePath)) {
|
|
2522
2646
|
onProgress?.(`\u2192 ${filePath}: keeping fork (protected conflict)`);
|
|
2523
2647
|
await restoreToHead(forkPath, filePath);
|
|
2524
|
-
if (!
|
|
2648
|
+
if (!protectedConflictSet.has(filePath)) {
|
|
2649
|
+
protectedConflictSet.add(filePath);
|
|
2650
|
+
protectedConflicts.push(filePath);
|
|
2651
|
+
}
|
|
2525
2652
|
} else {
|
|
2526
2653
|
onProgress?.(`\u2192 ${filePath}: removing (protected conflict, not in fork)`);
|
|
2527
2654
|
await removeFileFully(forkPath, filePath);
|
|
@@ -2553,8 +2680,7 @@ function calculateSummary(files) {
|
|
|
2553
2680
|
pinned: 0,
|
|
2554
2681
|
ignored: 0,
|
|
2555
2682
|
deleted: 0,
|
|
2556
|
-
renamed: 0
|
|
2557
|
-
total: files.length
|
|
2683
|
+
renamed: 0
|
|
2558
2684
|
};
|
|
2559
2685
|
for (const file of files) {
|
|
2560
2686
|
if (file.status !== "identical" && isManagedFile(file.path)) {
|
|
@@ -2600,7 +2726,7 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2600
2726
|
const track = config.track ?? configTrack;
|
|
2601
2727
|
await ensureRemote(forkPath, remoteName, config.settings.upstreamUrl);
|
|
2602
2728
|
onProgress?.(`fetching upstream (${remoteName})...`);
|
|
2603
|
-
await
|
|
2729
|
+
await fetchRemote(forkPath, remoteName);
|
|
2604
2730
|
let upstreamRef = branchRef;
|
|
2605
2731
|
let releaseTag;
|
|
2606
2732
|
if (config.ref) {
|
|
@@ -2638,21 +2764,23 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2638
2764
|
getShortSha(forkPath, syncPoint)
|
|
2639
2765
|
]);
|
|
2640
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}.`;
|
|
2641
|
-
throw new
|
|
2767
|
+
throw new BehindSyncPointError(
|
|
2642
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.
|
|
2643
2769
|
${hint}`
|
|
2644
2770
|
);
|
|
2645
2771
|
}
|
|
2646
2772
|
const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
|
|
2647
|
-
const
|
|
2648
|
-
const
|
|
2649
|
-
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, {
|
|
2650
|
-
oldestFirst: true,
|
|
2651
|
-
skip: commitSkip,
|
|
2652
|
-
limit: commitListMax
|
|
2653
|
-
}) : [];
|
|
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 }) : [];
|
|
2654
2775
|
onStep?.("fetched upstream", formatFetchedUpstreamDetail(commitCount, upstreamCommits, upstreamGitHubUrl));
|
|
2655
|
-
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);
|
|
2656
2784
|
}
|
|
2657
2785
|
function resultMeta(config, ctx) {
|
|
2658
2786
|
return {
|
|
@@ -2664,7 +2792,7 @@ function resultMeta(config, ctx) {
|
|
|
2664
2792
|
upstreamCommits: ctx.upstreamCommits
|
|
2665
2793
|
};
|
|
2666
2794
|
}
|
|
2667
|
-
async function upstreamOnlyReports(config, ctx, files) {
|
|
2795
|
+
async function upstreamOnlyReports(config, ctx, files, protectedConflicts) {
|
|
2668
2796
|
const { forkPath } = config;
|
|
2669
2797
|
try {
|
|
2670
2798
|
const [baseSha, upstreamSha, upstreamOverrides] = await Promise.all([
|
|
@@ -2676,7 +2804,8 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2676
2804
|
return {
|
|
2677
2805
|
upstreamDiffRange: `${baseSha}..${upstreamSha}`,
|
|
2678
2806
|
upstreamOverrides,
|
|
2679
|
-
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? [])
|
|
2807
|
+
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? []),
|
|
2808
|
+
protectedUpstreamChanges: groupProtectedUpstreamChanges(protectedConflicts, config)
|
|
2680
2809
|
};
|
|
2681
2810
|
} catch {
|
|
2682
2811
|
return {};
|
|
@@ -2685,12 +2814,10 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2685
2814
|
async function readNotesInRange(config, ctx) {
|
|
2686
2815
|
const { forkPath } = config;
|
|
2687
2816
|
try {
|
|
2688
|
-
const aggressive = config.hard === true || config.unpinned === true;
|
|
2689
|
-
const syncBase = aggressive ? (await getEffectiveMergeBase(forkPath, "HEAD", ctx.upstreamRef)).base : ctx.mergeBase;
|
|
2690
2817
|
const [total, arrived, pending] = await Promise.all([
|
|
2691
2818
|
listNoteIds(forkPath, ctx.upstreamRef),
|
|
2692
|
-
arrivedNoteIds(forkPath,
|
|
2693
|
-
readPending(forkPath,
|
|
2819
|
+
arrivedNoteIds(forkPath, ctx.syncPoint, ctx.upstreamRef),
|
|
2820
|
+
readPending(forkPath, ctx.syncPoint)
|
|
2694
2821
|
]);
|
|
2695
2822
|
return { total: total.length, arrived, pending: [.../* @__PURE__ */ new Set([...pending, ...arrived])] };
|
|
2696
2823
|
} catch {
|
|
@@ -2701,14 +2828,15 @@ function notesResult(notes) {
|
|
|
2701
2828
|
return notes ? { migrationNotes: { total: notes.total, arrived: notes.arrived, open: notes.pending.length } } : {};
|
|
2702
2829
|
}
|
|
2703
2830
|
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2704
|
-
if (!existsSync7(
|
|
2831
|
+
if (!existsSync7(join10(forkPath, NOTES_DIR))) return;
|
|
2705
2832
|
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", NOTES_DIR], forkPath);
|
|
2706
|
-
await rm2(
|
|
2833
|
+
await rm2(join10(forkPath, NOTES_DIR), { recursive: true, force: true });
|
|
2707
2834
|
onStep?.("migration notes", `removed ${NOTES_DIR}/: notes stay upstream, list them with pnpm cella migrate`);
|
|
2708
2835
|
}
|
|
2709
2836
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2710
2837
|
const { forkPath } = config;
|
|
2711
2838
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2839
|
+
await assertSyncConfigFollowed(config, ctx);
|
|
2712
2840
|
const notes = await readNotesInRange(config, ctx);
|
|
2713
2841
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2714
2842
|
forkPath,
|
|
@@ -2719,9 +2847,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2719
2847
|
);
|
|
2720
2848
|
const summary = calculateSummary(analyzedFiles);
|
|
2721
2849
|
const synced = analyzedFiles.filter((file) => ["behind", "diverged", "renamed"].includes(file.status)).length;
|
|
2722
|
-
const totalResolved = analyzedFiles.filter(
|
|
2723
|
-
(file) => ["behind", "diverged", "renamed", "ignored", "pinned"].includes(file.status)
|
|
2724
|
-
).length;
|
|
2850
|
+
const totalResolved = analyzedFiles.filter((file) => SYNC_APPLIED_STATUSES.includes(file.status)).length;
|
|
2725
2851
|
const syncManifest = {
|
|
2726
2852
|
upstream: {
|
|
2727
2853
|
repo: upstreamGitHubUrl ? upstreamGitHubUrl.replace("https://github.com/", "") : void 0,
|
|
@@ -2764,7 +2890,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2764
2890
|
autoMergedFiles,
|
|
2765
2891
|
protectedConflicts,
|
|
2766
2892
|
...resultMeta(config, ctx),
|
|
2767
|
-
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2893
|
+
...await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts),
|
|
2768
2894
|
...notesResult(notes)
|
|
2769
2895
|
};
|
|
2770
2896
|
}
|
|
@@ -2776,12 +2902,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2776
2902
|
await createWorktree(forkPath, worktreePath, "HEAD");
|
|
2777
2903
|
onStep?.("worktree created", worktreePath);
|
|
2778
2904
|
onProgress?.("performing merge in worktree...");
|
|
2779
|
-
await withTemporarySyncBaseGraft(
|
|
2780
|
-
worktreePath,
|
|
2781
|
-
"HEAD",
|
|
2782
|
-
ctx.mergeBase,
|
|
2783
|
-
() => merge(worktreePath, ctx.upstreamRef, { noCommit: true, noEdit: true })
|
|
2784
|
-
);
|
|
2905
|
+
await withTemporarySyncBaseGraft(worktreePath, "HEAD", ctx.mergeBase, () => merge(worktreePath, ctx.upstreamRef));
|
|
2785
2906
|
onStep?.("merge complete", "upstream merged into worktree");
|
|
2786
2907
|
onProgress?.("analyzing files...");
|
|
2787
2908
|
const analyzedFiles = await analyzeRefs(
|
|
@@ -2793,8 +2914,9 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2793
2914
|
onProgress
|
|
2794
2915
|
);
|
|
2795
2916
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2796
|
-
const
|
|
2797
|
-
|
|
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`);
|
|
2798
2920
|
onProgress?.("cleaning up worktree...");
|
|
2799
2921
|
await cleanupWorktree(forkPath, worktreePath);
|
|
2800
2922
|
return {
|
|
@@ -2803,7 +2925,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2803
2925
|
summary: calculateSummary(analyzedFiles),
|
|
2804
2926
|
// For analyze mode, count diverged files as potential conflicts
|
|
2805
2927
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2806
|
-
protectedConflicts
|
|
2928
|
+
protectedConflicts,
|
|
2807
2929
|
...resultMeta(config, ctx),
|
|
2808
2930
|
...reports,
|
|
2809
2931
|
...notesResult(notes)
|
|
@@ -2823,23 +2945,39 @@ async function runMergeEngine(config, options) {
|
|
|
2823
2945
|
throw error;
|
|
2824
2946
|
}
|
|
2825
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
|
+
}
|
|
2826
2960
|
|
|
2827
2961
|
// src/services/migrate.ts
|
|
2828
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
2962
|
+
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
2829
2963
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2830
|
-
import { dirname as dirname4, join as
|
|
2964
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
2831
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
|
+
}
|
|
2832
2976
|
async function resolveNotesRef(config) {
|
|
2833
2977
|
const { forkPath } = config;
|
|
2834
2978
|
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2835
2979
|
if (!ref) return null;
|
|
2836
|
-
|
|
2837
|
-
if (!await present()) {
|
|
2838
|
-
await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
|
|
2839
|
-
await fetch2(forkPath, DEFAULT_UPSTREAM_REMOTE);
|
|
2840
|
-
if (!await present())
|
|
2841
|
-
throw new Error(`upstream commit ${ref.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2842
|
-
}
|
|
2980
|
+
await ensureUpstreamCommit(config, ref);
|
|
2843
2981
|
return ref;
|
|
2844
2982
|
}
|
|
2845
2983
|
async function readNotesStatus(config) {
|
|
@@ -2881,27 +3019,88 @@ function printNote(index, note, url) {
|
|
|
2881
3019
|
if (note.summary) console.info(` ${note.summary}`);
|
|
2882
3020
|
console.info(colors_default.dim(` read: pnpm cella migrate --show ${note.id}`));
|
|
2883
3021
|
if (url) console.info(colors_default.dim(` ${url}`));
|
|
2884
|
-
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}`));
|
|
2885
3023
|
console.info();
|
|
2886
3024
|
}
|
|
2887
|
-
async function
|
|
3025
|
+
async function writeNoteFolder(config, ref, id) {
|
|
2888
3026
|
const { forkPath } = config;
|
|
2889
3027
|
const note = await readNote(forkPath, ref, id);
|
|
2890
3028
|
if (!note) throw new Error(`no migration note '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2891
3029
|
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${NOTES_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2892
|
-
const target =
|
|
3030
|
+
const target = join11(EXTRACT_DIR, id);
|
|
2893
3031
|
for (const file of files) {
|
|
2894
3032
|
const content = execFileSync2("git", ["show", `${ref}:${NOTES_DIR}/${id}/${file}`], {
|
|
2895
3033
|
cwd: forkPath,
|
|
2896
|
-
maxBuffer:
|
|
3034
|
+
maxBuffer: MAX_BUFFER
|
|
2897
3035
|
});
|
|
2898
|
-
const
|
|
2899
|
-
await mkdir4(dirname4(
|
|
2900
|
-
await writeFile3(
|
|
3036
|
+
const path = join11(forkPath, target, file);
|
|
3037
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
3038
|
+
await writeFile3(path, content);
|
|
2901
3039
|
}
|
|
2902
|
-
|
|
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}/`);
|
|
2903
3045
|
console.info(colors_default.dim(` the README's ${NOTES_DIR}/${id}/ paths are this folder.`));
|
|
2904
|
-
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
|
+
}
|
|
2905
3104
|
}
|
|
2906
3105
|
async function markNotes(config, ref, pending, ids) {
|
|
2907
3106
|
const open = new Set(pending);
|
|
@@ -2909,7 +3108,7 @@ async function markNotes(config, ref, pending, ids) {
|
|
|
2909
3108
|
for (const id of ids) open.delete(id);
|
|
2910
3109
|
await writePending(config.forkPath, [...open]);
|
|
2911
3110
|
const marked = ids.length - unknown.length;
|
|
2912
|
-
if (marked > 0) console.info(`${
|
|
3111
|
+
if (marked > 0) console.info(`${checkMark} recorded ${marked} note(s) as handled`);
|
|
2913
3112
|
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2914
3113
|
console.info(colors_default.dim(formatNotesLine((await listNoteIds(config.forkPath, ref)).length, open.size)));
|
|
2915
3114
|
}
|
|
@@ -2924,6 +3123,7 @@ async function runMigrate(config) {
|
|
|
2924
3123
|
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2925
3124
|
const urlOf = (id) => noteUrl(githubUrl, ref, id);
|
|
2926
3125
|
if (config.mark?.length) return markNotes(config, ref, pending, config.mark);
|
|
3126
|
+
if (config.run) return runNote(config, ref, config.run);
|
|
2927
3127
|
if (config.extract) return extractNote(config, ref, config.extract);
|
|
2928
3128
|
if (config.show) {
|
|
2929
3129
|
const source = await git(["show", `${ref}:${NOTES_DIR}/${config.show}/README.md`], forkPath, {
|
|
@@ -2997,16 +3197,7 @@ function printUnifiedDiff(config, filePath) {
|
|
|
2997
3197
|
}
|
|
2998
3198
|
async function runAnalyze(config) {
|
|
2999
3199
|
createSpinner("starting analysis...");
|
|
3000
|
-
const result = await
|
|
3001
|
-
apply: false,
|
|
3002
|
-
onProgress: (message) => {
|
|
3003
|
-
spinnerText(message);
|
|
3004
|
-
},
|
|
3005
|
-
onStep: (label, detail) => {
|
|
3006
|
-
spinnerSuccess(label, detail);
|
|
3007
|
-
createSpinner("...");
|
|
3008
|
-
}
|
|
3009
|
-
});
|
|
3200
|
+
const result = await runEngineWithSpinner(config, false);
|
|
3010
3201
|
spinnerSuccess();
|
|
3011
3202
|
const scopedFiles = filterByScope(result, config.scope ?? "all");
|
|
3012
3203
|
if (config.diff) {
|
|
@@ -3028,15 +3219,15 @@ async function runAnalyze(config) {
|
|
|
3028
3219
|
upstreamChangedLines: f.upstreamChangedLines ?? null,
|
|
3029
3220
|
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null,
|
|
3030
3221
|
upstreamOnly: f.upstreamOnly ?? false,
|
|
3031
|
-
// Only on the sync config: upstream override entries the fork config does not follow
|
|
3032
|
-
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
|
|
3033
3224
|
}));
|
|
3034
3225
|
writeStdout(JSON.stringify(out, null, 2));
|
|
3035
3226
|
return result;
|
|
3036
3227
|
}
|
|
3037
3228
|
if (config.list) {
|
|
3038
3229
|
for (const file of scopedFiles) {
|
|
3039
|
-
|
|
3230
|
+
writeStdout(file.path);
|
|
3040
3231
|
}
|
|
3041
3232
|
return result;
|
|
3042
3233
|
}
|
|
@@ -3045,7 +3236,7 @@ async function runAnalyze(config) {
|
|
|
3045
3236
|
if (!file) throw new Error(`file not found in analysis results: ${config.openDiff}`);
|
|
3046
3237
|
const patch = gitDiffFile(config.forkPath, config.upstreamRef, file.path);
|
|
3047
3238
|
if (patch.length === 0) {
|
|
3048
|
-
console.info(`${
|
|
3239
|
+
console.info(`${checkMark} ${file.path} is identical to upstream`);
|
|
3049
3240
|
return result;
|
|
3050
3241
|
}
|
|
3051
3242
|
const pagePath = await openDiffInBrowser(
|
|
@@ -3058,7 +3249,7 @@ async function runAnalyze(config) {
|
|
|
3058
3249
|
},
|
|
3059
3250
|
config.forkPath
|
|
3060
3251
|
);
|
|
3061
|
-
console.info(`${
|
|
3252
|
+
console.info(`${checkMark} opened browser diff for ${file.path}`);
|
|
3062
3253
|
console.info(colors_default.dim(` ${pagePath}`));
|
|
3063
3254
|
return result;
|
|
3064
3255
|
}
|
|
@@ -3068,41 +3259,34 @@ async function runAnalyze(config) {
|
|
|
3068
3259
|
fileLinkMode: config.settings.fileLinkMode,
|
|
3069
3260
|
forkPath: config.forkPath
|
|
3070
3261
|
};
|
|
3071
|
-
printAnalysisFileGroups(result.files, linkOptions);
|
|
3072
|
-
|
|
3073
|
-
printIgnoredUpstreamChanges(result);
|
|
3074
|
-
printUpstreamOverrideChanges(result);
|
|
3075
|
-
printMaskingPinWarning(result.files);
|
|
3262
|
+
printAnalysisFileGroups(result.files, linkOptions, result);
|
|
3263
|
+
printEngineReports(result, "analysis summary");
|
|
3076
3264
|
await printMigrationNotesLine(config, result);
|
|
3077
|
-
|
|
3078
|
-
const logPath = writeLogFile(config.forkPath, result.files);
|
|
3079
|
-
console.info();
|
|
3080
|
-
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
3081
|
-
}
|
|
3265
|
+
printLogFileReport(config, result.files);
|
|
3082
3266
|
console.info();
|
|
3083
3267
|
return result;
|
|
3084
3268
|
}
|
|
3085
3269
|
|
|
3086
3270
|
// src/services/audit.ts
|
|
3087
|
-
import { spawnSync } from "child_process";
|
|
3088
|
-
import
|
|
3089
|
-
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";
|
|
3090
3274
|
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
3091
3275
|
|
|
3092
3276
|
// src/utils/audit-utils.ts
|
|
3093
3277
|
import { execFile as execFile2 } from "child_process";
|
|
3094
|
-
import
|
|
3095
|
-
import
|
|
3278
|
+
import { existsSync as existsSync8, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3279
|
+
import { join as join12 } from "path";
|
|
3096
3280
|
import { promisify as promisify2 } from "util";
|
|
3097
3281
|
var execFileAsync2 = promisify2(execFile2);
|
|
3098
|
-
var CACHE_FILE =
|
|
3282
|
+
var CACHE_FILE = join12(import.meta.dirname, "..", "..", ".audit.cache.json");
|
|
3099
3283
|
var CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
|
|
3100
3284
|
var CHANGELOG_PATHS = ["CHANGELOG.md", "CHANGELOG", "changelog.md", "HISTORY.md", "CHANGES.md", "NEWS.md"];
|
|
3101
3285
|
var DEFAULT_BRANCHES = ["main", "master"];
|
|
3102
3286
|
function loadCache() {
|
|
3103
3287
|
try {
|
|
3104
|
-
if (
|
|
3105
|
-
return JSON.parse(
|
|
3288
|
+
if (existsSync8(CACHE_FILE)) {
|
|
3289
|
+
return JSON.parse(readFileSync2(CACHE_FILE, "utf8"));
|
|
3106
3290
|
}
|
|
3107
3291
|
} catch {
|
|
3108
3292
|
}
|
|
@@ -3110,15 +3294,15 @@ function loadCache() {
|
|
|
3110
3294
|
}
|
|
3111
3295
|
function saveCache(cache) {
|
|
3112
3296
|
try {
|
|
3113
|
-
|
|
3297
|
+
writeFileSync3(CACHE_FILE, `${JSON.stringify(cache, null, 2)}
|
|
3114
3298
|
`);
|
|
3115
3299
|
} catch {
|
|
3116
3300
|
}
|
|
3117
3301
|
}
|
|
3118
3302
|
function clearCache() {
|
|
3119
3303
|
try {
|
|
3120
|
-
if (
|
|
3121
|
-
|
|
3304
|
+
if (existsSync8(CACHE_FILE)) {
|
|
3305
|
+
unlinkSync(CACHE_FILE);
|
|
3122
3306
|
console.info(colors_default.green("\u2713 cache cleared successfully"));
|
|
3123
3307
|
} else {
|
|
3124
3308
|
console.info(colors_default.yellow("no cache file found"));
|
|
@@ -3206,56 +3390,32 @@ function isMajorVersionChange(current, latest) {
|
|
|
3206
3390
|
if (!currentMajor || !latestMajor) return false;
|
|
3207
3391
|
return Number.parseInt(latestMajor, 10) > Number.parseInt(currentMajor, 10);
|
|
3208
3392
|
}
|
|
3209
|
-
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
|
+
};
|
|
3210
3402
|
try {
|
|
3211
|
-
const { stdout } = await execFileAsync2("pnpm",
|
|
3212
|
-
encoding: "
|
|
3403
|
+
const { stdout } = await execFileAsync2("pnpm", args, {
|
|
3404
|
+
encoding: "utf8",
|
|
3213
3405
|
maxBuffer: 10 * 1024 * 1024,
|
|
3214
3406
|
// 10MB buffer for large outputs
|
|
3215
3407
|
cwd
|
|
3216
3408
|
});
|
|
3217
|
-
|
|
3218
|
-
return {};
|
|
3219
|
-
}
|
|
3220
|
-
return JSON.parse(stdout);
|
|
3409
|
+
return parse(stdout);
|
|
3221
3410
|
} catch (error) {
|
|
3222
|
-
|
|
3223
|
-
const stdout = error.stdout;
|
|
3224
|
-
if (stdout?.trim()) {
|
|
3225
|
-
try {
|
|
3226
|
-
return JSON.parse(stdout);
|
|
3227
|
-
} catch {
|
|
3228
|
-
return {};
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
return {};
|
|
3411
|
+
return parse(error instanceof Error && "stdout" in error ? error.stdout : void 0);
|
|
3233
3412
|
}
|
|
3234
3413
|
}
|
|
3414
|
+
async function getOutdatedPackages(cwd) {
|
|
3415
|
+
return await runPnpmJson(["-r", "outdated", "--json"], cwd) ?? {};
|
|
3416
|
+
}
|
|
3235
3417
|
async function runPnpmAudit(cwd) {
|
|
3236
|
-
|
|
3237
|
-
const { stdout } = await execFileAsync2("pnpm", ["audit", "--json"], {
|
|
3238
|
-
encoding: "utf-8",
|
|
3239
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
3240
|
-
cwd
|
|
3241
|
-
});
|
|
3242
|
-
if (!stdout || stdout.trim() === "") {
|
|
3243
|
-
return null;
|
|
3244
|
-
}
|
|
3245
|
-
return JSON.parse(stdout);
|
|
3246
|
-
} catch (error) {
|
|
3247
|
-
if (error instanceof Error && "stdout" in error) {
|
|
3248
|
-
const stdout = error.stdout;
|
|
3249
|
-
if (stdout?.trim()) {
|
|
3250
|
-
try {
|
|
3251
|
-
return JSON.parse(stdout);
|
|
3252
|
-
} catch {
|
|
3253
|
-
return null;
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
return null;
|
|
3258
|
-
}
|
|
3418
|
+
return await runPnpmJson(["audit", "--json"], cwd);
|
|
3259
3419
|
}
|
|
3260
3420
|
function parseDependencyPath(paths) {
|
|
3261
3421
|
for (const pathStr of paths) {
|
|
@@ -3277,12 +3437,9 @@ function buildVulnerabilityMap(auditResult) {
|
|
|
3277
3437
|
const { workspace, directDependency } = parseDependencyPath(allPaths);
|
|
3278
3438
|
const existing = map.get(advisory.module_name) || [];
|
|
3279
3439
|
existing.push({
|
|
3280
|
-
id: advisory.id,
|
|
3281
3440
|
title: advisory.title,
|
|
3282
3441
|
severity: advisory.severity,
|
|
3283
|
-
url: advisory.url,
|
|
3284
3442
|
vulnerableVersions: advisory.vulnerable_versions,
|
|
3285
|
-
patchedVersions: advisory.patched_versions,
|
|
3286
3443
|
cves: advisory.cves || [],
|
|
3287
3444
|
workspace,
|
|
3288
3445
|
directDependency
|
|
@@ -3291,19 +3448,15 @@ function buildVulnerabilityMap(auditResult) {
|
|
|
3291
3448
|
}
|
|
3292
3449
|
return map;
|
|
3293
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
|
+
};
|
|
3294
3458
|
function getVulnIcon(severity) {
|
|
3295
|
-
|
|
3296
|
-
case "critical":
|
|
3297
|
-
return colors_default.red("\u25CF");
|
|
3298
|
-
case "high":
|
|
3299
|
-
return colors_default.red("\u25CF");
|
|
3300
|
-
case "moderate":
|
|
3301
|
-
return colors_default.yellow("\u25CF");
|
|
3302
|
-
case "low":
|
|
3303
|
-
return colors_default.blue("\u25CF");
|
|
3304
|
-
default:
|
|
3305
|
-
return colors_default.gray("\u25CF");
|
|
3306
|
-
}
|
|
3459
|
+
return vulnIcons[severity] ?? colors_default.gray("\u25CF");
|
|
3307
3460
|
}
|
|
3308
3461
|
function getHighestSeverity(vulns) {
|
|
3309
3462
|
if (vulns.length === 0) return null;
|
|
@@ -3313,13 +3466,13 @@ function getHighestSeverity(vulns) {
|
|
|
3313
3466
|
}
|
|
3314
3467
|
return null;
|
|
3315
3468
|
}
|
|
3316
|
-
function middleTruncate(
|
|
3317
|
-
if (
|
|
3469
|
+
function middleTruncate(str2, maxLen) {
|
|
3470
|
+
if (str2.length <= maxLen) return str2;
|
|
3318
3471
|
const ellipsis = "\u2026";
|
|
3319
3472
|
const charsToShow = maxLen - ellipsis.length;
|
|
3320
3473
|
const frontChars = Math.ceil(charsToShow / 2);
|
|
3321
3474
|
const backChars = Math.floor(charsToShow / 2);
|
|
3322
|
-
return
|
|
3475
|
+
return str2.slice(0, frontChars) + ellipsis + str2.slice(-backChars);
|
|
3323
3476
|
}
|
|
3324
3477
|
function formatDependents(dependents) {
|
|
3325
3478
|
if (dependents.length === 0) return "";
|
|
@@ -3364,18 +3517,42 @@ function parseYamlBlockList(content, key) {
|
|
|
3364
3517
|
}
|
|
3365
3518
|
|
|
3366
3519
|
// src/services/audit.ts
|
|
3367
|
-
|
|
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) {
|
|
3368
3545
|
const { forkPath } = config;
|
|
3369
|
-
if (
|
|
3546
|
+
if (config.checkOverrides) {
|
|
3370
3547
|
await checkOverrides(forkPath);
|
|
3371
3548
|
return;
|
|
3372
3549
|
}
|
|
3373
3550
|
const spinner = createSpinner("checking for outdated packages & vulnerabilities...");
|
|
3374
3551
|
try {
|
|
3375
|
-
if (
|
|
3552
|
+
if (config.force) {
|
|
3376
3553
|
spinnerText("clearing pnpm metadata cache...");
|
|
3377
3554
|
clearCache();
|
|
3378
|
-
|
|
3555
|
+
spawnSync2("pnpm", ["cache", "delete", "*"], {
|
|
3379
3556
|
cwd: forkPath,
|
|
3380
3557
|
stdio: "ignore"
|
|
3381
3558
|
});
|
|
@@ -3389,25 +3566,13 @@ async function runAudit(config, options = {}) {
|
|
|
3389
3566
|
return;
|
|
3390
3567
|
}
|
|
3391
3568
|
const cache = loadCache();
|
|
3392
|
-
spinnerText(`found ${packageNames.length} outdated package(s)
|
|
3393
|
-
const
|
|
3394
|
-
const readDependentPkg = (location) => {
|
|
3395
|
-
let cached = pkgJsonCache.get(location);
|
|
3396
|
-
if (cached === void 0) {
|
|
3397
|
-
try {
|
|
3398
|
-
cached = JSON.parse(fs2.readFileSync(path2.join(location, "package.json"), "utf-8"));
|
|
3399
|
-
} catch {
|
|
3400
|
-
cached = null;
|
|
3401
|
-
}
|
|
3402
|
-
pkgJsonCache.set(location, cached ?? null);
|
|
3403
|
-
}
|
|
3404
|
-
return cached ?? null;
|
|
3405
|
-
};
|
|
3569
|
+
spinnerText(`found ${packageNames.length} outdated package(s), fetching metadata...`);
|
|
3570
|
+
const readDependentPkg = createDependentPkgReader();
|
|
3406
3571
|
const dependentNameCache = /* @__PURE__ */ new Map();
|
|
3407
3572
|
for (const name of packageNames) {
|
|
3408
3573
|
for (const dep of outdatedPackages[name].dependentPackages) {
|
|
3409
3574
|
if (!dependentNameCache.has(dep.location)) {
|
|
3410
|
-
dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name ||
|
|
3575
|
+
dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name || basename5(dep.location));
|
|
3411
3576
|
}
|
|
3412
3577
|
}
|
|
3413
3578
|
}
|
|
@@ -3518,7 +3683,7 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3518
3683
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
3519
3684
|
for (const pkg of enhancedPackages) {
|
|
3520
3685
|
for (const loc of pkg.dependentLocations) {
|
|
3521
|
-
const dependentName = dependentNameCache.get(loc) ||
|
|
3686
|
+
const dependentName = dependentNameCache.get(loc) || basename5(loc);
|
|
3522
3687
|
const existing = dependentMap.get(dependentName);
|
|
3523
3688
|
if (existing) {
|
|
3524
3689
|
existing.count++;
|
|
@@ -3530,34 +3695,15 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3530
3695
|
const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
3531
3696
|
const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
|
|
3532
3697
|
for (const [dependentName, { location, count }] of sortedDependents) {
|
|
3533
|
-
const packageJsonPath2 =
|
|
3534
|
-
const relativePath =
|
|
3698
|
+
const packageJsonPath2 = join13(location, "package.json");
|
|
3699
|
+
const relativePath = relative2(forkPath, packageJsonPath2);
|
|
3535
3700
|
const paddedName = dependentName.padEnd(maxDependentNameLen);
|
|
3536
3701
|
console.info(
|
|
3537
3702
|
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count}`)}`
|
|
3538
3703
|
);
|
|
3539
3704
|
}
|
|
3540
3705
|
}
|
|
3541
|
-
|
|
3542
|
-
const hasVulnerabilities = auditResult !== null && Object.values(auditResult.metadata?.vulnerabilities || {}).some((c) => c > 0);
|
|
3543
|
-
const hasPackages = packages.length > 0;
|
|
3544
|
-
let confirmMessage = "update packages?";
|
|
3545
|
-
if (hasVulnerabilities && hasPackages) {
|
|
3546
|
-
confirmMessage = "update packages and fix vulnerabilities?";
|
|
3547
|
-
} else if (hasVulnerabilities) {
|
|
3548
|
-
confirmMessage = "fix vulnerabilities?";
|
|
3549
|
-
}
|
|
3550
|
-
let wantsUpdate;
|
|
3551
|
-
try {
|
|
3552
|
-
wantsUpdate = await confirm({
|
|
3553
|
-
message: confirmMessage,
|
|
3554
|
-
default: true
|
|
3555
|
-
});
|
|
3556
|
-
} catch {
|
|
3557
|
-
return;
|
|
3558
|
-
}
|
|
3559
|
-
if (!wantsUpdate) return;
|
|
3560
|
-
console.info();
|
|
3706
|
+
function buildUpdateChoices(packages, auditResult, hasVulnerabilities) {
|
|
3561
3707
|
const makeChoice = (pkg) => {
|
|
3562
3708
|
const devTag = pkg.isDev ? colors_default.dim(" (dev)") : "";
|
|
3563
3709
|
const version = `${colors_default.red(pkg.current)} \u2192 ${colors_default.green(pkg.latest)}`;
|
|
@@ -3568,17 +3714,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3568
3714
|
checked: false
|
|
3569
3715
|
};
|
|
3570
3716
|
};
|
|
3571
|
-
const AUDIT_FIX_VALUE = "__audit_fix__";
|
|
3572
3717
|
const pinned = packages.filter((p) => p.pinnedIn.length > 0);
|
|
3573
3718
|
const regular = packages.filter((p) => p.pinnedIn.length === 0);
|
|
3574
3719
|
const choices = [];
|
|
3575
3720
|
if (hasVulnerabilities) {
|
|
3576
|
-
const
|
|
3577
|
-
const parts = [];
|
|
3578
|
-
if (vulnMeta.critical) parts.push(colors_default.red(`${vulnMeta.critical} critical`));
|
|
3579
|
-
if (vulnMeta.high) parts.push(colors_default.red(`${vulnMeta.high} high`));
|
|
3580
|
-
if (vulnMeta.moderate) parts.push(colors_default.yellow(`${vulnMeta.moderate} moderate`));
|
|
3581
|
-
if (vulnMeta.low) parts.push(colors_default.blue(`${vulnMeta.low} low`));
|
|
3721
|
+
const parts = formatSeverityCounts(auditResult?.metadata?.vulnerabilities || {});
|
|
3582
3722
|
const vulnSummary = parts.length > 0 ? ` ${colors_default.dim("\xB7")} ${parts.join(" ")}` : "";
|
|
3583
3723
|
choices.push(new Separator(colors_default.red(`\u2500\u2500 vulnerabilities${vulnSummary} \u2500\u2500`)));
|
|
3584
3724
|
choices.push({
|
|
@@ -3597,6 +3737,29 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3597
3737
|
}
|
|
3598
3738
|
choices.push(...regular.map(makeChoice));
|
|
3599
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);
|
|
3600
3763
|
let selected;
|
|
3601
3764
|
try {
|
|
3602
3765
|
selected = await checkbox({
|
|
@@ -3624,6 +3787,9 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3624
3787
|
}
|
|
3625
3788
|
const runAuditFix = selected.includes(AUDIT_FIX_VALUE);
|
|
3626
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) {
|
|
3627
3793
|
const summaryParts = [];
|
|
3628
3794
|
if (selectedPackages.length > 0) {
|
|
3629
3795
|
summaryParts.push(`updating ${selectedPackages.length} package${selectedPackages.length !== 1 ? "s" : ""}`);
|
|
@@ -3655,17 +3821,15 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3655
3821
|
for (const pkgName of selectedPackages) {
|
|
3656
3822
|
const pkg = packages.find((p) => p.name === pkgName);
|
|
3657
3823
|
if (!pkg) continue;
|
|
3658
|
-
for (
|
|
3659
|
-
const
|
|
3660
|
-
if (
|
|
3661
|
-
|
|
3662
|
-
}
|
|
3663
|
-
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]);
|
|
3664
3828
|
}
|
|
3665
3829
|
}
|
|
3666
3830
|
if (runAuditFix) {
|
|
3667
3831
|
console.info(colors_default.dim("running pnpm audit --fix override..."));
|
|
3668
|
-
const auditFixResult =
|
|
3832
|
+
const auditFixResult = spawnSync2("pnpm", ["audit", "--fix", "override"], {
|
|
3669
3833
|
cwd: forkPath,
|
|
3670
3834
|
stdio: "inherit"
|
|
3671
3835
|
});
|
|
@@ -3676,7 +3840,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3676
3840
|
console.error(colors_default.red(`\u2717 pnpm audit --fix override failed (exit code ${auditFixResult.status})`));
|
|
3677
3841
|
}
|
|
3678
3842
|
}
|
|
3679
|
-
for (const [workspace,
|
|
3843
|
+
for (const [workspace, pkgs] of workspacePackages) {
|
|
3680
3844
|
const pkgList = pkgs.join(", ");
|
|
3681
3845
|
console.info(`${colors_default.cyan("\u2191")} ${colors_default.bold(workspace)}: ${colors_default.dim(pkgList)}`);
|
|
3682
3846
|
}
|
|
@@ -3685,7 +3849,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3685
3849
|
const selectedCatalogPackages = selectedPackages.filter((pkgName) => catalogPackages.has(pkgName));
|
|
3686
3850
|
if (selectedCatalogPackages.length > 0) {
|
|
3687
3851
|
console.info(colors_default.dim(`normalizing catalog consumers: ${selectedCatalogPackages.join(", ")}`));
|
|
3688
|
-
const normalizeResult =
|
|
3852
|
+
const normalizeResult = spawnSync2("pnpm", ["-r", "up", ...selectedCatalogPackages], {
|
|
3689
3853
|
cwd: forkPath,
|
|
3690
3854
|
stdio: "inherit"
|
|
3691
3855
|
});
|
|
@@ -3695,7 +3859,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3695
3859
|
return;
|
|
3696
3860
|
}
|
|
3697
3861
|
}
|
|
3698
|
-
const result =
|
|
3862
|
+
const result = spawnSync2("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
|
|
3699
3863
|
cwd: forkPath,
|
|
3700
3864
|
stdio: "inherit"
|
|
3701
3865
|
});
|
|
@@ -3710,15 +3874,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3710
3874
|
function printVulnerabilityResults(auditResult, vulnMap) {
|
|
3711
3875
|
if (auditResult && vulnMap.size > 0) {
|
|
3712
3876
|
const vulnMeta = auditResult.metadata?.vulnerabilities || {};
|
|
3713
|
-
const criticalCount = vulnMeta.critical || 0;
|
|
3714
|
-
const highCount = vulnMeta.high || 0;
|
|
3715
|
-
const moderateCount = vulnMeta.moderate || 0;
|
|
3716
|
-
const lowCount = vulnMeta.low || 0;
|
|
3717
3877
|
console.info();
|
|
3718
3878
|
console.info(
|
|
3719
|
-
`${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("")}`
|
|
3720
3880
|
);
|
|
3721
|
-
console.info(
|
|
3881
|
+
console.info(DIVIDER);
|
|
3722
3882
|
for (const [pkgName, vulns] of vulnMap.entries()) {
|
|
3723
3883
|
for (const vuln of vulns) {
|
|
3724
3884
|
const severityIcon = getVulnIcon(vuln.severity);
|
|
@@ -3738,14 +3898,14 @@ function printVulnerabilityResults(auditResult, vulnMap) {
|
|
|
3738
3898
|
}
|
|
3739
3899
|
} else {
|
|
3740
3900
|
console.info();
|
|
3741
|
-
console.info(`${
|
|
3901
|
+
console.info(`${checkMark} no vulnerabilities found`);
|
|
3742
3902
|
}
|
|
3743
3903
|
console.info();
|
|
3744
3904
|
}
|
|
3745
3905
|
function readCatalogPackageNames(forkPath) {
|
|
3746
|
-
const workspacePath =
|
|
3906
|
+
const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
|
|
3747
3907
|
try {
|
|
3748
|
-
const content =
|
|
3908
|
+
const content = readFileSync3(workspacePath, "utf8");
|
|
3749
3909
|
return new Set(Object.keys(parseYamlBlockMap(content, "catalog")));
|
|
3750
3910
|
} catch {
|
|
3751
3911
|
return /* @__PURE__ */ new Set();
|
|
@@ -3753,18 +3913,18 @@ function readCatalogPackageNames(forkPath) {
|
|
|
3753
3913
|
}
|
|
3754
3914
|
function readOverrides(forkPath) {
|
|
3755
3915
|
const overrides = /* @__PURE__ */ new Map();
|
|
3756
|
-
const pkgJsonPath =
|
|
3916
|
+
const pkgJsonPath = join13(forkPath, "package.json");
|
|
3757
3917
|
try {
|
|
3758
|
-
const pkgJson = JSON.parse(
|
|
3918
|
+
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
3759
3919
|
const pnpmOverrides = pkgJson.pnpm?.overrides || {};
|
|
3760
3920
|
for (const [key, value] of Object.entries(pnpmOverrides)) {
|
|
3761
3921
|
overrides.set(key, { target: value, source: "package.json" });
|
|
3762
3922
|
}
|
|
3763
3923
|
} catch {
|
|
3764
3924
|
}
|
|
3765
|
-
const workspacePath =
|
|
3925
|
+
const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
|
|
3766
3926
|
try {
|
|
3767
|
-
const content =
|
|
3927
|
+
const content = readFileSync3(workspacePath, "utf8");
|
|
3768
3928
|
const wsOverrides = parseYamlBlockMap(content, "overrides");
|
|
3769
3929
|
for (const [key, value] of Object.entries(wsOverrides)) {
|
|
3770
3930
|
overrides.set(key, { target: value, source: "pnpm-workspace.yaml" });
|
|
@@ -3807,7 +3967,7 @@ async function checkOverrides(forkPath) {
|
|
|
3807
3967
|
const maxTargetLen = Math.max(6, ...allValues.map((v) => v.target.length));
|
|
3808
3968
|
if (securityOverrides.length > 0) {
|
|
3809
3969
|
console.info(colors_default.bold(`security overrides (${securityOverrides.length})`));
|
|
3810
|
-
console.info(
|
|
3970
|
+
console.info(DIVIDER);
|
|
3811
3971
|
for (const o of securityOverrides) {
|
|
3812
3972
|
const icon = o.status === "active" ? colors_default.red("\u25CF") : colors_default.green("\u25CB");
|
|
3813
3973
|
const statusText = o.status === "active" ? colors_default.red("active") : colors_default.green("likely stale");
|
|
@@ -3820,7 +3980,7 @@ async function checkOverrides(forkPath) {
|
|
|
3820
3980
|
}
|
|
3821
3981
|
if (pinOverrides.length > 0) {
|
|
3822
3982
|
console.info(colors_default.bold(`version pins (${pinOverrides.length})`));
|
|
3823
|
-
console.info(
|
|
3983
|
+
console.info(DIVIDER);
|
|
3824
3984
|
for (const o of pinOverrides) {
|
|
3825
3985
|
const keyText = colors_default.white(o.key.padEnd(maxKeyLen));
|
|
3826
3986
|
const targetText = colors_default.dim(o.target.padEnd(maxTargetLen));
|
|
@@ -3833,26 +3993,24 @@ async function checkOverrides(forkPath) {
|
|
|
3833
3993
|
const activeCount = securityOverrides.filter((o) => o.status === "active").length;
|
|
3834
3994
|
if (staleCount > 0) {
|
|
3835
3995
|
console.info(
|
|
3836
|
-
`${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`
|
|
3837
3997
|
);
|
|
3838
|
-
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"));
|
|
3839
3999
|
}
|
|
3840
4000
|
if (activeCount > 0) {
|
|
3841
4001
|
console.info(
|
|
3842
|
-
`${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`
|
|
3843
4003
|
);
|
|
3844
4004
|
}
|
|
3845
4005
|
if (pinOverrides.length > 0) {
|
|
3846
4006
|
console.info(
|
|
3847
|
-
`${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`
|
|
3848
4008
|
);
|
|
3849
4009
|
}
|
|
3850
4010
|
console.info();
|
|
3851
4011
|
}
|
|
3852
4012
|
|
|
3853
4013
|
// src/services/contributions.ts
|
|
3854
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
3855
|
-
import { join as join16 } from "path";
|
|
3856
4014
|
import {
|
|
3857
4015
|
createPrompt,
|
|
3858
4016
|
isDownKey,
|
|
@@ -3860,14 +4018,13 @@ import {
|
|
|
3860
4018
|
isSpaceKey,
|
|
3861
4019
|
isUpKey,
|
|
3862
4020
|
useKeypress,
|
|
3863
|
-
useMemo,
|
|
3864
4021
|
usePagination,
|
|
3865
4022
|
useState
|
|
3866
4023
|
} from "@inquirer/core";
|
|
3867
4024
|
import { select as select2 } from "@inquirer/prompts";
|
|
3868
4025
|
|
|
3869
4026
|
// src/services/contrib-core.ts
|
|
3870
|
-
import { dirname as dirname5, join as
|
|
4027
|
+
import { dirname as dirname5, join as join14, resolve as resolve5 } from "path";
|
|
3871
4028
|
async function listFilesAtRef(repoPath, ref) {
|
|
3872
4029
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
3873
4030
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -3875,8 +4032,8 @@ async function listFilesAtRef(repoPath, ref) {
|
|
|
3875
4032
|
async function detectContributableFiles(repoPath, baseRef, forkRef, config, forkTerritory = []) {
|
|
3876
4033
|
const mergeBase = (await git(["merge-base", baseRef, forkRef], repoPath, { ignoreErrors: true })).trim() || baseRef;
|
|
3877
4034
|
const predicates = {
|
|
3878
|
-
isIgnored: (
|
|
3879
|
-
isPinned: (
|
|
4035
|
+
isIgnored: (path) => isIgnored(path, config) || isUnderAnyFolder(path, forkTerritory),
|
|
4036
|
+
isPinned: (path) => isPinned(path, config)
|
|
3880
4037
|
};
|
|
3881
4038
|
const analyzed = await analyzeRefs(repoPath, baseRef, forkRef, mergeBase, predicates);
|
|
3882
4039
|
await enrichChangeInfo(repoPath, analyzed, mergeBase, baseRef, forkRef);
|
|
@@ -3931,7 +4088,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
3931
4088
|
async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
|
|
3932
4089
|
const branch = `contrib/${forkName}`;
|
|
3933
4090
|
const gitDir = resolve5(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
3934
|
-
const tmpIndex =
|
|
4091
|
+
const tmpIndex = join14(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
3935
4092
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
3936
4093
|
try {
|
|
3937
4094
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -4004,20 +4161,14 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
|
4004
4161
|
if (patch.length === 0) return null;
|
|
4005
4162
|
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
4006
4163
|
}
|
|
4007
|
-
function shouldApplyContributionsUnstaged(repoPath) {
|
|
4008
|
-
return JSON.parse(readFileSync4(join16(repoPath, "package.json"), "utf8")).name === "cella";
|
|
4009
|
-
}
|
|
4010
4164
|
var contribPrompt = createPrompt((config, done) => {
|
|
4011
4165
|
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
4012
4166
|
const [items, setItems] = useState(config.items);
|
|
4013
4167
|
const [active, setActive] = useState(0);
|
|
4014
4168
|
const [statusMsg, setStatusMsg] = useState("");
|
|
4015
4169
|
const [promptStatus, setPromptStatus] = useState("idle");
|
|
4016
|
-
const bounds = useMemo(() => {
|
|
4017
|
-
if (items.length === 0) return { first: 0, last: 0 };
|
|
4018
|
-
return { first: 0, last: items.length - 1 };
|
|
4019
|
-
}, [items]);
|
|
4020
4170
|
useKeypress(async (key) => {
|
|
4171
|
+
const last = items.length - 1;
|
|
4021
4172
|
if (items.length === 0) {
|
|
4022
4173
|
if (isEnterKey(key) || key.name === "q") {
|
|
4023
4174
|
setPromptStatus("done");
|
|
@@ -4041,22 +4192,22 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4041
4192
|
return;
|
|
4042
4193
|
}
|
|
4043
4194
|
if (key.ctrl && isUpKey(key)) {
|
|
4044
|
-
setActive(
|
|
4195
|
+
setActive(0);
|
|
4045
4196
|
setStatusMsg("");
|
|
4046
4197
|
return;
|
|
4047
4198
|
}
|
|
4048
4199
|
if (key.ctrl && isDownKey(key)) {
|
|
4049
|
-
setActive(
|
|
4200
|
+
setActive(last);
|
|
4050
4201
|
setStatusMsg("");
|
|
4051
4202
|
return;
|
|
4052
4203
|
}
|
|
4053
4204
|
if (isUpKey(key)) {
|
|
4054
|
-
setActive(active <=
|
|
4205
|
+
setActive(active <= 0 ? 0 : active - 1);
|
|
4055
4206
|
setStatusMsg("");
|
|
4056
4207
|
return;
|
|
4057
4208
|
}
|
|
4058
4209
|
if (isDownKey(key)) {
|
|
4059
|
-
setActive(active >=
|
|
4210
|
+
setActive(active >= last ? last : active + 1);
|
|
4060
4211
|
setStatusMsg("");
|
|
4061
4212
|
return;
|
|
4062
4213
|
}
|
|
@@ -4069,7 +4220,7 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4069
4220
|
const pagePath = await showContribDiff(items[active], baseRef, cwd, forkName);
|
|
4070
4221
|
setStatusMsg(pagePath ? `opened ${items[active].path} in browser` : `no changes for ${items[active].path}`);
|
|
4071
4222
|
} catch (error) {
|
|
4072
|
-
setStatusMsg(`diff failed: ${error
|
|
4223
|
+
setStatusMsg(`diff failed: ${errorMessage(error)}`);
|
|
4073
4224
|
}
|
|
4074
4225
|
return;
|
|
4075
4226
|
}
|
|
@@ -4083,12 +4234,12 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4083
4234
|
if (promptStatus === "done") {
|
|
4084
4235
|
const checked = items.filter((i) => i.checked);
|
|
4085
4236
|
if (checked.length > 0) {
|
|
4086
|
-
return `${
|
|
4237
|
+
return `${checkMark} ${checked.length} files accepted`;
|
|
4087
4238
|
}
|
|
4088
|
-
return `${
|
|
4239
|
+
return `${checkMark} done`;
|
|
4089
4240
|
}
|
|
4090
4241
|
if (items.length === 0) {
|
|
4091
|
-
return `${
|
|
4242
|
+
return `${checkMark} no contributions: press enter`;
|
|
4092
4243
|
}
|
|
4093
4244
|
const checkedCount = items.filter((i) => i.checked).length;
|
|
4094
4245
|
const page = usePagination({
|
|
@@ -4122,6 +4273,38 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4122
4273
|
const lines = [header, page, statusLine, helpLine].filter(Boolean).join("\n").trimEnd();
|
|
4123
4274
|
return `${lines}\x1B[?25l`;
|
|
4124
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
|
+
}
|
|
4125
4308
|
async function fetchForkBranch(cellaPath, fetchSource, pullBranch) {
|
|
4126
4309
|
await git(["fetch", fetchSource, pullBranch], cellaPath);
|
|
4127
4310
|
return git(["rev-parse", "FETCH_HEAD"], cellaPath);
|
|
@@ -4149,42 +4332,8 @@ async function runContributions(config) {
|
|
|
4149
4332
|
}
|
|
4150
4333
|
const forkBasePath = await resolveForkBasePath(config.forkPath);
|
|
4151
4334
|
const validated = forks.map((fork2) => validateForkPath(fork2, forkBasePath));
|
|
4152
|
-
const
|
|
4153
|
-
|
|
4154
|
-
if (config.fork) {
|
|
4155
|
-
const match = validated.find((v) => v.fork.name === config.fork);
|
|
4156
|
-
if (!match?.valid) {
|
|
4157
|
-
console.error(colors_default.red(`fork '${config.fork}' not found or invalid in config`));
|
|
4158
|
-
return;
|
|
4159
|
-
}
|
|
4160
|
-
selectedFork = match;
|
|
4161
|
-
} else if (config.list || config.json || config.diff) {
|
|
4162
|
-
if (validForks.length === 0) {
|
|
4163
|
-
console.error(colors_default.red("no valid forks configured"));
|
|
4164
|
-
return;
|
|
4165
|
-
}
|
|
4166
|
-
if (validForks.length > 1) {
|
|
4167
|
-
console.error(colors_default.red("multiple forks configured; pass --fork <name> to choose one"));
|
|
4168
|
-
return;
|
|
4169
|
-
}
|
|
4170
|
-
selectedFork = validForks[0];
|
|
4171
|
-
} else {
|
|
4172
|
-
const choices = validated.map((v) => ({
|
|
4173
|
-
value: v.fork.name,
|
|
4174
|
-
name: v.valid ? `${v.fork.name} ${colors_default.dim(`[${v.fork.pullBranch}] ${v.fork.localPath}`)}` : `${v.fork.name} ${colors_default.dim(v.fork.localPath)}`,
|
|
4175
|
-
disabled: v.valid ? false : v.error ?? "invalid"
|
|
4176
|
-
}));
|
|
4177
|
-
const picked = await select2({
|
|
4178
|
-
message: "select fork to pull contributions from:",
|
|
4179
|
-
choices,
|
|
4180
|
-
loop: false
|
|
4181
|
-
});
|
|
4182
|
-
selectedFork = validated.find((v) => v.fork.name === picked);
|
|
4183
|
-
}
|
|
4184
|
-
if (!selectedFork) {
|
|
4185
|
-
console.info(colors_default.dim("no fork selected."));
|
|
4186
|
-
return;
|
|
4187
|
-
}
|
|
4335
|
+
const selectedFork = await selectFork(config, validated);
|
|
4336
|
+
if (!selectedFork) return;
|
|
4188
4337
|
const { fork, resolvedPath } = selectedFork;
|
|
4189
4338
|
const forkName = fork.name;
|
|
4190
4339
|
createSpinner("pulling fork contributions...");
|
|
@@ -4207,40 +4356,28 @@ async function runContributions(config) {
|
|
|
4207
4356
|
if (appliedFiles.length > 0) {
|
|
4208
4357
|
const stat = await getDiffStat(config.forkPath, baseRef, branch);
|
|
4209
4358
|
const metaByPath = new Map(detection.files.map((f) => [f.path, f]));
|
|
4210
|
-
|
|
4211
|
-
const fileMeta = metaByPath.get(
|
|
4212
|
-
|
|
4213
|
-
path
|
|
4214
|
-
ref: branch,
|
|
4215
|
-
deleted: false,
|
|
4216
|
-
created: fileMeta?.kind === "created",
|
|
4217
|
-
status: fileMeta?.status,
|
|
4218
|
-
changedAt: fileMeta?.changedAt,
|
|
4219
|
-
changedTs: fileMeta?.changedTs,
|
|
4220
|
-
additions: stat.get(path4)?.additions ?? null,
|
|
4221
|
-
deletions: stat.get(path4)?.deletions ?? null,
|
|
4222
|
-
checked: false
|
|
4223
|
-
});
|
|
4224
|
-
}
|
|
4225
|
-
for (const path4 of detection.deleted) {
|
|
4226
|
-
const fileMeta = metaByPath.get(path4);
|
|
4227
|
-
allItems.push({
|
|
4228
|
-
path: path4,
|
|
4359
|
+
const toItem = (path, deleted) => {
|
|
4360
|
+
const fileMeta = metaByPath.get(path);
|
|
4361
|
+
return {
|
|
4362
|
+
path,
|
|
4229
4363
|
ref: branch,
|
|
4230
|
-
deleted
|
|
4364
|
+
deleted,
|
|
4365
|
+
...deleted ? {} : { created: fileMeta?.kind === "created" },
|
|
4231
4366
|
status: fileMeta?.status,
|
|
4232
4367
|
changedAt: fileMeta?.changedAt,
|
|
4233
4368
|
changedTs: fileMeta?.changedTs,
|
|
4234
|
-
additions: stat.get(
|
|
4235
|
-
deletions: stat.get(
|
|
4369
|
+
additions: stat.get(path)?.additions ?? null,
|
|
4370
|
+
deletions: stat.get(path)?.deletions ?? null,
|
|
4236
4371
|
checked: false
|
|
4237
|
-
}
|
|
4238
|
-
}
|
|
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));
|
|
4239
4376
|
}
|
|
4240
4377
|
}
|
|
4241
4378
|
} catch (error) {
|
|
4242
4379
|
spinnerFail(`failed to pull ${forkName}`);
|
|
4243
|
-
console.info(colors_default.red(` \u2717 ${error
|
|
4380
|
+
console.info(colors_default.red(` \u2717 ${errorMessage(error)}`));
|
|
4244
4381
|
return;
|
|
4245
4382
|
}
|
|
4246
4383
|
if (allItems.length === 0) {
|
|
@@ -4251,15 +4388,13 @@ async function runContributions(config) {
|
|
|
4251
4388
|
spinnerSuccess(`${allItems.length} files from ${forkName}`);
|
|
4252
4389
|
if (!config.list && !config.diff && forkBanner) {
|
|
4253
4390
|
console.info(
|
|
4254
|
-
` ${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`)}`
|
|
4255
4392
|
);
|
|
4256
4393
|
}
|
|
4257
4394
|
if (config.diff) {
|
|
4258
4395
|
const matches = allItems.filter((i) => i.path === config.diff);
|
|
4259
4396
|
if (matches.length === 0) {
|
|
4260
|
-
|
|
4261
|
-
process.exitCode = 1;
|
|
4262
|
-
return;
|
|
4397
|
+
throw new Error(`no contribution found for path: ${config.diff}`);
|
|
4263
4398
|
}
|
|
4264
4399
|
for (const item of matches) {
|
|
4265
4400
|
const diff = gitDiffFile(config.forkPath, `${baseRef}..${item.ref}`, item.path, { dstPrefix: forkName });
|
|
@@ -4286,7 +4421,7 @@ async function runContributions(config) {
|
|
|
4286
4421
|
const status = item.status ?? "behind";
|
|
4287
4422
|
const kind = item.deleted ? "deleted" : "modified";
|
|
4288
4423
|
const changedAt = item.changedAt ?? "-";
|
|
4289
|
-
|
|
4424
|
+
writeStdout(`${forkName} ${status} ${kind} ${changedAt} ${item.path}`);
|
|
4290
4425
|
}
|
|
4291
4426
|
return;
|
|
4292
4427
|
}
|
|
@@ -4305,7 +4440,7 @@ async function runContributions(config) {
|
|
|
4305
4440
|
return;
|
|
4306
4441
|
}
|
|
4307
4442
|
createSpinner(`applying ${selected.length} files...`);
|
|
4308
|
-
const applyUnstaged =
|
|
4443
|
+
const applyUnstaged = isUpstreamRepo(config.forkPath);
|
|
4309
4444
|
let applied = 0;
|
|
4310
4445
|
const errors = [];
|
|
4311
4446
|
for (const item of selected) {
|
|
@@ -4329,7 +4464,7 @@ async function runContributions(config) {
|
|
|
4329
4464
|
}
|
|
4330
4465
|
applied++;
|
|
4331
4466
|
} catch (error) {
|
|
4332
|
-
errors.push(`${item.path}: ${error
|
|
4467
|
+
errors.push(`${item.path}: ${errorMessage(error)}`);
|
|
4333
4468
|
}
|
|
4334
4469
|
}
|
|
4335
4470
|
if (errors.length > 0) {
|
|
@@ -4343,7 +4478,7 @@ async function runContributions(config) {
|
|
|
4343
4478
|
console.info();
|
|
4344
4479
|
console.info(
|
|
4345
4480
|
colors_default.dim(
|
|
4346
|
-
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"
|
|
4347
4482
|
)
|
|
4348
4483
|
);
|
|
4349
4484
|
console.info();
|
|
@@ -4355,16 +4490,16 @@ import process8 from "process";
|
|
|
4355
4490
|
import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
|
|
4356
4491
|
|
|
4357
4492
|
// src/services/sync.ts
|
|
4358
|
-
import { spawnSync as
|
|
4493
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4359
4494
|
import { select as select3 } from "@inquirer/prompts";
|
|
4360
4495
|
|
|
4361
4496
|
// src/utils/gh.ts
|
|
4362
|
-
import { spawnSync as
|
|
4497
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4363
4498
|
function ghAvailable() {
|
|
4364
|
-
return
|
|
4499
|
+
return spawnSync3("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
4365
4500
|
}
|
|
4366
4501
|
function runGh(cwd, args) {
|
|
4367
|
-
const result =
|
|
4502
|
+
const result = spawnSync3("gh", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4368
4503
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
4369
4504
|
return { ok: result.status === 0, output };
|
|
4370
4505
|
}
|
|
@@ -4404,12 +4539,12 @@ function closePr(cwd, ref) {
|
|
|
4404
4539
|
}
|
|
4405
4540
|
|
|
4406
4541
|
// src/services/packages.ts
|
|
4407
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4408
|
-
import { join as
|
|
4542
|
+
import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
4543
|
+
import { join as join16 } from "path";
|
|
4409
4544
|
|
|
4410
4545
|
// src/services/package-usage.ts
|
|
4411
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4412
|
-
import { join as
|
|
4546
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4, statSync } from "fs";
|
|
4547
|
+
import { join as join15 } from "path";
|
|
4413
4548
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4414
4549
|
function escapeRegExp(value) {
|
|
4415
4550
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4417,41 +4552,41 @@ function escapeRegExp(value) {
|
|
|
4417
4552
|
function readSearchable(filePath) {
|
|
4418
4553
|
try {
|
|
4419
4554
|
if (statSync(filePath).size > MAX_FILE_BYTES) return "";
|
|
4420
|
-
const text =
|
|
4555
|
+
const text = readFileSync4(filePath, "utf8");
|
|
4421
4556
|
return text.includes("\0") ? "" : text;
|
|
4422
4557
|
} catch {
|
|
4423
4558
|
return "";
|
|
4424
4559
|
}
|
|
4425
4560
|
}
|
|
4426
|
-
function fileSource(forkPath,
|
|
4561
|
+
function fileSource(forkPath, path) {
|
|
4427
4562
|
let text;
|
|
4428
|
-
return { path
|
|
4563
|
+
return { path, label: path, text: () => text ??= readSearchable(join15(forkPath, path)) };
|
|
4429
4564
|
}
|
|
4430
|
-
async function readScriptsAtRef(forkPath, ref,
|
|
4565
|
+
async function readScriptsAtRef(forkPath, ref, path) {
|
|
4431
4566
|
try {
|
|
4432
|
-
return JSON.parse(await git(["show", `${ref}:${
|
|
4567
|
+
return JSON.parse(await git(["show", `${ref}:${path}`], forkPath)).scripts ?? {};
|
|
4433
4568
|
} catch {
|
|
4434
4569
|
return {};
|
|
4435
4570
|
}
|
|
4436
4571
|
}
|
|
4437
|
-
async function scriptSources(forkPath, upstreamRef, baseRef,
|
|
4572
|
+
async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
4438
4573
|
let forkScripts;
|
|
4439
4574
|
try {
|
|
4440
|
-
forkScripts = JSON.parse(
|
|
4575
|
+
forkScripts = JSON.parse(readFileSync4(join15(forkPath, path), "utf8")).scripts ?? {};
|
|
4441
4576
|
} catch {
|
|
4442
4577
|
return [];
|
|
4443
4578
|
}
|
|
4444
|
-
const baseScripts = await readScriptsAtRef(forkPath, baseRef,
|
|
4445
|
-
const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef,
|
|
4446
|
-
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 }));
|
|
4447
4582
|
}
|
|
4448
4583
|
function binNames(forkPath, location, name) {
|
|
4449
4584
|
const bareName = name.split("/").pop();
|
|
4450
|
-
for (const dir of [
|
|
4451
|
-
const manifest =
|
|
4585
|
+
for (const dir of [join15(forkPath, location), forkPath]) {
|
|
4586
|
+
const manifest = join15(dir, "node_modules", name, "package.json");
|
|
4452
4587
|
if (!existsSync10(manifest)) continue;
|
|
4453
4588
|
try {
|
|
4454
|
-
const { bin } = JSON.parse(
|
|
4589
|
+
const { bin } = JSON.parse(readFileSync4(manifest, "utf8"));
|
|
4455
4590
|
if (typeof bin === "string") return [bareName];
|
|
4456
4591
|
return bin && typeof bin === "object" ? Object.keys(bin) : [];
|
|
4457
4592
|
} catch {
|
|
@@ -4476,9 +4611,9 @@ function scriptPattern(name) {
|
|
|
4476
4611
|
async function loadForkUsage(forkPath, upstreamRef, baseRef) {
|
|
4477
4612
|
const diff = await git(["diff", "--name-only", "-z", "--diff-filter=d", upstreamRef], forkPath);
|
|
4478
4613
|
const sources = [];
|
|
4479
|
-
for (const
|
|
4480
|
-
if (isPackageJson(
|
|
4481
|
-
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));
|
|
4482
4617
|
}
|
|
4483
4618
|
const findIn = (scoped, patterns) => scoped.find((source) => patterns.some((pattern) => pattern.test(source.text())))?.label;
|
|
4484
4619
|
return {
|
|
@@ -4631,14 +4766,14 @@ function setTopLevelKey(pkg, key, value) {
|
|
|
4631
4766
|
function readPackageJson(filePath) {
|
|
4632
4767
|
if (!existsSync11(filePath)) return null;
|
|
4633
4768
|
try {
|
|
4634
|
-
return JSON.parse(
|
|
4769
|
+
return JSON.parse(readFileSync5(filePath, "utf8"));
|
|
4635
4770
|
} catch {
|
|
4636
4771
|
return null;
|
|
4637
4772
|
}
|
|
4638
4773
|
}
|
|
4639
4774
|
function writePackageJson(filePath, data) {
|
|
4640
4775
|
writeFileSync4(filePath, `${JSON.stringify(data, null, 2)}
|
|
4641
|
-
`, "
|
|
4776
|
+
`, "utf8");
|
|
4642
4777
|
}
|
|
4643
4778
|
function packageJsonPath(relativePath) {
|
|
4644
4779
|
return relativePath ? `${relativePath}/package.json` : "package.json";
|
|
@@ -4667,16 +4802,16 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4667
4802
|
const changes = [];
|
|
4668
4803
|
const kept = [];
|
|
4669
4804
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4670
|
-
const pkgPath =
|
|
4805
|
+
const pkgPath = join16(forkPath, pkgRelPath);
|
|
4671
4806
|
const forkPkg = readPackageJson(pkgPath);
|
|
4672
4807
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4673
4808
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4674
4809
|
if (!upstream) return { updated: false, changes, kept };
|
|
4675
4810
|
if (!forkPkg) {
|
|
4676
|
-
const workspaceArrived = relativePath !== "" && existsSync11(
|
|
4811
|
+
const workspaceArrived = relativePath !== "" && existsSync11(join16(forkPath, relativePath));
|
|
4677
4812
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4678
4813
|
writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4679
|
-
`, "
|
|
4814
|
+
`, "utf8");
|
|
4680
4815
|
return { updated: true, changes: ["copied from upstream (new workspace)"], kept };
|
|
4681
4816
|
}
|
|
4682
4817
|
const upstreamPkg = upstream.data;
|
|
@@ -4748,7 +4883,7 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4748
4883
|
}
|
|
4749
4884
|
async function runPackages(config, options = {}) {
|
|
4750
4885
|
createSpinner("syncing package.json files...");
|
|
4751
|
-
const keysToSync = config.settings.packageJsonSync ||
|
|
4886
|
+
const keysToSync = config.settings.packageJsonSync || DEFAULT_PACKAGE_JSON_SYNC;
|
|
4752
4887
|
const conflictedSet = new Set(options.conflictedFiles ?? []);
|
|
4753
4888
|
const baseRef = await resolveMergeBase(config.forkPath, config.upstreamRef);
|
|
4754
4889
|
const usage = baseRef ? await loadForkUsage(config.forkPath, config.upstreamRef, baseRef) : null;
|
|
@@ -4770,26 +4905,26 @@ async function runPackages(config, options = {}) {
|
|
|
4770
4905
|
"package sync complete",
|
|
4771
4906
|
`${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
|
|
4772
4907
|
);
|
|
4773
|
-
for (const { path
|
|
4908
|
+
for (const { path, changes } of reports) {
|
|
4774
4909
|
if (changes.length === 0) continue;
|
|
4775
|
-
console.info(` ${
|
|
4910
|
+
console.info(` ${path}`);
|
|
4776
4911
|
for (const change of changes) console.info(` ${colors_default.dim(change)}`);
|
|
4777
4912
|
}
|
|
4778
|
-
const kept = reports.flatMap(({ path
|
|
4913
|
+
const kept = reports.flatMap(({ path, kept: kept2 }) => kept2.map((entry) => `${path} ${entry}`));
|
|
4779
4914
|
if (kept.length > 0) {
|
|
4780
4915
|
console.info();
|
|
4781
|
-
console.
|
|
4782
|
-
`${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"}:`)}`
|
|
4783
4918
|
);
|
|
4784
|
-
for (const entry of kept) console.
|
|
4919
|
+
for (const entry of kept) console.info(` ${colors_default.dim("\u2192")} ${entry}`);
|
|
4785
4920
|
}
|
|
4786
4921
|
if (skipped.length > 0) {
|
|
4787
4922
|
console.info();
|
|
4788
|
-
console.
|
|
4789
|
-
`${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\`:`)}`
|
|
4790
4925
|
);
|
|
4791
|
-
for (const
|
|
4792
|
-
console.
|
|
4926
|
+
for (const path of skipped) {
|
|
4927
|
+
console.info(` ${colors_default.dim("\u2192")} ${path}`);
|
|
4793
4928
|
}
|
|
4794
4929
|
}
|
|
4795
4930
|
}
|
|
@@ -4797,14 +4932,14 @@ async function runPackages(config, options = {}) {
|
|
|
4797
4932
|
// src/services/sync.ts
|
|
4798
4933
|
async function resolveCycleStart(forkPath, base, currentBranch) {
|
|
4799
4934
|
if (!await branchExists(forkPath, base)) {
|
|
4800
|
-
await
|
|
4935
|
+
await fetchRemote(forkPath, "origin").catch(() => {
|
|
4801
4936
|
});
|
|
4802
|
-
console.info(colors_default.dim(`'${base}' has no local branch
|
|
4937
|
+
console.info(colors_default.dim(`'${base}' has no local branch: cutting from 'origin/${base}'.`));
|
|
4803
4938
|
return `origin/${base}`;
|
|
4804
4939
|
}
|
|
4805
4940
|
const { upstream, ahead, behind } = await getUpstreamStatus(forkPath, base);
|
|
4806
4941
|
if (!upstream) {
|
|
4807
|
-
console.info(colors_default.dim(`'${base}' has no upstream
|
|
4942
|
+
console.info(colors_default.dim(`'${base}' has no upstream: skipping the up-to-date check.`));
|
|
4808
4943
|
return base;
|
|
4809
4944
|
}
|
|
4810
4945
|
if (ahead > 0 && behind > 0) {
|
|
@@ -4817,7 +4952,7 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
|
|
|
4817
4952
|
const worktree = currentBranch === base ? null : await getBranchWorktree(forkPath, base);
|
|
4818
4953
|
if (worktree) {
|
|
4819
4954
|
console.info(
|
|
4820
|
-
colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree}
|
|
4955
|
+
colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree}; leaving it as it is.`)
|
|
4821
4956
|
);
|
|
4822
4957
|
return upstream;
|
|
4823
4958
|
}
|
|
@@ -4827,7 +4962,7 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
|
|
|
4827
4962
|
return base;
|
|
4828
4963
|
}
|
|
4829
4964
|
if (ahead > 0) {
|
|
4830
|
-
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.`));
|
|
4831
4966
|
}
|
|
4832
4967
|
return base;
|
|
4833
4968
|
}
|
|
@@ -4852,7 +4987,7 @@ async function resolveCutBase(forkPath, base) {
|
|
|
4852
4987
|
async function returnToBase(forkPath, base) {
|
|
4853
4988
|
const worktree = await getBranchWorktree(forkPath, base);
|
|
4854
4989
|
if (worktree) {
|
|
4855
|
-
console.info(colors_default.dim(`'${base}' is checked out at ${worktree}
|
|
4990
|
+
console.info(colors_default.dim(`'${base}' is checked out at ${worktree}; detaching at '${base}' here instead...`));
|
|
4856
4991
|
await switchDetached(forkPath, base);
|
|
4857
4992
|
return `detached at '${base}'`;
|
|
4858
4993
|
}
|
|
@@ -4864,30 +4999,24 @@ async function returnToBase(forkPath, base) {
|
|
|
4864
4999
|
}
|
|
4865
5000
|
async function runSync(config, options) {
|
|
4866
5001
|
createSpinner("starting sync...");
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
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();
|
|
4875
5010
|
}
|
|
4876
|
-
|
|
5011
|
+
throw error;
|
|
5012
|
+
}
|
|
4877
5013
|
if (result.success) {
|
|
4878
5014
|
spinnerSuccess();
|
|
4879
5015
|
} else {
|
|
4880
5016
|
spinnerFail("sync completed with conflicts");
|
|
4881
5017
|
}
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
printUpstreamOverrideChanges(result);
|
|
4885
|
-
printMaskingPinWarning(result.files);
|
|
4886
|
-
if (config.logFile) {
|
|
4887
|
-
const logPath = writeLogFile(config.forkPath, result.files);
|
|
4888
|
-
console.info();
|
|
4889
|
-
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
4890
|
-
}
|
|
5018
|
+
printEngineReports(result, "merge summary");
|
|
5019
|
+
printLogFileReport(config, result.files);
|
|
4891
5020
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
|
|
4892
5021
|
printSyncComplete(result, { stagedBranch });
|
|
4893
5022
|
printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
|
|
@@ -4895,7 +5024,6 @@ async function runSync(config, options) {
|
|
|
4895
5024
|
return result;
|
|
4896
5025
|
}
|
|
4897
5026
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
4898
|
-
var PR_BODY_COMMIT_MAX = 50;
|
|
4899
5027
|
async function buildSyncCommitMessage(forkPath, upstreamRef) {
|
|
4900
5028
|
const shortSha = await getShortSha(forkPath, upstreamRef).catch(() => "");
|
|
4901
5029
|
if (!shortSha) return SYNC_PR_TITLE;
|
|
@@ -4947,9 +5075,8 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
4947
5075
|
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
4948
5076
|
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
4949
5077
|
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, {
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
limit: PR_BODY_COMMIT_MAX
|
|
5078
|
+
skip: totalCount > COMMIT_LIST_MAX ? totalCount - COMMIT_LIST_MAX : 0,
|
|
5079
|
+
limit: COMMIT_LIST_MAX
|
|
4953
5080
|
}) : [];
|
|
4954
5081
|
const notes = await readPrBodyNotes(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
4955
5082
|
return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount, notes });
|
|
@@ -4973,11 +5100,8 @@ function printShipSteps(temporaryBranch, base, title) {
|
|
|
4973
5100
|
console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
|
|
4974
5101
|
printPrCreateStep(temporaryBranch, base, title);
|
|
4975
5102
|
}
|
|
4976
|
-
function printFinishSteps() {
|
|
4977
|
-
console.info(colors_default.dim(" pnpm cella sync"));
|
|
4978
|
-
}
|
|
4979
5103
|
function hasStagedSyncChanges(result) {
|
|
4980
|
-
return result.files.some((file) =>
|
|
5104
|
+
return result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
4981
5105
|
}
|
|
4982
5106
|
function extractFirstUrl(output) {
|
|
4983
5107
|
return output.match(/https?:\/\/\S+/)?.[0];
|
|
@@ -4988,7 +5112,7 @@ async function flattenSyncBranch(forkPath, branch, base) {
|
|
|
4988
5112
|
const message = await buildSyncCommitMessage(forkPath, `${mergeCommits[0]}^2`);
|
|
4989
5113
|
console.info(
|
|
4990
5114
|
colors_default.yellow(
|
|
4991
|
-
`'${branch}' contains ${mergeCommits.length} merge commit(s)
|
|
5115
|
+
`'${branch}' contains ${mergeCommits.length} merge commit(s): the PR would list the entire upstream history.`
|
|
4992
5116
|
)
|
|
4993
5117
|
);
|
|
4994
5118
|
console.info(colors_default.dim(`flattening '${branch}' to a single commit (same content)...`));
|
|
@@ -5011,8 +5135,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
5011
5135
|
try {
|
|
5012
5136
|
await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
|
|
5013
5137
|
} catch (error) {
|
|
5014
|
-
|
|
5015
|
-
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:`));
|
|
5016
5139
|
printShipSteps(branch, base, prTitle);
|
|
5017
5140
|
return;
|
|
5018
5141
|
}
|
|
@@ -5020,9 +5143,9 @@ async function shipSyncBranch(config, branch) {
|
|
|
5020
5143
|
console.info(colors_default.dim("opening a pull request..."));
|
|
5021
5144
|
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
5022
5145
|
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
5023
|
-
const pr =
|
|
5146
|
+
const pr = spawnSync4("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
5024
5147
|
cwd: forkPath,
|
|
5025
|
-
encoding: "
|
|
5148
|
+
encoding: "utf8",
|
|
5026
5149
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5027
5150
|
});
|
|
5028
5151
|
prUrl = extractFirstUrl(`${pr.stdout ?? ""}
|
|
@@ -5034,16 +5157,16 @@ ${pr.stderr ?? ""}`);
|
|
|
5034
5157
|
printPrCreateStep(branch, base, prTitle);
|
|
5035
5158
|
}
|
|
5036
5159
|
} else {
|
|
5037
|
-
console.info(colors_default.yellow("`gh` not found
|
|
5160
|
+
console.info(colors_default.yellow("`gh` not found, open the PR manually:"));
|
|
5038
5161
|
printPrCreateStep(branch, base, prTitle);
|
|
5039
5162
|
}
|
|
5040
5163
|
const position = await returnToBase(forkPath, base);
|
|
5041
5164
|
console.info();
|
|
5042
5165
|
if (prUrl) {
|
|
5043
|
-
console.info(`${
|
|
5166
|
+
console.info(`${checkMark} sync pull request ${prOpened ? "opened" : "ready"}`);
|
|
5044
5167
|
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, ${position}`));
|
|
5045
5168
|
} else {
|
|
5046
|
-
console.info(`${
|
|
5169
|
+
console.info(`${checkMark} sync branch pushed`);
|
|
5047
5170
|
console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
|
|
5048
5171
|
}
|
|
5049
5172
|
await printMigrationNotesLine(config);
|
|
@@ -5051,23 +5174,43 @@ ${pr.stderr ?? ""}`);
|
|
|
5051
5174
|
function finalizeWorkspace(forkPath) {
|
|
5052
5175
|
for (const args of [["install"], ["check"]]) {
|
|
5053
5176
|
console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
|
|
5054
|
-
const result =
|
|
5177
|
+
const result = spawnSync4("pnpm", args, { cwd: forkPath, stdio: "inherit" });
|
|
5055
5178
|
if (result.status !== 0) return false;
|
|
5056
5179
|
}
|
|
5057
5180
|
return true;
|
|
5058
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
|
+
}
|
|
5059
5195
|
async function runSyncCycle(config) {
|
|
5060
5196
|
const { forkPath } = config;
|
|
5061
5197
|
const branch = await setupTemporarySyncBranch(config);
|
|
5062
|
-
|
|
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
|
+
}
|
|
5063
5208
|
if (config.settings.syncWithPackages !== false) {
|
|
5064
5209
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
5065
5210
|
}
|
|
5066
5211
|
if (result.conflicts.length > 0) return { status: "conflicts", branch };
|
|
5067
5212
|
if (!await mergeInProgress(forkPath)) {
|
|
5068
|
-
|
|
5069
|
-
else await switchBranch(forkPath, branch.startBranch);
|
|
5070
|
-
await deleteBranch(forkPath, branch.temporaryBranch);
|
|
5213
|
+
await discardTemporarySyncBranch(forkPath, branch);
|
|
5071
5214
|
return { status: "noop" };
|
|
5072
5215
|
}
|
|
5073
5216
|
return { status: "staged", branch };
|
|
@@ -5112,15 +5255,15 @@ async function mergeOpenSyncPrs(config, open) {
|
|
|
5112
5255
|
const merged = mergePrSquash(forkPath, newest.number, { deleteBranch: true });
|
|
5113
5256
|
if (!merged.ok) {
|
|
5114
5257
|
console.info();
|
|
5115
|
-
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\`:`));
|
|
5116
5259
|
if (merged.output) console.info(colors_default.dim(indentLines(merged.output)));
|
|
5117
5260
|
console.info(colors_default.dim(` ${newest.url}`));
|
|
5118
5261
|
return "cancel";
|
|
5119
5262
|
}
|
|
5120
|
-
console.info(`${
|
|
5263
|
+
console.info(`${checkMark} merged #${newest.number}`);
|
|
5121
5264
|
await deleteBranch(forkPath, newest.headRefName);
|
|
5122
5265
|
for (const pr of superseded) {
|
|
5123
|
-
console.info(colors_default.dim(`closing #${pr.number}
|
|
5266
|
+
console.info(colors_default.dim(`closing #${pr.number}: its changes are included in #${newest.number}...`));
|
|
5124
5267
|
closePr(forkPath, pr.number);
|
|
5125
5268
|
await deleteBranch(forkPath, pr.headRefName);
|
|
5126
5269
|
}
|
|
@@ -5185,15 +5328,20 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5185
5328
|
const outcome = await runSyncCycle(config);
|
|
5186
5329
|
console.info();
|
|
5187
5330
|
if (outcome.status === "noop") {
|
|
5188
|
-
|
|
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
|
+
}
|
|
5189
5337
|
return;
|
|
5190
5338
|
}
|
|
5191
5339
|
const { temporaryBranch } = outcome.branch;
|
|
5192
5340
|
if (outcome.status === "conflicts") {
|
|
5193
5341
|
console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve and stage them, then:`)}`);
|
|
5194
|
-
|
|
5342
|
+
console.info(colors_default.dim(" pnpm cella sync"));
|
|
5195
5343
|
console.info(colors_default.dim(" rerun commits the sync and stops for drift triage; a further rerun ships (push + PR)."));
|
|
5196
|
-
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."));
|
|
5197
5345
|
return;
|
|
5198
5346
|
}
|
|
5199
5347
|
await commitSyncMerge(config, temporaryBranch);
|
|
@@ -5201,12 +5349,11 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5201
5349
|
|
|
5202
5350
|
// src/services/forks.ts
|
|
5203
5351
|
async function gatherForkStatus(forkPath) {
|
|
5204
|
-
const [branch,
|
|
5352
|
+
const [branch, dirty, syncRef] = await Promise.all([
|
|
5205
5353
|
getCurrentBranch(forkPath).catch(() => "unknown"),
|
|
5206
|
-
|
|
5354
|
+
getWorkingTreeChangeCount(forkPath).catch(() => 0),
|
|
5207
5355
|
getStoredSyncRef(forkPath)
|
|
5208
5356
|
]);
|
|
5209
|
-
const dirty = porcelain ? porcelain.split("\n").filter(Boolean).length : 0;
|
|
5210
5357
|
let lastSync = null;
|
|
5211
5358
|
if (syncRef) {
|
|
5212
5359
|
const commitInfo = await getCommitInfo(forkPath, syncRef).catch(() => null);
|
|
@@ -5269,7 +5416,8 @@ async function syncFork(config, fork, forkPath) {
|
|
|
5269
5416
|
list: false,
|
|
5270
5417
|
json: false,
|
|
5271
5418
|
verbose: config.verbose,
|
|
5272
|
-
hard: config.hard
|
|
5419
|
+
hard: config.hard,
|
|
5420
|
+
keepConfig: config.keepConfig
|
|
5273
5421
|
};
|
|
5274
5422
|
await runSyncCommand(forkRuntimeConfig);
|
|
5275
5423
|
}
|
|
@@ -5282,8 +5430,7 @@ async function runForks(config) {
|
|
|
5282
5430
|
if (config.fork) {
|
|
5283
5431
|
const match = forks.find((f) => f.name === config.fork);
|
|
5284
5432
|
if (!match) {
|
|
5285
|
-
|
|
5286
|
-
return;
|
|
5433
|
+
throw new Error(`fork '${config.fork}' not found in config`);
|
|
5287
5434
|
}
|
|
5288
5435
|
const resolvedPath = resolve7(await resolveForkBasePath(config.forkPath), match.localPath);
|
|
5289
5436
|
await syncFork(config, match, resolvedPath);
|
|
@@ -5292,7 +5439,7 @@ async function runForks(config) {
|
|
|
5292
5439
|
const forkBasePath = await resolveForkBasePath(config.forkPath);
|
|
5293
5440
|
while (true) {
|
|
5294
5441
|
const choices = await buildForkChoices(forks, forkBasePath);
|
|
5295
|
-
const forkChoices = [...choices, new Separator2(
|
|
5442
|
+
const forkChoices = [...choices, new Separator2(MENU_DIVIDER), { value: "_exit", name: colors_default.dim("exit") }];
|
|
5296
5443
|
const selectedPath = await select4({
|
|
5297
5444
|
message: "select fork to sync:",
|
|
5298
5445
|
choices: forkChoices,
|
|
@@ -5314,15 +5461,15 @@ async function runForks(config) {
|
|
|
5314
5461
|
|
|
5315
5462
|
// src/services/stats.ts
|
|
5316
5463
|
import { readFile as readFile3 } from "fs/promises";
|
|
5317
|
-
import { join as
|
|
5464
|
+
import { join as join18 } from "path";
|
|
5318
5465
|
|
|
5319
5466
|
// src/utils/coverage-utils.ts
|
|
5320
|
-
import { spawnSync as
|
|
5321
|
-
import
|
|
5322
|
-
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";
|
|
5323
5470
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5324
5471
|
function summaryPath(forkPath) {
|
|
5325
|
-
return
|
|
5472
|
+
return join17(forkPath, ".coverage", "coverage-summary.json");
|
|
5326
5473
|
}
|
|
5327
5474
|
function colorPct(pct) {
|
|
5328
5475
|
if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
|
|
@@ -5341,7 +5488,7 @@ function formatAge(ms) {
|
|
|
5341
5488
|
}
|
|
5342
5489
|
function refreshCoverage(forkPath) {
|
|
5343
5490
|
console.info(colors_default.dim("running `pnpm test` (this spins up Docker and runs the full suite)..."));
|
|
5344
|
-
const result =
|
|
5491
|
+
const result = spawnSync5("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
|
|
5345
5492
|
return result.status === 0;
|
|
5346
5493
|
}
|
|
5347
5494
|
function printCoverageSummary(forkPath, options = {}) {
|
|
@@ -5349,24 +5496,24 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5349
5496
|
if (options.refresh) {
|
|
5350
5497
|
const ok = refreshCoverage(forkPath);
|
|
5351
5498
|
if (!ok) {
|
|
5352
|
-
console.info(colors_default.red("coverage run failed
|
|
5499
|
+
console.info(colors_default.red("coverage run failed: showing last cached results (if any)"));
|
|
5353
5500
|
}
|
|
5354
5501
|
console.info();
|
|
5355
5502
|
}
|
|
5356
|
-
if (!
|
|
5503
|
+
if (!existsSync12(file)) {
|
|
5357
5504
|
console.info(colors_default.bold("coverage"));
|
|
5358
|
-
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"));
|
|
5359
5506
|
console.info();
|
|
5360
5507
|
return;
|
|
5361
5508
|
}
|
|
5362
5509
|
let summary;
|
|
5363
5510
|
let mtimeMs;
|
|
5364
5511
|
try {
|
|
5365
|
-
summary = JSON.parse(
|
|
5366
|
-
mtimeMs =
|
|
5512
|
+
summary = JSON.parse(readFileSync6(file, "utf8"));
|
|
5513
|
+
mtimeMs = statSync2(file).mtimeMs;
|
|
5367
5514
|
} catch {
|
|
5368
5515
|
console.info(colors_default.bold("coverage"));
|
|
5369
|
-
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"));
|
|
5370
5517
|
console.info();
|
|
5371
5518
|
return;
|
|
5372
5519
|
}
|
|
@@ -5375,9 +5522,9 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5375
5522
|
const perPackage = /* @__PURE__ */ new Map();
|
|
5376
5523
|
for (const [key, entry] of Object.entries(summary)) {
|
|
5377
5524
|
if (key === "total") continue;
|
|
5378
|
-
const rel =
|
|
5525
|
+
const rel = relative3(forkPath, key);
|
|
5379
5526
|
if (rel.startsWith("..")) continue;
|
|
5380
|
-
const pkg = rel.split(
|
|
5527
|
+
const pkg = rel.split(sep2)[0];
|
|
5381
5528
|
const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
|
|
5382
5529
|
acc.covered += entry.lines.covered;
|
|
5383
5530
|
acc.total += entry.lines.total;
|
|
@@ -5386,10 +5533,10 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5386
5533
|
const ageLabel = formatAge(ageMs);
|
|
5387
5534
|
console.info(colors_default.bold("coverage"), colors_default.dim(`(updated ${ageLabel})`));
|
|
5388
5535
|
if (stale) {
|
|
5389
|
-
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"));
|
|
5390
5537
|
}
|
|
5391
5538
|
if (!Object.keys(summary).some((key) => key !== "total")) {
|
|
5392
|
-
console.info(colors_default.yellow(" no coverage data found
|
|
5539
|
+
console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to regenerate"));
|
|
5393
5540
|
console.info();
|
|
5394
5541
|
return;
|
|
5395
5542
|
}
|
|
@@ -5408,7 +5555,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5408
5555
|
// src/services/stats.ts
|
|
5409
5556
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
5410
5557
|
async function getWorkspacePackages(forkPath) {
|
|
5411
|
-
const content = await readFile3(
|
|
5558
|
+
const content = await readFile3(join18(forkPath, "pnpm-workspace.yaml"), "utf8");
|
|
5412
5559
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
5413
5560
|
if (pattern.endsWith("/*")) {
|
|
5414
5561
|
const base = pattern.slice(0, -2);
|
|
@@ -5472,12 +5619,12 @@ async function collectStats(forkPath) {
|
|
|
5472
5619
|
const categories = emptyCat();
|
|
5473
5620
|
const pkgStats = /* @__PURE__ */ new Map();
|
|
5474
5621
|
let totalLoc = 0;
|
|
5475
|
-
for (const { path
|
|
5476
|
-
const category = classifyFile(
|
|
5622
|
+
for (const { path, loc } of entries) {
|
|
5623
|
+
const category = classifyFile(path);
|
|
5477
5624
|
categories[category].files++;
|
|
5478
5625
|
categories[category].loc += loc;
|
|
5479
5626
|
totalLoc += loc;
|
|
5480
|
-
const pkg = matchPackage(
|
|
5627
|
+
const pkg = matchPackage(path, packages);
|
|
5481
5628
|
let entry = pkgStats.get(pkg);
|
|
5482
5629
|
if (!entry) {
|
|
5483
5630
|
entry = { total: 0, loc: 0, categories: emptyCat() };
|
|
@@ -5554,17 +5701,237 @@ function printStats(stats, verbose) {
|
|
|
5554
5701
|
}
|
|
5555
5702
|
console.info();
|
|
5556
5703
|
}
|
|
5557
|
-
async function runStats(
|
|
5704
|
+
async function runStats(config) {
|
|
5705
|
+
const { forkPath } = config;
|
|
5558
5706
|
createSpinner("counting files...");
|
|
5559
5707
|
const stats = await collectStats(forkPath);
|
|
5560
|
-
spinnerSuccess("
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
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) {
|
|
5565
5899
|
console.info();
|
|
5566
|
-
|
|
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
|
+
}
|
|
5567
5904
|
}
|
|
5905
|
+
if (verbose) {
|
|
5906
|
+
console.info();
|
|
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;
|
|
5933
|
+
}
|
|
5934
|
+
printBranchStats(stats, options.verbose ?? false);
|
|
5568
5935
|
}
|
|
5569
5936
|
|
|
5570
5937
|
// src/cella-cli.ts
|
|
@@ -5583,8 +5950,8 @@ function getForkPath() {
|
|
|
5583
5950
|
}
|
|
5584
5951
|
return cwd;
|
|
5585
5952
|
}
|
|
5586
|
-
|
|
5587
|
-
if (!existsSync13(
|
|
5953
|
+
function preflight(forkPath) {
|
|
5954
|
+
if (!existsSync13(join19(forkPath, ".git"))) {
|
|
5588
5955
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5589
5956
|
}
|
|
5590
5957
|
}
|
|
@@ -5602,7 +5969,7 @@ async function main() {
|
|
|
5602
5969
|
const userConfig = await loadConfig(forkPath);
|
|
5603
5970
|
const config = await parseCli(userConfig, forkPath);
|
|
5604
5971
|
if (!["audit", "forks", "contributions", "stats"].includes(config.service)) {
|
|
5605
|
-
|
|
5972
|
+
preflight(forkPath);
|
|
5606
5973
|
}
|
|
5607
5974
|
switch (config.service) {
|
|
5608
5975
|
case "analyze": {
|
|
@@ -5613,26 +5980,36 @@ async function main() {
|
|
|
5613
5980
|
await runSyncCommand(config);
|
|
5614
5981
|
break;
|
|
5615
5982
|
}
|
|
5616
|
-
case "migrate":
|
|
5983
|
+
case "migrate": {
|
|
5617
5984
|
await runMigrate(config);
|
|
5618
5985
|
break;
|
|
5619
|
-
|
|
5620
|
-
|
|
5986
|
+
}
|
|
5987
|
+
case "audit": {
|
|
5988
|
+
await runAudit(config);
|
|
5621
5989
|
break;
|
|
5622
|
-
|
|
5990
|
+
}
|
|
5991
|
+
case "forks": {
|
|
5623
5992
|
await runForks(config);
|
|
5624
5993
|
break;
|
|
5625
|
-
|
|
5994
|
+
}
|
|
5995
|
+
case "contributions": {
|
|
5626
5996
|
await runContributions(config);
|
|
5627
5997
|
break;
|
|
5628
|
-
|
|
5629
|
-
|
|
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
|
+
}
|
|
5630
6006
|
break;
|
|
6007
|
+
}
|
|
5631
6008
|
}
|
|
5632
6009
|
console.info();
|
|
5633
6010
|
} catch (error) {
|
|
5634
6011
|
console.error();
|
|
5635
|
-
console.error(`${colors_default.red("\u2717")} ${error
|
|
6012
|
+
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
5636
6013
|
process9.exit(1);
|
|
5637
6014
|
}
|
|
5638
6015
|
}
|