@cellajs/cli 0.3.1 → 0.3.3
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 +14 -22
- package/dist/config.js +13 -14
- package/dist/config.js.map +1 -1
- package/dist/index.js +1071 -692
- 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.3",
|
|
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,12 +989,13 @@ 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) {
|
|
972
996
|
await git(["fetch", remoteName, "--no-tags", `+refs/tags/*:refs/${remoteName}/tags/*`], cwd);
|
|
973
997
|
}
|
|
998
|
+
var releaseTagPattern = /^v?\d[\w.+-]*$/;
|
|
974
999
|
async function resolveLatestReleaseTag(cwd, remoteName) {
|
|
975
1000
|
const prefix = `refs/${remoteName}/tags/`;
|
|
976
1001
|
const output = await git(["for-each-ref", "--sort=-version:refname", "--format=%(refname)", prefix], cwd, {
|
|
@@ -981,7 +1006,7 @@ async function resolveLatestReleaseTag(cwd, remoteName) {
|
|
|
981
1006
|
const ref = line.trim();
|
|
982
1007
|
if (!ref) continue;
|
|
983
1008
|
const tag = ref.slice(prefix.length);
|
|
984
|
-
if (
|
|
1009
|
+
if (releaseTagPattern.test(tag)) return { tag, ref };
|
|
985
1010
|
}
|
|
986
1011
|
return null;
|
|
987
1012
|
}
|
|
@@ -991,7 +1016,7 @@ async function resolveUpstreamCommit(cwd, remoteName, ref) {
|
|
|
991
1016
|
for (const candidate of [`refs/remotes/${remoteName}/${ref}`, tagRef, ref]) {
|
|
992
1017
|
const sha = await git(["rev-parse", "-q", "--verify", `${candidate}^{commit}`], cwd, { ignoreErrors: true });
|
|
993
1018
|
if (!sha) continue;
|
|
994
|
-
const release = candidate === tagRef &&
|
|
1019
|
+
const release = candidate === tagRef && releaseTagPattern.test(ref) ? { tag: ref, ref: tagRef } : void 0;
|
|
995
1020
|
return { sha, release };
|
|
996
1021
|
}
|
|
997
1022
|
return null;
|
|
@@ -1005,7 +1030,8 @@ async function isPublishedUpstream(cwd, remoteName, branchRef, sha) {
|
|
|
1005
1030
|
"--contains",
|
|
1006
1031
|
sha,
|
|
1007
1032
|
`refs/remotes/${branchRef}`,
|
|
1008
|
-
`refs/${remoteName}/tags/
|
|
1033
|
+
`refs/${remoteName}/tags/[0-9]*`,
|
|
1034
|
+
`refs/${remoteName}/tags/v[0-9]*`
|
|
1009
1035
|
],
|
|
1010
1036
|
cwd,
|
|
1011
1037
|
{ ignoreErrors: true }
|
|
@@ -1023,10 +1049,8 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
|
1023
1049
|
return Number.parseInt(output.trim(), 10) || 0;
|
|
1024
1050
|
}
|
|
1025
1051
|
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}`);
|
|
1052
|
+
const { skip, limit } = options;
|
|
1053
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
|
|
1030
1054
|
const output = await git(args, cwd, { ignoreErrors: true });
|
|
1031
1055
|
if (!output) return [];
|
|
1032
1056
|
const commits = [];
|
|
@@ -1036,14 +1060,8 @@ async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
|
1036
1060
|
if (!hash || !message || !date) continue;
|
|
1037
1061
|
commits.push({ hash, message, date });
|
|
1038
1062
|
}
|
|
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);
|
|
1063
|
+
const start = Math.max(0, skip ?? 0);
|
|
1064
|
+
return limit && limit > 0 ? commits.slice(start, start + limit) : commits.slice(start);
|
|
1047
1065
|
}
|
|
1048
1066
|
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
1049
1067
|
const info = /* @__PURE__ */ new Map();
|
|
@@ -1098,12 +1116,9 @@ async function listWorktrees(cwd) {
|
|
|
1098
1116
|
}
|
|
1099
1117
|
return paths;
|
|
1100
1118
|
}
|
|
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");
|
|
1119
|
+
async function merge(cwd, ref) {
|
|
1105
1120
|
try {
|
|
1106
|
-
await git(
|
|
1121
|
+
await git(["merge", "--no-ff", "--no-commit", "--no-edit", ref], cwd, { skipEditor: true });
|
|
1107
1122
|
return { success: true, conflicts: [] };
|
|
1108
1123
|
} catch (error) {
|
|
1109
1124
|
const conflicts = await getConflictedFiles(cwd);
|
|
@@ -1131,6 +1146,22 @@ async function checkoutFromRef(cwd, ref, filePath) {
|
|
|
1131
1146
|
async function restoreWorktreeFromRef(cwd, ref, filePath) {
|
|
1132
1147
|
await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
|
|
1133
1148
|
}
|
|
1149
|
+
async function batchRestoreWorktreeFromRef(cwd, ref, filePaths) {
|
|
1150
|
+
const batchSize = 200;
|
|
1151
|
+
for (let i = 0; i < filePaths.length; i += batchSize) {
|
|
1152
|
+
const batch = filePaths.slice(i, i + batchSize);
|
|
1153
|
+
await git(["--literal-pathspecs", "restore", `--source=${ref}`, "--worktree", "--", ...batch], cwd);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async function listIdenticalToRef(cwd, ref) {
|
|
1157
|
+
const nulSeparated = (out) => out.split("\0").filter(Boolean);
|
|
1158
|
+
const [tracked, changed] = await Promise.all([
|
|
1159
|
+
git(["ls-tree", "-r", "--name-only", "-z", ref], cwd),
|
|
1160
|
+
git(["diff", "--name-only", "--no-renames", "-z", ref, "--"], cwd)
|
|
1161
|
+
]);
|
|
1162
|
+
const differs = new Set(nulSeparated(changed));
|
|
1163
|
+
return new Set(nulSeparated(tracked).filter((path) => !differs.has(path)));
|
|
1164
|
+
}
|
|
1134
1165
|
async function getMergeBase(cwd, ref1, ref2) {
|
|
1135
1166
|
return git(["merge-base", ref1, ref2], cwd);
|
|
1136
1167
|
}
|
|
@@ -1140,6 +1171,7 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1140
1171
|
});
|
|
1141
1172
|
const changes = /* @__PURE__ */ new Map();
|
|
1142
1173
|
if (!output) return changes;
|
|
1174
|
+
const hashOrNull = (hash) => hash === "0".repeat(40) ? null : hash;
|
|
1143
1175
|
for (const line of output.split("\n")) {
|
|
1144
1176
|
if (!line) continue;
|
|
1145
1177
|
const renameMatch = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) R\d*\s+(.+)\t(.+)$/);
|
|
@@ -1147,10 +1179,9 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1147
1179
|
const [, baseHash, targetHash, oldPath, newPath] = renameMatch;
|
|
1148
1180
|
changes.set(newPath, {
|
|
1149
1181
|
status: "R",
|
|
1150
|
-
baseHash: baseHash
|
|
1151
|
-
targetHash: targetHash
|
|
1152
|
-
oldPath
|
|
1153
|
-
newPath
|
|
1182
|
+
baseHash: hashOrNull(baseHash),
|
|
1183
|
+
targetHash: hashOrNull(targetHash),
|
|
1184
|
+
oldPath
|
|
1154
1185
|
});
|
|
1155
1186
|
continue;
|
|
1156
1187
|
}
|
|
@@ -1159,8 +1190,8 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1159
1190
|
const [, baseHash, targetHash, status, filePath] = match;
|
|
1160
1191
|
changes.set(filePath, {
|
|
1161
1192
|
status,
|
|
1162
|
-
baseHash: baseHash
|
|
1163
|
-
targetHash: targetHash
|
|
1193
|
+
baseHash: hashOrNull(baseHash),
|
|
1194
|
+
targetHash: hashOrNull(targetHash)
|
|
1164
1195
|
});
|
|
1165
1196
|
}
|
|
1166
1197
|
}
|
|
@@ -1174,9 +1205,9 @@ async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
|
1174
1205
|
for (const line of out.split("\n")) {
|
|
1175
1206
|
if (!line.trim()) continue;
|
|
1176
1207
|
const [a, d, ...rest] = line.split(" ");
|
|
1177
|
-
const
|
|
1178
|
-
if (!
|
|
1179
|
-
stat.set(
|
|
1208
|
+
const path = rest.join(" ");
|
|
1209
|
+
if (!path) continue;
|
|
1210
|
+
stat.set(path, {
|
|
1180
1211
|
additions: a === "-" ? null : Number(a),
|
|
1181
1212
|
deletions: d === "-" ? null : Number(d)
|
|
1182
1213
|
});
|
|
@@ -1258,8 +1289,8 @@ async function storeLastSyncRef(cwd, upstreamHash) {
|
|
|
1258
1289
|
await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
|
|
1259
1290
|
}
|
|
1260
1291
|
}
|
|
1261
|
-
async function stagePath(cwd,
|
|
1262
|
-
await git(["add", "--",
|
|
1292
|
+
async function stagePath(cwd, path) {
|
|
1293
|
+
await git(["add", "--", path], cwd, { ignoreErrors: true });
|
|
1263
1294
|
}
|
|
1264
1295
|
async function getStoredSyncRef(cwd) {
|
|
1265
1296
|
const ref = await git(["rev-parse", "refs/cella/last-sync"], cwd, { ignoreErrors: true });
|
|
@@ -1272,14 +1303,7 @@ async function getStoredSyncHead(cwd) {
|
|
|
1272
1303
|
async function readManifestAtRef(cwd, ref) {
|
|
1273
1304
|
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
1274
1305
|
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
|
-
}
|
|
1306
|
+
return parseSyncManifest(raw);
|
|
1283
1307
|
}
|
|
1284
1308
|
async function readManifestBaseAtRef(cwd, ref) {
|
|
1285
1309
|
const manifest = await readManifestAtRef(cwd, ref);
|
|
@@ -1400,8 +1424,8 @@ async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
|
1400
1424
|
throw new Error(
|
|
1401
1425
|
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1402
1426
|
|
|
1403
|
-
This fork has unrelated history with upstream
|
|
1404
|
-
upstream squashed its history
|
|
1427
|
+
This fork has unrelated history with upstream (typical for a create-cella scaffold, or after
|
|
1428
|
+
upstream squashed its history) and no recorded or inferable base commit exists after fetching
|
|
1405
1429
|
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1406
1430
|
|
|
1407
1431
|
git update-ref refs/cella/last-sync <upstream-sha>
|
|
@@ -1460,9 +1484,15 @@ async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
|
1460
1484
|
}
|
|
1461
1485
|
|
|
1462
1486
|
// src/utils/migration-notes.ts
|
|
1487
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1488
|
+
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1489
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1463
1490
|
var NOTES_DIR = "cella/migrations";
|
|
1464
1491
|
var PENDING_FILE = "cella/cella.migrations.json";
|
|
1465
1492
|
var NOTE_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1493
|
+
function isCodemodFile(name) {
|
|
1494
|
+
return name.endsWith(".ts") && !name.endsWith(".test.ts");
|
|
1495
|
+
}
|
|
1466
1496
|
function isUpstreamOnly(filePath) {
|
|
1467
1497
|
return filePath === NOTES_DIR || filePath.startsWith(`${NOTES_DIR}/`);
|
|
1468
1498
|
}
|
|
@@ -1493,7 +1523,7 @@ function parseNote(id, source, fileNames) {
|
|
|
1493
1523
|
syncBreaking: meta.get("syncBreaking") === "true",
|
|
1494
1524
|
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1495
1525
|
roots: (meta.get("roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean),
|
|
1496
|
-
codemod: fileNames.find(
|
|
1526
|
+
codemod: fileNames.find(isCodemodFile) ?? null
|
|
1497
1527
|
};
|
|
1498
1528
|
}
|
|
1499
1529
|
async function listNoteIds(cwd, ref) {
|
|
@@ -1512,23 +1542,23 @@ async function arrivedNoteIds(cwd, from, to) {
|
|
|
1512
1542
|
return after.filter((id) => !known.has(id));
|
|
1513
1543
|
}
|
|
1514
1544
|
async function readPending(cwd, syncRef) {
|
|
1515
|
-
const
|
|
1516
|
-
if (!existsSync4(
|
|
1517
|
-
const parsed = JSON.parse(await readFile2(
|
|
1545
|
+
const path = join6(cwd, PENDING_FILE);
|
|
1546
|
+
if (!existsSync4(path)) return [];
|
|
1547
|
+
const parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
1518
1548
|
if (Array.isArray(parsed.pending)) return parsed.pending.filter((id) => typeof id === "string");
|
|
1519
1549
|
if (!Array.isArray(parsed.applied) || !syncRef) return [];
|
|
1520
1550
|
const applied = new Set(parsed.applied);
|
|
1521
1551
|
return (await listNoteIds(cwd, syncRef)).filter((id) => !applied.has(id));
|
|
1522
1552
|
}
|
|
1523
1553
|
async function writePending(cwd, ids) {
|
|
1524
|
-
const
|
|
1554
|
+
const path = join6(cwd, PENDING_FILE);
|
|
1525
1555
|
const pending = [...new Set(ids)].sort();
|
|
1526
1556
|
if (pending.length === 0) {
|
|
1527
|
-
await rm(
|
|
1557
|
+
await rm(path, { force: true });
|
|
1528
1558
|
return;
|
|
1529
1559
|
}
|
|
1530
|
-
await mkdir3(dirname3(
|
|
1531
|
-
await writeFile2(
|
|
1560
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1561
|
+
await writeFile2(path, `${JSON.stringify({ pending }, null, 2)}
|
|
1532
1562
|
`, "utf8");
|
|
1533
1563
|
}
|
|
1534
1564
|
function noteUrl(upstreamGitHubUrl, commit, id) {
|
|
@@ -1572,6 +1602,19 @@ function groupIgnoredUpstreamChanges(files, ignored) {
|
|
|
1572
1602
|
for (const group of groups.values()) group.paths.sort();
|
|
1573
1603
|
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
1574
1604
|
}
|
|
1605
|
+
function groupProtectedUpstreamChanges(paths, config) {
|
|
1606
|
+
const entries = [...config.overrides?.pinned ?? [], ...config.overrides?.ignored ?? []].map((entry) => entry.replace(/\/+$/, "")).filter(Boolean);
|
|
1607
|
+
const mostSpecificFirst = [...new Set(entries)].sort((a, b) => b.length - a.length);
|
|
1608
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1609
|
+
for (const path of paths) {
|
|
1610
|
+
const entry = mostSpecificFirst.find((candidate) => isUnderAnyFolder(path, [candidate])) ?? path;
|
|
1611
|
+
const group = groups.get(entry) ?? { entry, paths: [] };
|
|
1612
|
+
group.paths.push(path);
|
|
1613
|
+
groups.set(entry, group);
|
|
1614
|
+
}
|
|
1615
|
+
for (const group of groups.values()) group.paths.sort();
|
|
1616
|
+
return [...groups.values()];
|
|
1617
|
+
}
|
|
1575
1618
|
function isPinnedForSync(filePath, config, unpinned) {
|
|
1576
1619
|
if (isManagedFile(filePath)) return true;
|
|
1577
1620
|
if (unpinned) return false;
|
|
@@ -1580,7 +1623,7 @@ function isPinnedForSync(filePath, config, unpinned) {
|
|
|
1580
1623
|
function hasGlobChars(entry) {
|
|
1581
1624
|
return entry.includes("*") || entry.includes("?");
|
|
1582
1625
|
}
|
|
1583
|
-
function validateOverrides(config, forkPath) {
|
|
1626
|
+
async function validateOverrides(config, forkPath, upstreamRef) {
|
|
1584
1627
|
const warnings = [];
|
|
1585
1628
|
const pinned = config.overrides?.pinned || [];
|
|
1586
1629
|
for (const entry of pinned) {
|
|
@@ -1607,6 +1650,7 @@ function validateOverrides(config, forkPath) {
|
|
|
1607
1650
|
message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
|
|
1608
1651
|
});
|
|
1609
1652
|
} else if (!existsSync5(join7(forkPath, entry.replace(/\/+$/, "")))) {
|
|
1653
|
+
if (upstreamRef && await fileExistsAtRef(forkPath, upstreamRef, entry.replace(/\/+$/, ""))) continue;
|
|
1610
1654
|
warnings.push({
|
|
1611
1655
|
type: "ignored-not-found",
|
|
1612
1656
|
pattern: entry,
|
|
@@ -1623,78 +1667,124 @@ function printWarnings(warnings) {
|
|
|
1623
1667
|
}
|
|
1624
1668
|
|
|
1625
1669
|
// src/cli.ts
|
|
1626
|
-
|
|
1627
|
-
|
|
1670
|
+
var str = (value) => typeof value === "string" ? value : void 0;
|
|
1671
|
+
var flag = (value) => value === true;
|
|
1672
|
+
function readOptions(opts, operands = []) {
|
|
1673
|
+
const scope = str(opts.scope);
|
|
1628
1674
|
if (scope && scope !== "all" && scope !== "risk" && scope !== "protected") {
|
|
1629
1675
|
throw new Error(`invalid --scope '${scope}'. expected one of: all, risk, protected`);
|
|
1630
1676
|
}
|
|
1631
1677
|
const normalizedScope = scope;
|
|
1678
|
+
const run = str(opts.run);
|
|
1679
|
+
if (operands.length > 0 && !run) {
|
|
1680
|
+
throw new Error(`unexpected argument '${operands[0]}'. arguments after the options only go with --run <id>`);
|
|
1681
|
+
}
|
|
1632
1682
|
return {
|
|
1633
|
-
logFile: opts.log
|
|
1634
|
-
verbose: opts.verbose
|
|
1635
|
-
list: opts.list
|
|
1636
|
-
json: opts.json
|
|
1637
|
-
diff:
|
|
1638
|
-
openDiff:
|
|
1683
|
+
logFile: flag(opts.log),
|
|
1684
|
+
verbose: flag(opts.verbose),
|
|
1685
|
+
list: flag(opts.list),
|
|
1686
|
+
json: flag(opts.json),
|
|
1687
|
+
diff: str(opts.diff),
|
|
1688
|
+
openDiff: str(opts.openDiff),
|
|
1639
1689
|
scope: normalizedScope,
|
|
1640
|
-
fork:
|
|
1641
|
-
hard: opts.hard
|
|
1642
|
-
unpinned: opts.unpinned
|
|
1690
|
+
fork: str(opts.fork),
|
|
1691
|
+
hard: flag(opts.hard),
|
|
1692
|
+
unpinned: flag(opts.unpinned),
|
|
1643
1693
|
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
1644
|
-
ref:
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1694
|
+
ref: str(opts.ref) || void 0,
|
|
1695
|
+
keepConfig: flag(opts.keepConfig),
|
|
1696
|
+
force: flag(opts.force),
|
|
1697
|
+
checkOverrides: flag(opts.checkOverrides),
|
|
1698
|
+
coverage: flag(opts.coverage),
|
|
1699
|
+
since: str(opts.since) || void 0,
|
|
1700
|
+
md: flag(opts.md),
|
|
1701
|
+
all: flag(opts.all),
|
|
1702
|
+
show: str(opts.show),
|
|
1703
|
+
extract: str(opts.extract),
|
|
1704
|
+
run,
|
|
1705
|
+
script: str(opts.script),
|
|
1706
|
+
runArgs: run ? operands : void 0,
|
|
1651
1707
|
mark: Array.isArray(opts.mark) ? opts.mark.filter((id) => typeof id === "string") : void 0
|
|
1652
1708
|
};
|
|
1653
1709
|
}
|
|
1710
|
+
var logOption = {
|
|
1711
|
+
flags: "--log",
|
|
1712
|
+
description: "write complete file list to cella-sync.log"
|
|
1713
|
+
};
|
|
1714
|
+
var jsonOption = {
|
|
1715
|
+
flags: "--json",
|
|
1716
|
+
description: "machine-readable output for tooling/agents"
|
|
1717
|
+
};
|
|
1718
|
+
var trackOption = {
|
|
1719
|
+
flags: "--track <mode>",
|
|
1720
|
+
description: "override upstream tracking for this run: release|branch"
|
|
1721
|
+
};
|
|
1722
|
+
var refOption = {
|
|
1723
|
+
flags: "--ref <ref>",
|
|
1724
|
+
description: "pin the upstream commit for this run: sha, release tag or branch"
|
|
1725
|
+
};
|
|
1726
|
+
var hardOption = {
|
|
1727
|
+
flags: "--hard",
|
|
1728
|
+
description: "overwrite drifted files with upstream version (aggressive realignment)"
|
|
1729
|
+
};
|
|
1730
|
+
var verboseOption = {
|
|
1731
|
+
flags: "-V, --verbose",
|
|
1732
|
+
description: "show detailed output during operations"
|
|
1733
|
+
};
|
|
1734
|
+
var forkOption = {
|
|
1735
|
+
flags: "--fork <name>",
|
|
1736
|
+
description: "pre-select a fork by name (skips the fork selection prompt)"
|
|
1737
|
+
};
|
|
1654
1738
|
var serviceDefinitions = [
|
|
1655
1739
|
{
|
|
1656
1740
|
name: "analyze",
|
|
1657
1741
|
description: "dry run to see what would change on sync",
|
|
1658
1742
|
options: [
|
|
1659
|
-
|
|
1743
|
+
logOption,
|
|
1660
1744
|
{ flags: "--list", description: "non-interactive output for tooling (one file per line)" },
|
|
1661
|
-
|
|
1745
|
+
jsonOption,
|
|
1662
1746
|
{ 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" },
|
|
1747
|
+
trackOption,
|
|
1748
|
+
refOption,
|
|
1749
|
+
{ flags: "--diff <path>", description: "print the unified diff for one file, then exit" },
|
|
1666
1750
|
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
1667
1751
|
],
|
|
1668
1752
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1669
1753
|
},
|
|
1670
1754
|
{
|
|
1671
1755
|
name: "sync",
|
|
1672
|
-
description: "merge upstream changes
|
|
1756
|
+
description: "merge upstream changes onto a fresh branch, sync package.json and open a squash-merge PR",
|
|
1673
1757
|
options: [
|
|
1674
|
-
|
|
1675
|
-
|
|
1758
|
+
logOption,
|
|
1759
|
+
hardOption,
|
|
1676
1760
|
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
1677
|
-
|
|
1678
|
-
|
|
1761
|
+
trackOption,
|
|
1762
|
+
refOption,
|
|
1763
|
+
{ flags: "--keep-config", description: "merge with your sync config as it stands when upstream changed its own" }
|
|
1679
1764
|
],
|
|
1680
|
-
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1681
|
-
menuDescription: () => "merge upstream changes + sync package.json"
|
|
1765
|
+
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1682
1766
|
},
|
|
1683
1767
|
{
|
|
1684
1768
|
name: "migrate",
|
|
1685
1769
|
description: "list the upstream migration notes this app has not handled yet",
|
|
1686
1770
|
options: [
|
|
1687
1771
|
{ flags: "--all", description: "list every upstream note, handled or not" },
|
|
1688
|
-
|
|
1772
|
+
jsonOption,
|
|
1689
1773
|
{ flags: "--show <id>", description: "print one note's README" },
|
|
1690
1774
|
{ flags: "--extract <id>", description: "write one note's folder under node_modules/.cache to run its codemod" },
|
|
1775
|
+
{ flags: "--run <id>", description: "run one note's codemod; files identical to upstream stay as they are" },
|
|
1776
|
+
{ flags: "--script <file>", description: "with --run: the script to run when the note folder holds several" },
|
|
1691
1777
|
{ flags: "--mark <ids...>", description: "record notes as handled" }
|
|
1692
1778
|
],
|
|
1779
|
+
operands: {
|
|
1780
|
+
flags: "[codemodArgs...]",
|
|
1781
|
+
description: "with --run, after `--`: arguments for the codemod (default: inventory and the note's roots)"
|
|
1782
|
+
},
|
|
1693
1783
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1694
1784
|
},
|
|
1695
1785
|
{
|
|
1696
1786
|
name: "audit",
|
|
1697
|
-
description: "check for outdated packages
|
|
1787
|
+
description: "check for outdated packages and vulnerabilities",
|
|
1698
1788
|
options: [
|
|
1699
1789
|
{ flags: "--list", description: "skip interactive update prompts after printing audit results" },
|
|
1700
1790
|
{ flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
|
|
@@ -1703,23 +1793,27 @@ var serviceDefinitions = [
|
|
|
1703
1793
|
},
|
|
1704
1794
|
{
|
|
1705
1795
|
name: "forks",
|
|
1706
|
-
description: "sync
|
|
1796
|
+
description: "run normal sync inside local fork repositories",
|
|
1707
1797
|
options: [
|
|
1708
|
-
|
|
1798
|
+
forkOption,
|
|
1709
1799
|
{ flags: "--log", description: "write complete file list to cella-sync.log for each synced fork" },
|
|
1710
|
-
|
|
1711
|
-
|
|
1800
|
+
verboseOption,
|
|
1801
|
+
hardOption,
|
|
1802
|
+
{
|
|
1803
|
+
flags: "--keep-config",
|
|
1804
|
+
description: "merge with the fork's sync config as it stands when upstream changed its own"
|
|
1805
|
+
}
|
|
1712
1806
|
],
|
|
1713
1807
|
includeInMenu: (context) => context.hasForks
|
|
1714
1808
|
},
|
|
1715
1809
|
{
|
|
1716
1810
|
name: "contributions",
|
|
1717
|
-
description: "pull and adopt changes from forks",
|
|
1811
|
+
description: "pull and adopt changes from local forks",
|
|
1718
1812
|
options: [
|
|
1719
|
-
|
|
1720
|
-
{ flags: "--list", description: "non-interactive output
|
|
1721
|
-
|
|
1722
|
-
{ flags: "--diff <path>", description: "print the unified diff for
|
|
1813
|
+
forkOption,
|
|
1814
|
+
{ flags: "--list", description: "non-interactive output for tooling (tab-separated rows)" },
|
|
1815
|
+
jsonOption,
|
|
1816
|
+
{ flags: "--diff <path>", description: "print the unified diff for one contributed file, then exit" }
|
|
1723
1817
|
],
|
|
1724
1818
|
includeInMenu: (context) => context.hasForks
|
|
1725
1819
|
},
|
|
@@ -1727,26 +1821,31 @@ var serviceDefinitions = [
|
|
|
1727
1821
|
name: "stats",
|
|
1728
1822
|
description: "count files by category and workspace package",
|
|
1729
1823
|
options: [
|
|
1730
|
-
|
|
1731
|
-
{ flags: "--coverage", description: "regenerate test coverage before showing the stats summary" }
|
|
1824
|
+
verboseOption,
|
|
1825
|
+
{ flags: "--coverage", description: "regenerate test coverage before showing the stats summary" },
|
|
1826
|
+
{
|
|
1827
|
+
flags: "--since <ref>",
|
|
1828
|
+
description: "count what the current branch changed since it left <ref>, by kind of file"
|
|
1829
|
+
},
|
|
1830
|
+
{ flags: "--md", description: "with --since: print the table as markdown for a pull request description" }
|
|
1732
1831
|
]
|
|
1733
1832
|
}
|
|
1734
1833
|
];
|
|
1735
|
-
|
|
1834
|
+
function getMenuContext(userConfig, forkPath) {
|
|
1736
1835
|
return {
|
|
1737
1836
|
hasForks: (userConfig.forks?.length ?? 0) > 0,
|
|
1738
|
-
isUpstreamRepo:
|
|
1837
|
+
isUpstreamRepo: isUpstreamRepo(forkPath)
|
|
1739
1838
|
};
|
|
1740
1839
|
}
|
|
1741
1840
|
function buildServiceChoices(context) {
|
|
1742
1841
|
const label = (name) => name.padEnd(14);
|
|
1743
1842
|
const baseChoices = serviceDefinitions.filter((service) => service.includeInMenu?.(context) ?? true).map((service) => ({
|
|
1744
1843
|
value: service.name,
|
|
1745
|
-
name: `${label(service.name)}${colors_default.dim(service.
|
|
1844
|
+
name: `${label(service.name)}${colors_default.dim(service.description)}`
|
|
1746
1845
|
}));
|
|
1747
1846
|
return [
|
|
1748
1847
|
...baseChoices,
|
|
1749
|
-
{ type: "separator", separator:
|
|
1848
|
+
{ type: "separator", separator: MENU_DIVIDER },
|
|
1750
1849
|
{ value: "exit", name: colors_default.red(`${label("exit")}${colors_default.dim("quit without doing anything")}`) }
|
|
1751
1850
|
];
|
|
1752
1851
|
}
|
|
@@ -1755,10 +1854,13 @@ function addServiceCommand(program, definition, setSelection) {
|
|
|
1755
1854
|
for (const option of definition.options ?? []) {
|
|
1756
1855
|
command.option(option.flags, option.description);
|
|
1757
1856
|
}
|
|
1758
|
-
command.
|
|
1857
|
+
if (definition.operands) command.argument(definition.operands.flags, definition.operands.description);
|
|
1858
|
+
command.action((...args) => {
|
|
1859
|
+
const operands = definition.operands ? args[0] : [];
|
|
1860
|
+
const opts = definition.operands ? args[1] : args[0];
|
|
1759
1861
|
setSelection({
|
|
1760
1862
|
service: definition.name,
|
|
1761
|
-
options: readOptions(opts)
|
|
1863
|
+
options: readOptions(opts, operands)
|
|
1762
1864
|
});
|
|
1763
1865
|
});
|
|
1764
1866
|
}
|
|
@@ -1775,8 +1877,10 @@ function buildProgram(setSelection) {
|
|
|
1775
1877
|
" $ cella sync --unpinned",
|
|
1776
1878
|
" $ cella sync --track branch",
|
|
1777
1879
|
" $ cella sync --ref 4f7d87c",
|
|
1880
|
+
" $ cella migrate --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
|
|
1778
1881
|
" $ cella migrate --mark 20261002T0614-config-switch",
|
|
1779
1882
|
" $ cella audit --check-overrides",
|
|
1883
|
+
" $ cella stats --since origin/main --md",
|
|
1780
1884
|
" $ cella contributions --fork raak --json"
|
|
1781
1885
|
].join("\n")
|
|
1782
1886
|
);
|
|
@@ -1799,7 +1903,7 @@ function parseCommandLine(argv) {
|
|
|
1799
1903
|
async function promptForService(userConfig, forkPath) {
|
|
1800
1904
|
const selected = await select({
|
|
1801
1905
|
message: "choose a service:",
|
|
1802
|
-
choices: buildServiceChoices(
|
|
1906
|
+
choices: buildServiceChoices(getMenuContext(userConfig, forkPath)),
|
|
1803
1907
|
loop: false
|
|
1804
1908
|
});
|
|
1805
1909
|
if (selected === "exit") {
|
|
@@ -1822,9 +1926,10 @@ function buildRuntimeConfig(userConfig, forkPath, selection) {
|
|
|
1822
1926
|
}
|
|
1823
1927
|
async function parseCli(userConfig, forkPath) {
|
|
1824
1928
|
const selection = parseCommandLine(process5.argv);
|
|
1825
|
-
|
|
1929
|
+
const machineList = selection.options.list && selection.service !== "audit";
|
|
1930
|
+
if (selection.options.json || selection.options.diff || selection.options.md || machineList) setJsonMode(true);
|
|
1826
1931
|
printHeader();
|
|
1827
|
-
const warnings = validateOverrides(userConfig, forkPath);
|
|
1932
|
+
const warnings = await validateOverrides(userConfig, forkPath, resolveUpstream(userConfig.settings).branchRef);
|
|
1828
1933
|
if (warnings.length > 0) {
|
|
1829
1934
|
printWarnings(warnings);
|
|
1830
1935
|
console.info();
|
|
@@ -1842,14 +1947,14 @@ import { basename as basename4 } from "path";
|
|
|
1842
1947
|
import { execFileSync } from "child_process";
|
|
1843
1948
|
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1844
1949
|
import { tmpdir } from "os";
|
|
1845
|
-
import { basename as basename2, join as
|
|
1950
|
+
import { basename as basename2, join as join8 } from "path";
|
|
1846
1951
|
import process6 from "process";
|
|
1847
1952
|
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
1848
1953
|
const args = ["diff", "--no-color"];
|
|
1849
1954
|
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
1850
1955
|
args.push(range, "--", filePath);
|
|
1851
1956
|
try {
|
|
1852
|
-
return execFileSync("git", args, { cwd, maxBuffer:
|
|
1957
|
+
return execFileSync("git", args, { cwd, maxBuffer: MAX_BUFFER });
|
|
1853
1958
|
} catch (error) {
|
|
1854
1959
|
const stderr = error.stderr?.toString().trim();
|
|
1855
1960
|
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
@@ -1936,9 +2041,9 @@ function openWithDefaultApp(target) {
|
|
|
1936
2041
|
}
|
|
1937
2042
|
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
1938
2043
|
const html = await renderDiffPage(patch, meta);
|
|
1939
|
-
const dir =
|
|
2044
|
+
const dir = join8(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1940
2045
|
mkdirSync(dir, { recursive: true });
|
|
1941
|
-
const outPath =
|
|
2046
|
+
const outPath = join8(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
1942
2047
|
writeFileSync2(outPath, html);
|
|
1943
2048
|
openWithDefaultApp(outPath);
|
|
1944
2049
|
return outPath;
|
|
@@ -1947,13 +2052,13 @@ async function openDiffInBrowser(patch, meta, repoPath) {
|
|
|
1947
2052
|
// src/services/merge-engine.ts
|
|
1948
2053
|
import { existsSync as existsSync7 } from "fs";
|
|
1949
2054
|
import { rm as rm2 } from "fs/promises";
|
|
1950
|
-
import { join as
|
|
2055
|
+
import { join as join10 } from "path";
|
|
1951
2056
|
|
|
1952
2057
|
// src/utils/cleanup.ts
|
|
1953
2058
|
import { createHash } from "crypto";
|
|
1954
2059
|
import { existsSync as existsSync6, rmSync } from "fs";
|
|
1955
2060
|
import { tmpdir as tmpdir2 } from "os";
|
|
1956
|
-
import { basename as basename3, join as
|
|
2061
|
+
import { basename as basename3, join as join9, resolve as resolve4 } from "path";
|
|
1957
2062
|
import process7 from "process";
|
|
1958
2063
|
var WORKTREE_PREFIXES = {
|
|
1959
2064
|
sync: "cella-sync-",
|
|
@@ -1961,10 +2066,10 @@ var WORKTREE_PREFIXES = {
|
|
|
1961
2066
|
};
|
|
1962
2067
|
function buildWorktreePath(kind, repoPath) {
|
|
1963
2068
|
const key = createHash("sha256").update(resolve4(repoPath)).digest("hex").slice(0, 8);
|
|
1964
|
-
return
|
|
2069
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
|
|
1965
2070
|
}
|
|
1966
2071
|
function legacyWorktreePath(kind, repoPath) {
|
|
1967
|
-
return
|
|
2072
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
1968
2073
|
}
|
|
1969
2074
|
function getWorktreePath(repoPath) {
|
|
1970
2075
|
return buildWorktreePath("sync", repoPath);
|
|
@@ -2003,7 +2108,7 @@ async function cleanupLeftoverWorktrees(repoPath) {
|
|
|
2003
2108
|
}
|
|
2004
2109
|
async function handleAbort(signal) {
|
|
2005
2110
|
console.info();
|
|
2006
|
-
console.info(`${warningMark}
|
|
2111
|
+
console.info(`${warningMark} interrupted (${signal}): cleaning up...`);
|
|
2007
2112
|
if (currentRepoPath && currentWorktreePath) {
|
|
2008
2113
|
try {
|
|
2009
2114
|
await mergeAbort(currentWorktreePath);
|
|
@@ -2011,11 +2116,9 @@ async function handleAbort(signal) {
|
|
|
2011
2116
|
}
|
|
2012
2117
|
try {
|
|
2013
2118
|
await cleanupWorktree(currentRepoPath, currentWorktreePath);
|
|
2014
|
-
console.info(`${
|
|
2119
|
+
console.info(`${checkMark} no changes were made to your repository.`);
|
|
2015
2120
|
} catch (error) {
|
|
2016
|
-
console.error(
|
|
2017
|
-
`${colors_default.red("\u2717")} failed to clean up worktree: ${error instanceof Error ? error.message : "unknown error"}`
|
|
2018
|
-
);
|
|
2121
|
+
console.error(`${colors_default.red("\u2717")} failed to clean up worktree: ${errorMessage(error)}`);
|
|
2019
2122
|
}
|
|
2020
2123
|
}
|
|
2021
2124
|
process7.exit(1);
|
|
@@ -2111,25 +2214,44 @@ function readProperty(object, name, sourceFile) {
|
|
|
2111
2214
|
}
|
|
2112
2215
|
return void 0;
|
|
2113
2216
|
}
|
|
2217
|
+
function readStringEntries(node) {
|
|
2218
|
+
if (!Node.isArrayLiteralExpression(node)) return null;
|
|
2219
|
+
const entries = [];
|
|
2220
|
+
for (const element of node.getElements()) {
|
|
2221
|
+
if (Node.isStringLiteral(element) || Node.isNoSubstitutionTemplateLiteral(element)) {
|
|
2222
|
+
entries.push(element.getLiteralValue());
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
return entries;
|
|
2226
|
+
}
|
|
2227
|
+
function readPackageJsonSync(config, sourceFile) {
|
|
2228
|
+
const settings = readProperty(config, "settings", sourceFile);
|
|
2229
|
+
if (settings === void 0) return [...DEFAULT_PACKAGE_JSON_SYNC];
|
|
2230
|
+
if (!settings || !Node.isObjectLiteralExpression(settings)) return null;
|
|
2231
|
+
const keys = readProperty(settings, "packageJsonSync", sourceFile);
|
|
2232
|
+
if (keys === void 0) return [...DEFAULT_PACKAGE_JSON_SYNC];
|
|
2233
|
+
return keys ? readStringEntries(keys) : null;
|
|
2234
|
+
}
|
|
2114
2235
|
function parseOverrideLists(source) {
|
|
2115
2236
|
try {
|
|
2116
2237
|
const project = new Project2({ useInMemoryFileSystem: true, skipLoadingLibFiles: true });
|
|
2117
2238
|
const sourceFile = project.createSourceFile("cella.config.ts", source);
|
|
2118
2239
|
const config = findConfigObject(sourceFile);
|
|
2119
2240
|
if (!config) return null;
|
|
2241
|
+
const lists = {
|
|
2242
|
+
pinned: [],
|
|
2243
|
+
ignored: [],
|
|
2244
|
+
packageJsonSync: readPackageJsonSync(config, sourceFile)
|
|
2245
|
+
};
|
|
2120
2246
|
const overrides = readProperty(config, "overrides", sourceFile);
|
|
2121
|
-
if (overrides === void 0) return
|
|
2247
|
+
if (overrides === void 0) return lists;
|
|
2122
2248
|
if (!overrides || !Node.isObjectLiteralExpression(overrides)) return null;
|
|
2123
|
-
const lists = { pinned: [], ignored: [] };
|
|
2124
2249
|
for (const key of LIST_KEYS) {
|
|
2125
2250
|
const list = readProperty(overrides, key, sourceFile);
|
|
2126
2251
|
if (list === void 0) continue;
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
lists[key].push(element.getLiteralValue());
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2252
|
+
const entries = list ? readStringEntries(list) : null;
|
|
2253
|
+
if (!entries) return null;
|
|
2254
|
+
lists[key] = entries;
|
|
2133
2255
|
}
|
|
2134
2256
|
return lists;
|
|
2135
2257
|
} catch {
|
|
@@ -2149,11 +2271,19 @@ function diffOverrideLists(base, incoming, fork) {
|
|
|
2149
2271
|
removed: [...baseSet].filter((entry) => !incomingSet.has(entry) && forkEntries.includes(entry))
|
|
2150
2272
|
};
|
|
2151
2273
|
};
|
|
2152
|
-
|
|
2274
|
+
const diffKeys = () => {
|
|
2275
|
+
const [baseKeys, incomingKeys, forkKeys] = [base.packageJsonSync, incoming.packageJsonSync, fork.packageJsonSync];
|
|
2276
|
+
if (!baseKeys || !incomingKeys || !forkKeys) return { added: [], removed: [] };
|
|
2277
|
+
return {
|
|
2278
|
+
added: [...new Set(incomingKeys)].filter((key) => !baseKeys.includes(key) && !forkKeys.includes(key)),
|
|
2279
|
+
removed: [...new Set(baseKeys)].filter((key) => !incomingKeys.includes(key) && forkKeys.includes(key))
|
|
2280
|
+
};
|
|
2281
|
+
};
|
|
2282
|
+
return { pinned: diff("pinned"), ignored: diff("ignored"), packageJsonSync: diffKeys() };
|
|
2153
2283
|
}
|
|
2154
2284
|
async function readConfigSourceAtRef(repoPath, ref) {
|
|
2155
|
-
for (const
|
|
2156
|
-
const source = await git(["show", `${ref}:${
|
|
2285
|
+
for (const path of CONFIG_FILE_PATHS) {
|
|
2286
|
+
const source = await git(["show", `${ref}:${path}`], repoPath, { ignoreErrors: true });
|
|
2157
2287
|
if (source) return source;
|
|
2158
2288
|
}
|
|
2159
2289
|
return null;
|
|
@@ -2170,10 +2300,11 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
|
|
|
2170
2300
|
if (!incoming) return { kind: "unreadable", side: "incoming" };
|
|
2171
2301
|
const fork = {
|
|
2172
2302
|
pinned: forkConfig.overrides?.pinned ?? [],
|
|
2173
|
-
ignored: forkConfig.overrides?.ignored ?? []
|
|
2303
|
+
ignored: forkConfig.overrides?.ignored ?? [],
|
|
2304
|
+
packageJsonSync: forkConfig.settings.packageJsonSync ?? DEFAULT_PACKAGE_JSON_SYNC
|
|
2174
2305
|
};
|
|
2175
2306
|
const changes = diffOverrideLists(base, incoming, fork);
|
|
2176
|
-
const count =
|
|
2307
|
+
const count = Object.values(changes).reduce((n, list) => n + list.added.length + list.removed.length, 0);
|
|
2177
2308
|
return count > 0 ? { kind: "changes", ...changes } : void 0;
|
|
2178
2309
|
}
|
|
2179
2310
|
|
|
@@ -2297,10 +2428,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
2297
2428
|
status = fileIsPinned ? "pinned" : "diverged";
|
|
2298
2429
|
} else if (forkChanged && !upstreamChanged2) {
|
|
2299
2430
|
status = fileIsPinned ? "ahead" : predicates.hard ? "behind" : "drifted";
|
|
2300
|
-
} else if (!forkChanged && upstreamChanged2) {
|
|
2301
|
-
status = "behind";
|
|
2302
2431
|
} else {
|
|
2303
|
-
status = "
|
|
2432
|
+
status = "behind";
|
|
2304
2433
|
}
|
|
2305
2434
|
}
|
|
2306
2435
|
const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
|
|
@@ -2385,6 +2514,21 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
2385
2514
|
}
|
|
2386
2515
|
|
|
2387
2516
|
// src/services/merge-engine.ts
|
|
2517
|
+
var BehindSyncPointError = class extends Error {
|
|
2518
|
+
};
|
|
2519
|
+
var UpstreamConfigChangedError = class extends Error {
|
|
2520
|
+
report;
|
|
2521
|
+
constructor(report) {
|
|
2522
|
+
super(
|
|
2523
|
+
[
|
|
2524
|
+
"upstream changed its sync config and yours does not follow it: stopped before the merge, nothing changed.",
|
|
2525
|
+
` add or drop the entries that fit your app in ${CONFIG_FILE}, commit, then rerun.`,
|
|
2526
|
+
" to merge with your config as it stands, rerun with --keep-config."
|
|
2527
|
+
].join("\n")
|
|
2528
|
+
);
|
|
2529
|
+
this.report = report;
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2388
2532
|
function getGitHubBaseUrl(remoteUrl) {
|
|
2389
2533
|
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^.]+)(?:\.git)?$/);
|
|
2390
2534
|
if (sshMatch) {
|
|
@@ -2396,10 +2540,13 @@ function getGitHubBaseUrl(remoteUrl) {
|
|
|
2396
2540
|
}
|
|
2397
2541
|
return null;
|
|
2398
2542
|
}
|
|
2543
|
+
function isProtectedFile(file) {
|
|
2544
|
+
return file.isIgnored || file.isPinned;
|
|
2545
|
+
}
|
|
2399
2546
|
function syncPredicates(config) {
|
|
2400
2547
|
return {
|
|
2401
|
-
isIgnored: (
|
|
2402
|
-
isPinned: (
|
|
2548
|
+
isIgnored: (path) => isIgnored(path, config),
|
|
2549
|
+
isPinned: (path) => isPinnedForSync(path, config, config.unpinned),
|
|
2403
2550
|
hard: config.hard
|
|
2404
2551
|
};
|
|
2405
2552
|
}
|
|
@@ -2418,6 +2565,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2418
2565
|
for (const file of analyzedFiles) {
|
|
2419
2566
|
const { path: filePath, status, isPinned: pinned, isIgnored: ignored, existsInFork } = file;
|
|
2420
2567
|
if (status === "identical" || status === "ahead") continue;
|
|
2568
|
+
if (!isProtectedFile(file)) continue;
|
|
2421
2569
|
if (ignored && existsInFork) {
|
|
2422
2570
|
batchRestorePaths.push(filePath);
|
|
2423
2571
|
} else if (ignored && !existsInFork) {
|
|
@@ -2429,12 +2577,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2429
2577
|
}
|
|
2430
2578
|
}
|
|
2431
2579
|
onProgress?.("starting merge in fork...");
|
|
2432
|
-
await withTemporarySyncBaseGraft(
|
|
2433
|
-
forkPath,
|
|
2434
|
-
"HEAD",
|
|
2435
|
-
mergeBaseRef,
|
|
2436
|
-
() => merge(forkPath, upstreamRef, { noCommit: true, noEdit: true })
|
|
2437
|
-
);
|
|
2580
|
+
await withTemporarySyncBaseGraft(forkPath, "HEAD", mergeBaseRef, () => merge(forkPath, upstreamRef));
|
|
2438
2581
|
if (batchRestorePaths.length > 0) {
|
|
2439
2582
|
onProgress?.(`batch restoring ${batchRestorePaths.length} pinned/ignored files...`);
|
|
2440
2583
|
await batchRestoreToHead(forkPath, batchRestorePaths);
|
|
@@ -2447,29 +2590,11 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2447
2590
|
}
|
|
2448
2591
|
}
|
|
2449
2592
|
for (const file of analyzedFiles) {
|
|
2450
|
-
const { path: filePath, status,
|
|
2593
|
+
const { path: filePath, status, existsInFork } = file;
|
|
2451
2594
|
if (status === "identical" || status === "ahead") {
|
|
2452
2595
|
continue;
|
|
2453
2596
|
}
|
|
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
|
-
}
|
|
2597
|
+
if (isProtectedFile(file)) continue;
|
|
2473
2598
|
if (status === "diverged") {
|
|
2474
2599
|
onProgress?.(`\u2192 ${filePath}: using git merge result (diverged)`);
|
|
2475
2600
|
continue;
|
|
@@ -2514,6 +2639,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2514
2639
|
}
|
|
2515
2640
|
}
|
|
2516
2641
|
const protectedConflicts = analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path);
|
|
2642
|
+
const protectedConflictSet = new Set(protectedConflicts);
|
|
2517
2643
|
const gitConflicts = await getConflictedFiles(forkPath);
|
|
2518
2644
|
for (const filePath of gitConflicts) {
|
|
2519
2645
|
const isProtected = isIgnored(filePath, config) || isPinnedForSync(filePath, config, config.unpinned);
|
|
@@ -2521,7 +2647,10 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2521
2647
|
if (await fileExistsAtRef(forkPath, "HEAD", filePath)) {
|
|
2522
2648
|
onProgress?.(`\u2192 ${filePath}: keeping fork (protected conflict)`);
|
|
2523
2649
|
await restoreToHead(forkPath, filePath);
|
|
2524
|
-
if (!
|
|
2650
|
+
if (!protectedConflictSet.has(filePath)) {
|
|
2651
|
+
protectedConflictSet.add(filePath);
|
|
2652
|
+
protectedConflicts.push(filePath);
|
|
2653
|
+
}
|
|
2525
2654
|
} else {
|
|
2526
2655
|
onProgress?.(`\u2192 ${filePath}: removing (protected conflict, not in fork)`);
|
|
2527
2656
|
await removeFileFully(forkPath, filePath);
|
|
@@ -2553,8 +2682,7 @@ function calculateSummary(files) {
|
|
|
2553
2682
|
pinned: 0,
|
|
2554
2683
|
ignored: 0,
|
|
2555
2684
|
deleted: 0,
|
|
2556
|
-
renamed: 0
|
|
2557
|
-
total: files.length
|
|
2685
|
+
renamed: 0
|
|
2558
2686
|
};
|
|
2559
2687
|
for (const file of files) {
|
|
2560
2688
|
if (file.status !== "identical" && isManagedFile(file.path)) {
|
|
@@ -2574,12 +2702,12 @@ async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
|
|
|
2574
2702
|
const resolved = await resolveUpstreamCommit(forkPath, remoteName, ref);
|
|
2575
2703
|
if (!resolved) {
|
|
2576
2704
|
throw new Error(
|
|
2577
|
-
`--ref '${ref}' does not resolve to a commit. Pass a commit sha, a release tag
|
|
2705
|
+
`--ref '${ref}' does not resolve to a commit. Pass a commit sha, a release tag or a branch on ${config.settings.upstreamUrl}.`
|
|
2578
2706
|
);
|
|
2579
2707
|
}
|
|
2580
2708
|
if (!await isPublishedUpstream(forkPath, remoteName, branchRef, resolved.sha)) {
|
|
2581
2709
|
throw new Error(
|
|
2582
|
-
`--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release
|
|
2710
|
+
`--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release. Sync only to commits upstream has published there.`
|
|
2583
2711
|
);
|
|
2584
2712
|
}
|
|
2585
2713
|
return resolved.release ? { ref: resolved.release.ref, releaseTag: resolved.release.tag } : { ref: resolved.sha };
|
|
@@ -2600,7 +2728,7 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2600
2728
|
const track = config.track ?? configTrack;
|
|
2601
2729
|
await ensureRemote(forkPath, remoteName, config.settings.upstreamUrl);
|
|
2602
2730
|
onProgress?.(`fetching upstream (${remoteName})...`);
|
|
2603
|
-
await
|
|
2731
|
+
await fetchRemote(forkPath, remoteName);
|
|
2604
2732
|
let upstreamRef = branchRef;
|
|
2605
2733
|
let releaseTag;
|
|
2606
2734
|
if (config.ref) {
|
|
@@ -2613,7 +2741,7 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2613
2741
|
const latest = await resolveLatestReleaseTag(forkPath, remoteName);
|
|
2614
2742
|
if (!latest) {
|
|
2615
2743
|
throw new Error(
|
|
2616
|
-
`no upstream
|
|
2744
|
+
`no upstream release tags found on '${remoteName}'. Set upstreamTrack: 'branch' to follow ${branchRef}, or check that ${config.settings.upstreamUrl} publishes release tags.`
|
|
2617
2745
|
);
|
|
2618
2746
|
}
|
|
2619
2747
|
upstreamRef = latest.ref;
|
|
@@ -2638,21 +2766,23 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2638
2766
|
getShortSha(forkPath, syncPoint)
|
|
2639
2767
|
]);
|
|
2640
2768
|
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
|
|
2769
|
+
throw new BehindSyncPointError(
|
|
2642
2770
|
`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
2771
|
${hint}`
|
|
2644
2772
|
);
|
|
2645
2773
|
}
|
|
2646
2774
|
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
|
-
}) : [];
|
|
2775
|
+
const commitSkip = commitCount > COMMIT_LIST_MAX ? commitCount - COMMIT_LIST_MAX : 0;
|
|
2776
|
+
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, { skip: commitSkip, limit: COMMIT_LIST_MAX }) : [];
|
|
2654
2777
|
onStep?.("fetched upstream", formatFetchedUpstreamDetail(commitCount, upstreamCommits, upstreamGitHubUrl));
|
|
2655
|
-
return { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit, upstreamCommits };
|
|
2778
|
+
return { upstreamRef, releaseTag, mergeBase, syncPoint, upstreamGitHubUrl, upstreamCommit, upstreamCommits };
|
|
2779
|
+
}
|
|
2780
|
+
async function assertSyncConfigFollowed(config, ctx) {
|
|
2781
|
+
if (config.keepConfig) return;
|
|
2782
|
+
const report = await compareUpstreamOverrides(config.forkPath, ctx.syncPoint, ctx.upstreamRef, config).catch(
|
|
2783
|
+
() => void 0
|
|
2784
|
+
);
|
|
2785
|
+
if (report?.kind === "changes") throw new UpstreamConfigChangedError(report);
|
|
2656
2786
|
}
|
|
2657
2787
|
function resultMeta(config, ctx) {
|
|
2658
2788
|
return {
|
|
@@ -2664,7 +2794,7 @@ function resultMeta(config, ctx) {
|
|
|
2664
2794
|
upstreamCommits: ctx.upstreamCommits
|
|
2665
2795
|
};
|
|
2666
2796
|
}
|
|
2667
|
-
async function upstreamOnlyReports(config, ctx, files) {
|
|
2797
|
+
async function upstreamOnlyReports(config, ctx, files, protectedConflicts) {
|
|
2668
2798
|
const { forkPath } = config;
|
|
2669
2799
|
try {
|
|
2670
2800
|
const [baseSha, upstreamSha, upstreamOverrides] = await Promise.all([
|
|
@@ -2676,7 +2806,8 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2676
2806
|
return {
|
|
2677
2807
|
upstreamDiffRange: `${baseSha}..${upstreamSha}`,
|
|
2678
2808
|
upstreamOverrides,
|
|
2679
|
-
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? [])
|
|
2809
|
+
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? []),
|
|
2810
|
+
protectedUpstreamChanges: groupProtectedUpstreamChanges(protectedConflicts, config)
|
|
2680
2811
|
};
|
|
2681
2812
|
} catch {
|
|
2682
2813
|
return {};
|
|
@@ -2685,12 +2816,10 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2685
2816
|
async function readNotesInRange(config, ctx) {
|
|
2686
2817
|
const { forkPath } = config;
|
|
2687
2818
|
try {
|
|
2688
|
-
const aggressive = config.hard === true || config.unpinned === true;
|
|
2689
|
-
const syncBase = aggressive ? (await getEffectiveMergeBase(forkPath, "HEAD", ctx.upstreamRef)).base : ctx.mergeBase;
|
|
2690
2819
|
const [total, arrived, pending] = await Promise.all([
|
|
2691
2820
|
listNoteIds(forkPath, ctx.upstreamRef),
|
|
2692
|
-
arrivedNoteIds(forkPath,
|
|
2693
|
-
readPending(forkPath,
|
|
2821
|
+
arrivedNoteIds(forkPath, ctx.syncPoint, ctx.upstreamRef),
|
|
2822
|
+
readPending(forkPath, ctx.syncPoint)
|
|
2694
2823
|
]);
|
|
2695
2824
|
return { total: total.length, arrived, pending: [.../* @__PURE__ */ new Set([...pending, ...arrived])] };
|
|
2696
2825
|
} catch {
|
|
@@ -2701,14 +2830,15 @@ function notesResult(notes) {
|
|
|
2701
2830
|
return notes ? { migrationNotes: { total: notes.total, arrived: notes.arrived, open: notes.pending.length } } : {};
|
|
2702
2831
|
}
|
|
2703
2832
|
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2704
|
-
if (!existsSync7(
|
|
2833
|
+
if (!existsSync7(join10(forkPath, NOTES_DIR))) return;
|
|
2705
2834
|
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", NOTES_DIR], forkPath);
|
|
2706
|
-
await rm2(
|
|
2835
|
+
await rm2(join10(forkPath, NOTES_DIR), { recursive: true, force: true });
|
|
2707
2836
|
onStep?.("migration notes", `removed ${NOTES_DIR}/: notes stay upstream, list them with pnpm cella migrate`);
|
|
2708
2837
|
}
|
|
2709
2838
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2710
2839
|
const { forkPath } = config;
|
|
2711
2840
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2841
|
+
await assertSyncConfigFollowed(config, ctx);
|
|
2712
2842
|
const notes = await readNotesInRange(config, ctx);
|
|
2713
2843
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2714
2844
|
forkPath,
|
|
@@ -2719,9 +2849,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2719
2849
|
);
|
|
2720
2850
|
const summary = calculateSummary(analyzedFiles);
|
|
2721
2851
|
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;
|
|
2852
|
+
const totalResolved = analyzedFiles.filter((file) => SYNC_APPLIED_STATUSES.includes(file.status)).length;
|
|
2725
2853
|
const syncManifest = {
|
|
2726
2854
|
upstream: {
|
|
2727
2855
|
repo: upstreamGitHubUrl ? upstreamGitHubUrl.replace("https://github.com/", "") : void 0,
|
|
@@ -2764,7 +2892,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2764
2892
|
autoMergedFiles,
|
|
2765
2893
|
protectedConflicts,
|
|
2766
2894
|
...resultMeta(config, ctx),
|
|
2767
|
-
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2895
|
+
...await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts),
|
|
2768
2896
|
...notesResult(notes)
|
|
2769
2897
|
};
|
|
2770
2898
|
}
|
|
@@ -2776,12 +2904,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2776
2904
|
await createWorktree(forkPath, worktreePath, "HEAD");
|
|
2777
2905
|
onStep?.("worktree created", worktreePath);
|
|
2778
2906
|
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
|
-
);
|
|
2907
|
+
await withTemporarySyncBaseGraft(worktreePath, "HEAD", ctx.mergeBase, () => merge(worktreePath, ctx.upstreamRef));
|
|
2785
2908
|
onStep?.("merge complete", "upstream merged into worktree");
|
|
2786
2909
|
onProgress?.("analyzing files...");
|
|
2787
2910
|
const analyzedFiles = await analyzeRefs(
|
|
@@ -2793,8 +2916,9 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2793
2916
|
onProgress
|
|
2794
2917
|
);
|
|
2795
2918
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2796
|
-
const
|
|
2797
|
-
|
|
2919
|
+
const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
|
|
2920
|
+
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts);
|
|
2921
|
+
onStep?.("analysis complete", `${analyzedFiles.length} files analyzed, dry run, no changes applied`);
|
|
2798
2922
|
onProgress?.("cleaning up worktree...");
|
|
2799
2923
|
await cleanupWorktree(forkPath, worktreePath);
|
|
2800
2924
|
return {
|
|
@@ -2803,7 +2927,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2803
2927
|
summary: calculateSummary(analyzedFiles),
|
|
2804
2928
|
// For analyze mode, count diverged files as potential conflicts
|
|
2805
2929
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2806
|
-
protectedConflicts
|
|
2930
|
+
protectedConflicts,
|
|
2807
2931
|
...resultMeta(config, ctx),
|
|
2808
2932
|
...reports,
|
|
2809
2933
|
...notesResult(notes)
|
|
@@ -2823,23 +2947,39 @@ async function runMergeEngine(config, options) {
|
|
|
2823
2947
|
throw error;
|
|
2824
2948
|
}
|
|
2825
2949
|
}
|
|
2950
|
+
async function runEngineWithSpinner(config, apply) {
|
|
2951
|
+
return runMergeEngine(config, {
|
|
2952
|
+
apply,
|
|
2953
|
+
onProgress: (message) => {
|
|
2954
|
+
spinnerText(message);
|
|
2955
|
+
},
|
|
2956
|
+
onStep: (label, detail) => {
|
|
2957
|
+
spinnerSuccess(label, detail);
|
|
2958
|
+
createSpinner("...");
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2826
2962
|
|
|
2827
2963
|
// src/services/migrate.ts
|
|
2828
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
2964
|
+
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
2829
2965
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2830
|
-
import { dirname as dirname4, join as
|
|
2966
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
2831
2967
|
var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
|
|
2968
|
+
var RESTORED_LIST_MAX = 20;
|
|
2969
|
+
async function ensureUpstreamCommit(config, sha) {
|
|
2970
|
+
const { forkPath } = config;
|
|
2971
|
+
const present = async () => !!await git(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`], forkPath, { ignoreErrors: true });
|
|
2972
|
+
if (await present()) return;
|
|
2973
|
+
await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
|
|
2974
|
+
await fetchRemote(forkPath, DEFAULT_UPSTREAM_REMOTE);
|
|
2975
|
+
if (!await present())
|
|
2976
|
+
throw new Error(`upstream commit ${sha.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2977
|
+
}
|
|
2832
2978
|
async function resolveNotesRef(config) {
|
|
2833
2979
|
const { forkPath } = config;
|
|
2834
2980
|
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2835
2981
|
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
|
-
}
|
|
2982
|
+
await ensureUpstreamCommit(config, ref);
|
|
2843
2983
|
return ref;
|
|
2844
2984
|
}
|
|
2845
2985
|
async function readNotesStatus(config) {
|
|
@@ -2881,27 +3021,88 @@ function printNote(index, note, url) {
|
|
|
2881
3021
|
if (note.summary) console.info(` ${note.summary}`);
|
|
2882
3022
|
console.info(colors_default.dim(` read: pnpm cella migrate --show ${note.id}`));
|
|
2883
3023
|
if (url) console.info(colors_default.dim(` ${url}`));
|
|
2884
|
-
if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --
|
|
3024
|
+
if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --run ${note.id}`));
|
|
2885
3025
|
console.info();
|
|
2886
3026
|
}
|
|
2887
|
-
async function
|
|
3027
|
+
async function writeNoteFolder(config, ref, id) {
|
|
2888
3028
|
const { forkPath } = config;
|
|
2889
3029
|
const note = await readNote(forkPath, ref, id);
|
|
2890
3030
|
if (!note) throw new Error(`no migration note '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2891
3031
|
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${NOTES_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2892
|
-
const target =
|
|
3032
|
+
const target = join11(EXTRACT_DIR, id);
|
|
2893
3033
|
for (const file of files) {
|
|
2894
3034
|
const content = execFileSync2("git", ["show", `${ref}:${NOTES_DIR}/${id}/${file}`], {
|
|
2895
3035
|
cwd: forkPath,
|
|
2896
|
-
maxBuffer:
|
|
3036
|
+
maxBuffer: MAX_BUFFER
|
|
2897
3037
|
});
|
|
2898
|
-
const
|
|
2899
|
-
await mkdir4(dirname4(
|
|
2900
|
-
await writeFile3(
|
|
3038
|
+
const path = join11(forkPath, target, file);
|
|
3039
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
3040
|
+
await writeFile3(path, content);
|
|
2901
3041
|
}
|
|
2902
|
-
|
|
3042
|
+
return { note, target, files };
|
|
3043
|
+
}
|
|
3044
|
+
async function extractNote(config, ref, id) {
|
|
3045
|
+
const { note, target, files } = await writeNoteFolder(config, ref, id);
|
|
3046
|
+
console.info(`${checkMark} extracted ${files.length} file(s) to ${target}/`);
|
|
2903
3047
|
console.info(colors_default.dim(` the README's ${NOTES_DIR}/${id}/ paths are this folder.`));
|
|
2904
|
-
if (note.codemod)
|
|
3048
|
+
if (note.codemod) {
|
|
3049
|
+
console.info(colors_default.dim(` pnpm exec tsx ${target}/${note.codemod} inventory ${note.roots.join(" ")}`));
|
|
3050
|
+
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella migrate --run ${id}`));
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
function resolveCodemod(id, files, script) {
|
|
3054
|
+
if (script) {
|
|
3055
|
+
const name = script.replace(/^\.\//, "");
|
|
3056
|
+
if (!files.includes(name)) {
|
|
3057
|
+
throw new Error(`migration note '${id}' has no file '${script}'. its files: ${files.join(", ")}`);
|
|
3058
|
+
}
|
|
3059
|
+
return name;
|
|
3060
|
+
}
|
|
3061
|
+
const scripts = files.filter((file) => !file.includes("/") && isCodemodFile(file));
|
|
3062
|
+
if (scripts.length === 0) {
|
|
3063
|
+
throw new Error(
|
|
3064
|
+
`migration note '${id}' ships no codemod script: its steps are manual, read them with \`pnpm cella migrate --show ${id}\`.`
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
if (scripts.length > 1) {
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`migration note '${id}' ships ${scripts.length} scripts (${scripts.join(", ")}): name the one to run with --script <file>.`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
return scripts[0];
|
|
3073
|
+
}
|
|
3074
|
+
async function runNote(config, ref, id) {
|
|
3075
|
+
const { forkPath } = config;
|
|
3076
|
+
const { note, target, files } = await writeNoteFolder(config, ref, id);
|
|
3077
|
+
const script = join11(target, resolveCodemod(id, files, config.script));
|
|
3078
|
+
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...note.roots];
|
|
3079
|
+
const upstreamCommit = await readManifestBase(forkPath) ?? ref;
|
|
3080
|
+
await ensureUpstreamCommit(config, upstreamCommit);
|
|
3081
|
+
const identical = await listIdenticalToRef(forkPath, upstreamCommit);
|
|
3082
|
+
console.info(colors_default.dim(`running pnpm exec tsx ${script} ${args.join(" ")}`));
|
|
3083
|
+
console.info();
|
|
3084
|
+
const child = spawnSync("pnpm", ["exec", "tsx", script, ...args], { cwd: forkPath, stdio: "inherit" });
|
|
3085
|
+
if (child.error) throw new Error(`could not run the codemod: ${child.error.message}`);
|
|
3086
|
+
const stillIdentical = await listIdenticalToRef(forkPath, upstreamCommit);
|
|
3087
|
+
const restored = [...identical].filter((path) => !stillIdentical.has(path)).sort();
|
|
3088
|
+
await batchRestoreWorktreeFromRef(forkPath, upstreamCommit, restored);
|
|
3089
|
+
const upstreamShort = upstreamCommit.slice(0, 9);
|
|
3090
|
+
console.info();
|
|
3091
|
+
if (restored.length > 0) {
|
|
3092
|
+
console.info(`${checkMark} restored ${restored.length} file(s) to upstream ${upstreamShort}`);
|
|
3093
|
+
console.info(colors_default.dim(" the codemod changed them, and they were identical to upstream before the run:"));
|
|
3094
|
+
for (const path of restored.slice(0, RESTORED_LIST_MAX)) console.info(colors_default.dim(` ${path}`));
|
|
3095
|
+
if (restored.length > RESTORED_LIST_MAX) {
|
|
3096
|
+
console.info(colors_default.dim(` ... + ${restored.length - RESTORED_LIST_MAX} more`));
|
|
3097
|
+
}
|
|
3098
|
+
} else {
|
|
3099
|
+
console.info(colors_default.dim(`no file identical to upstream ${upstreamShort} was changed by the codemod.`));
|
|
3100
|
+
}
|
|
3101
|
+
if (child.status !== 0) throw new Error(`the codemod exited with code ${child.status ?? "unknown"}`);
|
|
3102
|
+
if (!config.runArgs?.length) {
|
|
3103
|
+
const apply = ["rewrite", ...note.roots].join(" ");
|
|
3104
|
+
console.info(colors_default.dim(`report only. to apply: pnpm cella migrate --run ${id} -- ${apply}`));
|
|
3105
|
+
}
|
|
2905
3106
|
}
|
|
2906
3107
|
async function markNotes(config, ref, pending, ids) {
|
|
2907
3108
|
const open = new Set(pending);
|
|
@@ -2909,7 +3110,7 @@ async function markNotes(config, ref, pending, ids) {
|
|
|
2909
3110
|
for (const id of ids) open.delete(id);
|
|
2910
3111
|
await writePending(config.forkPath, [...open]);
|
|
2911
3112
|
const marked = ids.length - unknown.length;
|
|
2912
|
-
if (marked > 0) console.info(`${
|
|
3113
|
+
if (marked > 0) console.info(`${checkMark} recorded ${marked} note(s) as handled`);
|
|
2913
3114
|
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2914
3115
|
console.info(colors_default.dim(formatNotesLine((await listNoteIds(config.forkPath, ref)).length, open.size)));
|
|
2915
3116
|
}
|
|
@@ -2924,6 +3125,7 @@ async function runMigrate(config) {
|
|
|
2924
3125
|
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2925
3126
|
const urlOf = (id) => noteUrl(githubUrl, ref, id);
|
|
2926
3127
|
if (config.mark?.length) return markNotes(config, ref, pending, config.mark);
|
|
3128
|
+
if (config.run) return runNote(config, ref, config.run);
|
|
2927
3129
|
if (config.extract) return extractNote(config, ref, config.extract);
|
|
2928
3130
|
if (config.show) {
|
|
2929
3131
|
const source = await git(["show", `${ref}:${NOTES_DIR}/${config.show}/README.md`], forkPath, {
|
|
@@ -2997,16 +3199,7 @@ function printUnifiedDiff(config, filePath) {
|
|
|
2997
3199
|
}
|
|
2998
3200
|
async function runAnalyze(config) {
|
|
2999
3201
|
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
|
-
});
|
|
3202
|
+
const result = await runEngineWithSpinner(config, false);
|
|
3010
3203
|
spinnerSuccess();
|
|
3011
3204
|
const scopedFiles = filterByScope(result, config.scope ?? "all");
|
|
3012
3205
|
if (config.diff) {
|
|
@@ -3028,15 +3221,15 @@ async function runAnalyze(config) {
|
|
|
3028
3221
|
upstreamChangedLines: f.upstreamChangedLines ?? null,
|
|
3029
3222
|
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null,
|
|
3030
3223
|
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
|
|
3224
|
+
// Only on the sync config: upstream override entries and packageJsonSync keys the fork config does not follow
|
|
3225
|
+
upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored, packageJsonSync: overrides.packageJsonSync } : null
|
|
3033
3226
|
}));
|
|
3034
3227
|
writeStdout(JSON.stringify(out, null, 2));
|
|
3035
3228
|
return result;
|
|
3036
3229
|
}
|
|
3037
3230
|
if (config.list) {
|
|
3038
3231
|
for (const file of scopedFiles) {
|
|
3039
|
-
|
|
3232
|
+
writeStdout(file.path);
|
|
3040
3233
|
}
|
|
3041
3234
|
return result;
|
|
3042
3235
|
}
|
|
@@ -3045,7 +3238,7 @@ async function runAnalyze(config) {
|
|
|
3045
3238
|
if (!file) throw new Error(`file not found in analysis results: ${config.openDiff}`);
|
|
3046
3239
|
const patch = gitDiffFile(config.forkPath, config.upstreamRef, file.path);
|
|
3047
3240
|
if (patch.length === 0) {
|
|
3048
|
-
console.info(`${
|
|
3241
|
+
console.info(`${checkMark} ${file.path} is identical to upstream`);
|
|
3049
3242
|
return result;
|
|
3050
3243
|
}
|
|
3051
3244
|
const pagePath = await openDiffInBrowser(
|
|
@@ -3058,7 +3251,7 @@ async function runAnalyze(config) {
|
|
|
3058
3251
|
},
|
|
3059
3252
|
config.forkPath
|
|
3060
3253
|
);
|
|
3061
|
-
console.info(`${
|
|
3254
|
+
console.info(`${checkMark} opened browser diff for ${file.path}`);
|
|
3062
3255
|
console.info(colors_default.dim(` ${pagePath}`));
|
|
3063
3256
|
return result;
|
|
3064
3257
|
}
|
|
@@ -3068,41 +3261,34 @@ async function runAnalyze(config) {
|
|
|
3068
3261
|
fileLinkMode: config.settings.fileLinkMode,
|
|
3069
3262
|
forkPath: config.forkPath
|
|
3070
3263
|
};
|
|
3071
|
-
printAnalysisFileGroups(result.files, linkOptions);
|
|
3072
|
-
|
|
3073
|
-
printIgnoredUpstreamChanges(result);
|
|
3074
|
-
printUpstreamOverrideChanges(result);
|
|
3075
|
-
printMaskingPinWarning(result.files);
|
|
3264
|
+
printAnalysisFileGroups(result.files, linkOptions, result);
|
|
3265
|
+
printEngineReports(result, "analysis summary");
|
|
3076
3266
|
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
|
-
}
|
|
3267
|
+
printLogFileReport(config, result.files);
|
|
3082
3268
|
console.info();
|
|
3083
3269
|
return result;
|
|
3084
3270
|
}
|
|
3085
3271
|
|
|
3086
3272
|
// src/services/audit.ts
|
|
3087
|
-
import { spawnSync } from "child_process";
|
|
3088
|
-
import
|
|
3089
|
-
import
|
|
3273
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3274
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3275
|
+
import { basename as basename5, join as join13, relative as relative2 } from "path";
|
|
3090
3276
|
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
3091
3277
|
|
|
3092
3278
|
// src/utils/audit-utils.ts
|
|
3093
3279
|
import { execFile as execFile2 } from "child_process";
|
|
3094
|
-
import
|
|
3095
|
-
import
|
|
3280
|
+
import { existsSync as existsSync8, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3281
|
+
import { join as join12 } from "path";
|
|
3096
3282
|
import { promisify as promisify2 } from "util";
|
|
3097
3283
|
var execFileAsync2 = promisify2(execFile2);
|
|
3098
|
-
var CACHE_FILE =
|
|
3284
|
+
var CACHE_FILE = join12(import.meta.dirname, "..", "..", ".audit.cache.json");
|
|
3099
3285
|
var CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
|
|
3100
3286
|
var CHANGELOG_PATHS = ["CHANGELOG.md", "CHANGELOG", "changelog.md", "HISTORY.md", "CHANGES.md", "NEWS.md"];
|
|
3101
3287
|
var DEFAULT_BRANCHES = ["main", "master"];
|
|
3102
3288
|
function loadCache() {
|
|
3103
3289
|
try {
|
|
3104
|
-
if (
|
|
3105
|
-
return JSON.parse(
|
|
3290
|
+
if (existsSync8(CACHE_FILE)) {
|
|
3291
|
+
return JSON.parse(readFileSync2(CACHE_FILE, "utf8"));
|
|
3106
3292
|
}
|
|
3107
3293
|
} catch {
|
|
3108
3294
|
}
|
|
@@ -3110,15 +3296,15 @@ function loadCache() {
|
|
|
3110
3296
|
}
|
|
3111
3297
|
function saveCache(cache) {
|
|
3112
3298
|
try {
|
|
3113
|
-
|
|
3299
|
+
writeFileSync3(CACHE_FILE, `${JSON.stringify(cache, null, 2)}
|
|
3114
3300
|
`);
|
|
3115
3301
|
} catch {
|
|
3116
3302
|
}
|
|
3117
3303
|
}
|
|
3118
3304
|
function clearCache() {
|
|
3119
3305
|
try {
|
|
3120
|
-
if (
|
|
3121
|
-
|
|
3306
|
+
if (existsSync8(CACHE_FILE)) {
|
|
3307
|
+
unlinkSync(CACHE_FILE);
|
|
3122
3308
|
console.info(colors_default.green("\u2713 cache cleared successfully"));
|
|
3123
3309
|
} else {
|
|
3124
3310
|
console.info(colors_default.yellow("no cache file found"));
|
|
@@ -3206,56 +3392,32 @@ function isMajorVersionChange(current, latest) {
|
|
|
3206
3392
|
if (!currentMajor || !latestMajor) return false;
|
|
3207
3393
|
return Number.parseInt(latestMajor, 10) > Number.parseInt(currentMajor, 10);
|
|
3208
3394
|
}
|
|
3209
|
-
async function
|
|
3395
|
+
async function runPnpmJson(args, cwd) {
|
|
3396
|
+
const parse = (stdout) => {
|
|
3397
|
+
if (!stdout || stdout.trim() === "") return null;
|
|
3398
|
+
try {
|
|
3399
|
+
return JSON.parse(stdout);
|
|
3400
|
+
} catch {
|
|
3401
|
+
return null;
|
|
3402
|
+
}
|
|
3403
|
+
};
|
|
3210
3404
|
try {
|
|
3211
|
-
const { stdout } = await execFileAsync2("pnpm",
|
|
3212
|
-
encoding: "
|
|
3405
|
+
const { stdout } = await execFileAsync2("pnpm", args, {
|
|
3406
|
+
encoding: "utf8",
|
|
3213
3407
|
maxBuffer: 10 * 1024 * 1024,
|
|
3214
3408
|
// 10MB buffer for large outputs
|
|
3215
3409
|
cwd
|
|
3216
3410
|
});
|
|
3217
|
-
|
|
3218
|
-
return {};
|
|
3219
|
-
}
|
|
3220
|
-
return JSON.parse(stdout);
|
|
3411
|
+
return parse(stdout);
|
|
3221
3412
|
} 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 {};
|
|
3413
|
+
return parse(error instanceof Error && "stdout" in error ? error.stdout : void 0);
|
|
3233
3414
|
}
|
|
3234
3415
|
}
|
|
3416
|
+
async function getOutdatedPackages(cwd) {
|
|
3417
|
+
return await runPnpmJson(["-r", "outdated", "--json"], cwd) ?? {};
|
|
3418
|
+
}
|
|
3235
3419
|
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
|
-
}
|
|
3420
|
+
return await runPnpmJson(["audit", "--json"], cwd);
|
|
3259
3421
|
}
|
|
3260
3422
|
function parseDependencyPath(paths) {
|
|
3261
3423
|
for (const pathStr of paths) {
|
|
@@ -3277,12 +3439,9 @@ function buildVulnerabilityMap(auditResult) {
|
|
|
3277
3439
|
const { workspace, directDependency } = parseDependencyPath(allPaths);
|
|
3278
3440
|
const existing = map.get(advisory.module_name) || [];
|
|
3279
3441
|
existing.push({
|
|
3280
|
-
id: advisory.id,
|
|
3281
3442
|
title: advisory.title,
|
|
3282
3443
|
severity: advisory.severity,
|
|
3283
|
-
url: advisory.url,
|
|
3284
3444
|
vulnerableVersions: advisory.vulnerable_versions,
|
|
3285
|
-
patchedVersions: advisory.patched_versions,
|
|
3286
3445
|
cves: advisory.cves || [],
|
|
3287
3446
|
workspace,
|
|
3288
3447
|
directDependency
|
|
@@ -3291,19 +3450,15 @@ function buildVulnerabilityMap(auditResult) {
|
|
|
3291
3450
|
}
|
|
3292
3451
|
return map;
|
|
3293
3452
|
}
|
|
3453
|
+
var vulnIcons = {
|
|
3454
|
+
critical: colors_default.red("\u25CF"),
|
|
3455
|
+
high: colors_default.red("\u25CF"),
|
|
3456
|
+
moderate: colors_default.yellow("\u25CF"),
|
|
3457
|
+
low: colors_default.blue("\u25CF"),
|
|
3458
|
+
info: colors_default.gray("\u25CF")
|
|
3459
|
+
};
|
|
3294
3460
|
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
|
-
}
|
|
3461
|
+
return vulnIcons[severity] ?? colors_default.gray("\u25CF");
|
|
3307
3462
|
}
|
|
3308
3463
|
function getHighestSeverity(vulns) {
|
|
3309
3464
|
if (vulns.length === 0) return null;
|
|
@@ -3313,13 +3468,13 @@ function getHighestSeverity(vulns) {
|
|
|
3313
3468
|
}
|
|
3314
3469
|
return null;
|
|
3315
3470
|
}
|
|
3316
|
-
function middleTruncate(
|
|
3317
|
-
if (
|
|
3471
|
+
function middleTruncate(str2, maxLen) {
|
|
3472
|
+
if (str2.length <= maxLen) return str2;
|
|
3318
3473
|
const ellipsis = "\u2026";
|
|
3319
3474
|
const charsToShow = maxLen - ellipsis.length;
|
|
3320
3475
|
const frontChars = Math.ceil(charsToShow / 2);
|
|
3321
3476
|
const backChars = Math.floor(charsToShow / 2);
|
|
3322
|
-
return
|
|
3477
|
+
return str2.slice(0, frontChars) + ellipsis + str2.slice(-backChars);
|
|
3323
3478
|
}
|
|
3324
3479
|
function formatDependents(dependents) {
|
|
3325
3480
|
if (dependents.length === 0) return "";
|
|
@@ -3364,18 +3519,42 @@ function parseYamlBlockList(content, key) {
|
|
|
3364
3519
|
}
|
|
3365
3520
|
|
|
3366
3521
|
// src/services/audit.ts
|
|
3367
|
-
|
|
3522
|
+
var AUDIT_FIX_VALUE = "__audit_fix__";
|
|
3523
|
+
function createDependentPkgReader() {
|
|
3524
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3525
|
+
return (location) => {
|
|
3526
|
+
let cached = cache.get(location);
|
|
3527
|
+
if (cached === void 0) {
|
|
3528
|
+
try {
|
|
3529
|
+
cached = JSON.parse(readFileSync3(join13(location, "package.json"), "utf8"));
|
|
3530
|
+
} catch {
|
|
3531
|
+
cached = null;
|
|
3532
|
+
}
|
|
3533
|
+
cache.set(location, cached ?? null);
|
|
3534
|
+
}
|
|
3535
|
+
return cached ?? null;
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
function formatSeverityCounts(counts, suffix = "") {
|
|
3539
|
+
const parts = [];
|
|
3540
|
+
if (counts.critical) parts.push(colors_default.red(`${counts.critical} critical${suffix}`));
|
|
3541
|
+
if (counts.high) parts.push(colors_default.red(`${counts.high} high${suffix}`));
|
|
3542
|
+
if (counts.moderate) parts.push(colors_default.yellow(`${counts.moderate} moderate${suffix}`));
|
|
3543
|
+
if (counts.low) parts.push(colors_default.blue(`${counts.low} low`));
|
|
3544
|
+
return parts;
|
|
3545
|
+
}
|
|
3546
|
+
async function runAudit(config) {
|
|
3368
3547
|
const { forkPath } = config;
|
|
3369
|
-
if (
|
|
3548
|
+
if (config.checkOverrides) {
|
|
3370
3549
|
await checkOverrides(forkPath);
|
|
3371
3550
|
return;
|
|
3372
3551
|
}
|
|
3373
3552
|
const spinner = createSpinner("checking for outdated packages & vulnerabilities...");
|
|
3374
3553
|
try {
|
|
3375
|
-
if (
|
|
3554
|
+
if (config.force) {
|
|
3376
3555
|
spinnerText("clearing pnpm metadata cache...");
|
|
3377
3556
|
clearCache();
|
|
3378
|
-
|
|
3557
|
+
spawnSync2("pnpm", ["cache", "delete", "*"], {
|
|
3379
3558
|
cwd: forkPath,
|
|
3380
3559
|
stdio: "ignore"
|
|
3381
3560
|
});
|
|
@@ -3389,25 +3568,13 @@ async function runAudit(config, options = {}) {
|
|
|
3389
3568
|
return;
|
|
3390
3569
|
}
|
|
3391
3570
|
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
|
-
};
|
|
3571
|
+
spinnerText(`found ${packageNames.length} outdated package(s), fetching metadata...`);
|
|
3572
|
+
const readDependentPkg = createDependentPkgReader();
|
|
3406
3573
|
const dependentNameCache = /* @__PURE__ */ new Map();
|
|
3407
3574
|
for (const name of packageNames) {
|
|
3408
3575
|
for (const dep of outdatedPackages[name].dependentPackages) {
|
|
3409
3576
|
if (!dependentNameCache.has(dep.location)) {
|
|
3410
|
-
dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name ||
|
|
3577
|
+
dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name || basename5(dep.location));
|
|
3411
3578
|
}
|
|
3412
3579
|
}
|
|
3413
3580
|
}
|
|
@@ -3518,7 +3685,7 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3518
3685
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
3519
3686
|
for (const pkg of enhancedPackages) {
|
|
3520
3687
|
for (const loc of pkg.dependentLocations) {
|
|
3521
|
-
const dependentName = dependentNameCache.get(loc) ||
|
|
3688
|
+
const dependentName = dependentNameCache.get(loc) || basename5(loc);
|
|
3522
3689
|
const existing = dependentMap.get(dependentName);
|
|
3523
3690
|
if (existing) {
|
|
3524
3691
|
existing.count++;
|
|
@@ -3530,34 +3697,15 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3530
3697
|
const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
3531
3698
|
const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
|
|
3532
3699
|
for (const [dependentName, { location, count }] of sortedDependents) {
|
|
3533
|
-
const packageJsonPath2 =
|
|
3534
|
-
const relativePath =
|
|
3700
|
+
const packageJsonPath2 = join13(location, "package.json");
|
|
3701
|
+
const relativePath = relative2(forkPath, packageJsonPath2);
|
|
3535
3702
|
const paddedName = dependentName.padEnd(maxDependentNameLen);
|
|
3536
3703
|
console.info(
|
|
3537
3704
|
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count}`)}`
|
|
3538
3705
|
);
|
|
3539
3706
|
}
|
|
3540
3707
|
}
|
|
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();
|
|
3708
|
+
function buildUpdateChoices(packages, auditResult, hasVulnerabilities) {
|
|
3561
3709
|
const makeChoice = (pkg) => {
|
|
3562
3710
|
const devTag = pkg.isDev ? colors_default.dim(" (dev)") : "";
|
|
3563
3711
|
const version = `${colors_default.red(pkg.current)} \u2192 ${colors_default.green(pkg.latest)}`;
|
|
@@ -3568,17 +3716,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3568
3716
|
checked: false
|
|
3569
3717
|
};
|
|
3570
3718
|
};
|
|
3571
|
-
const AUDIT_FIX_VALUE = "__audit_fix__";
|
|
3572
3719
|
const pinned = packages.filter((p) => p.pinnedIn.length > 0);
|
|
3573
3720
|
const regular = packages.filter((p) => p.pinnedIn.length === 0);
|
|
3574
3721
|
const choices = [];
|
|
3575
3722
|
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`));
|
|
3723
|
+
const parts = formatSeverityCounts(auditResult?.metadata?.vulnerabilities || {});
|
|
3582
3724
|
const vulnSummary = parts.length > 0 ? ` ${colors_default.dim("\xB7")} ${parts.join(" ")}` : "";
|
|
3583
3725
|
choices.push(new Separator(colors_default.red(`\u2500\u2500 vulnerabilities${vulnSummary} \u2500\u2500`)));
|
|
3584
3726
|
choices.push({
|
|
@@ -3597,6 +3739,29 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3597
3739
|
}
|
|
3598
3740
|
choices.push(...regular.map(makeChoice));
|
|
3599
3741
|
}
|
|
3742
|
+
return choices;
|
|
3743
|
+
}
|
|
3744
|
+
async function promptForUpdates(packages, forkPath, auditResult) {
|
|
3745
|
+
const hasVulnerabilities = auditResult !== null && Object.values(auditResult.metadata?.vulnerabilities || {}).some((c) => c > 0);
|
|
3746
|
+
const hasPackages = packages.length > 0;
|
|
3747
|
+
let confirmMessage = "update packages?";
|
|
3748
|
+
if (hasVulnerabilities && hasPackages) {
|
|
3749
|
+
confirmMessage = "update packages and fix vulnerabilities?";
|
|
3750
|
+
} else if (hasVulnerabilities) {
|
|
3751
|
+
confirmMessage = "fix vulnerabilities?";
|
|
3752
|
+
}
|
|
3753
|
+
let wantsUpdate;
|
|
3754
|
+
try {
|
|
3755
|
+
wantsUpdate = await confirm({
|
|
3756
|
+
message: confirmMessage,
|
|
3757
|
+
default: true
|
|
3758
|
+
});
|
|
3759
|
+
} catch {
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (!wantsUpdate) return;
|
|
3763
|
+
console.info();
|
|
3764
|
+
const choices = buildUpdateChoices(packages, auditResult, hasVulnerabilities);
|
|
3600
3765
|
let selected;
|
|
3601
3766
|
try {
|
|
3602
3767
|
selected = await checkbox({
|
|
@@ -3624,6 +3789,9 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3624
3789
|
}
|
|
3625
3790
|
const runAuditFix = selected.includes(AUDIT_FIX_VALUE);
|
|
3626
3791
|
const selectedPackages = selected.filter((s) => s !== AUDIT_FIX_VALUE);
|
|
3792
|
+
await executeUpdates(packages, forkPath, selectedPackages, runAuditFix);
|
|
3793
|
+
}
|
|
3794
|
+
async function executeUpdates(packages, forkPath, selectedPackages, runAuditFix) {
|
|
3627
3795
|
const summaryParts = [];
|
|
3628
3796
|
if (selectedPackages.length > 0) {
|
|
3629
3797
|
summaryParts.push(`updating ${selectedPackages.length} package${selectedPackages.length !== 1 ? "s" : ""}`);
|
|
@@ -3655,17 +3823,15 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3655
3823
|
for (const pkgName of selectedPackages) {
|
|
3656
3824
|
const pkg = packages.find((p) => p.name === pkgName);
|
|
3657
3825
|
if (!pkg) continue;
|
|
3658
|
-
for (
|
|
3659
|
-
const
|
|
3660
|
-
if (
|
|
3661
|
-
|
|
3662
|
-
}
|
|
3663
|
-
workspacePackages.get(dependent)?.packages.push(pkgName);
|
|
3826
|
+
for (const dependent of pkg.dependents) {
|
|
3827
|
+
const group = workspacePackages.get(dependent);
|
|
3828
|
+
if (group) group.push(pkgName);
|
|
3829
|
+
else workspacePackages.set(dependent, [pkgName]);
|
|
3664
3830
|
}
|
|
3665
3831
|
}
|
|
3666
3832
|
if (runAuditFix) {
|
|
3667
3833
|
console.info(colors_default.dim("running pnpm audit --fix override..."));
|
|
3668
|
-
const auditFixResult =
|
|
3834
|
+
const auditFixResult = spawnSync2("pnpm", ["audit", "--fix", "override"], {
|
|
3669
3835
|
cwd: forkPath,
|
|
3670
3836
|
stdio: "inherit"
|
|
3671
3837
|
});
|
|
@@ -3676,7 +3842,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3676
3842
|
console.error(colors_default.red(`\u2717 pnpm audit --fix override failed (exit code ${auditFixResult.status})`));
|
|
3677
3843
|
}
|
|
3678
3844
|
}
|
|
3679
|
-
for (const [workspace,
|
|
3845
|
+
for (const [workspace, pkgs] of workspacePackages) {
|
|
3680
3846
|
const pkgList = pkgs.join(", ");
|
|
3681
3847
|
console.info(`${colors_default.cyan("\u2191")} ${colors_default.bold(workspace)}: ${colors_default.dim(pkgList)}`);
|
|
3682
3848
|
}
|
|
@@ -3685,7 +3851,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3685
3851
|
const selectedCatalogPackages = selectedPackages.filter((pkgName) => catalogPackages.has(pkgName));
|
|
3686
3852
|
if (selectedCatalogPackages.length > 0) {
|
|
3687
3853
|
console.info(colors_default.dim(`normalizing catalog consumers: ${selectedCatalogPackages.join(", ")}`));
|
|
3688
|
-
const normalizeResult =
|
|
3854
|
+
const normalizeResult = spawnSync2("pnpm", ["-r", "up", ...selectedCatalogPackages], {
|
|
3689
3855
|
cwd: forkPath,
|
|
3690
3856
|
stdio: "inherit"
|
|
3691
3857
|
});
|
|
@@ -3695,7 +3861,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3695
3861
|
return;
|
|
3696
3862
|
}
|
|
3697
3863
|
}
|
|
3698
|
-
const result =
|
|
3864
|
+
const result = spawnSync2("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
|
|
3699
3865
|
cwd: forkPath,
|
|
3700
3866
|
stdio: "inherit"
|
|
3701
3867
|
});
|
|
@@ -3710,15 +3876,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3710
3876
|
function printVulnerabilityResults(auditResult, vulnMap) {
|
|
3711
3877
|
if (auditResult && vulnMap.size > 0) {
|
|
3712
3878
|
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
3879
|
console.info();
|
|
3718
3880
|
console.info(
|
|
3719
|
-
`${colors_default.red("\u26A0")} ${colors_default.bold("vulnerabilities")} ${colors_default.dim("\xB7")}
|
|
3881
|
+
`${colors_default.red("\u26A0")} ${colors_default.bold("vulnerabilities")} ${colors_default.dim("\xB7")} ${formatSeverityCounts(vulnMeta, " ").join("")}`
|
|
3720
3882
|
);
|
|
3721
|
-
console.info(
|
|
3883
|
+
console.info(DIVIDER);
|
|
3722
3884
|
for (const [pkgName, vulns] of vulnMap.entries()) {
|
|
3723
3885
|
for (const vuln of vulns) {
|
|
3724
3886
|
const severityIcon = getVulnIcon(vuln.severity);
|
|
@@ -3738,14 +3900,14 @@ function printVulnerabilityResults(auditResult, vulnMap) {
|
|
|
3738
3900
|
}
|
|
3739
3901
|
} else {
|
|
3740
3902
|
console.info();
|
|
3741
|
-
console.info(`${
|
|
3903
|
+
console.info(`${checkMark} no vulnerabilities found`);
|
|
3742
3904
|
}
|
|
3743
3905
|
console.info();
|
|
3744
3906
|
}
|
|
3745
3907
|
function readCatalogPackageNames(forkPath) {
|
|
3746
|
-
const workspacePath =
|
|
3908
|
+
const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
|
|
3747
3909
|
try {
|
|
3748
|
-
const content =
|
|
3910
|
+
const content = readFileSync3(workspacePath, "utf8");
|
|
3749
3911
|
return new Set(Object.keys(parseYamlBlockMap(content, "catalog")));
|
|
3750
3912
|
} catch {
|
|
3751
3913
|
return /* @__PURE__ */ new Set();
|
|
@@ -3753,18 +3915,18 @@ function readCatalogPackageNames(forkPath) {
|
|
|
3753
3915
|
}
|
|
3754
3916
|
function readOverrides(forkPath) {
|
|
3755
3917
|
const overrides = /* @__PURE__ */ new Map();
|
|
3756
|
-
const pkgJsonPath =
|
|
3918
|
+
const pkgJsonPath = join13(forkPath, "package.json");
|
|
3757
3919
|
try {
|
|
3758
|
-
const pkgJson = JSON.parse(
|
|
3920
|
+
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
3759
3921
|
const pnpmOverrides = pkgJson.pnpm?.overrides || {};
|
|
3760
3922
|
for (const [key, value] of Object.entries(pnpmOverrides)) {
|
|
3761
3923
|
overrides.set(key, { target: value, source: "package.json" });
|
|
3762
3924
|
}
|
|
3763
3925
|
} catch {
|
|
3764
3926
|
}
|
|
3765
|
-
const workspacePath =
|
|
3927
|
+
const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
|
|
3766
3928
|
try {
|
|
3767
|
-
const content =
|
|
3929
|
+
const content = readFileSync3(workspacePath, "utf8");
|
|
3768
3930
|
const wsOverrides = parseYamlBlockMap(content, "overrides");
|
|
3769
3931
|
for (const [key, value] of Object.entries(wsOverrides)) {
|
|
3770
3932
|
overrides.set(key, { target: value, source: "pnpm-workspace.yaml" });
|
|
@@ -3807,7 +3969,7 @@ async function checkOverrides(forkPath) {
|
|
|
3807
3969
|
const maxTargetLen = Math.max(6, ...allValues.map((v) => v.target.length));
|
|
3808
3970
|
if (securityOverrides.length > 0) {
|
|
3809
3971
|
console.info(colors_default.bold(`security overrides (${securityOverrides.length})`));
|
|
3810
|
-
console.info(
|
|
3972
|
+
console.info(DIVIDER);
|
|
3811
3973
|
for (const o of securityOverrides) {
|
|
3812
3974
|
const icon = o.status === "active" ? colors_default.red("\u25CF") : colors_default.green("\u25CB");
|
|
3813
3975
|
const statusText = o.status === "active" ? colors_default.red("active") : colors_default.green("likely stale");
|
|
@@ -3820,7 +3982,7 @@ async function checkOverrides(forkPath) {
|
|
|
3820
3982
|
}
|
|
3821
3983
|
if (pinOverrides.length > 0) {
|
|
3822
3984
|
console.info(colors_default.bold(`version pins (${pinOverrides.length})`));
|
|
3823
|
-
console.info(
|
|
3985
|
+
console.info(DIVIDER);
|
|
3824
3986
|
for (const o of pinOverrides) {
|
|
3825
3987
|
const keyText = colors_default.white(o.key.padEnd(maxKeyLen));
|
|
3826
3988
|
const targetText = colors_default.dim(o.target.padEnd(maxTargetLen));
|
|
@@ -3833,26 +3995,24 @@ async function checkOverrides(forkPath) {
|
|
|
3833
3995
|
const activeCount = securityOverrides.filter((o) => o.status === "active").length;
|
|
3834
3996
|
if (staleCount > 0) {
|
|
3835
3997
|
console.info(
|
|
3836
|
-
`${colors_default.green("\u25CB")} ${staleCount} override${staleCount !== 1 ? "s" : ""} likely stale
|
|
3998
|
+
`${colors_default.green("\u25CB")} ${staleCount} override${staleCount !== 1 ? "s" : ""} likely stale: no matching advisory found in current audit`
|
|
3837
3999
|
);
|
|
3838
|
-
console.info(colors_default.dim(" overrides mask vulnerabilities
|
|
4000
|
+
console.info(colors_default.dim(" overrides mask vulnerabilities: verify by temporarily removing and running pnpm audit"));
|
|
3839
4001
|
}
|
|
3840
4002
|
if (activeCount > 0) {
|
|
3841
4003
|
console.info(
|
|
3842
|
-
`${colors_default.red("\u25CF")} ${activeCount} override${activeCount !== 1 ? "s" : ""} still active
|
|
4004
|
+
`${colors_default.red("\u25CF")} ${activeCount} override${activeCount !== 1 ? "s" : ""} still active: advisory present without override`
|
|
3843
4005
|
);
|
|
3844
4006
|
}
|
|
3845
4007
|
if (pinOverrides.length > 0) {
|
|
3846
4008
|
console.info(
|
|
3847
|
-
`${colors_default.blue("\u25C6")} ${pinOverrides.length} version pin${pinOverrides.length !== 1 ? "s" : ""}
|
|
4009
|
+
`${colors_default.blue("\u25C6")} ${pinOverrides.length} version pin${pinOverrides.length !== 1 ? "s" : ""}: not security-related, review manually`
|
|
3848
4010
|
);
|
|
3849
4011
|
}
|
|
3850
4012
|
console.info();
|
|
3851
4013
|
}
|
|
3852
4014
|
|
|
3853
4015
|
// src/services/contributions.ts
|
|
3854
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
3855
|
-
import { join as join16 } from "path";
|
|
3856
4016
|
import {
|
|
3857
4017
|
createPrompt,
|
|
3858
4018
|
isDownKey,
|
|
@@ -3860,14 +4020,13 @@ import {
|
|
|
3860
4020
|
isSpaceKey,
|
|
3861
4021
|
isUpKey,
|
|
3862
4022
|
useKeypress,
|
|
3863
|
-
useMemo,
|
|
3864
4023
|
usePagination,
|
|
3865
4024
|
useState
|
|
3866
4025
|
} from "@inquirer/core";
|
|
3867
4026
|
import { select as select2 } from "@inquirer/prompts";
|
|
3868
4027
|
|
|
3869
4028
|
// src/services/contrib-core.ts
|
|
3870
|
-
import { dirname as dirname5, join as
|
|
4029
|
+
import { dirname as dirname5, join as join14, resolve as resolve5 } from "path";
|
|
3871
4030
|
async function listFilesAtRef(repoPath, ref) {
|
|
3872
4031
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
3873
4032
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -3875,8 +4034,8 @@ async function listFilesAtRef(repoPath, ref) {
|
|
|
3875
4034
|
async function detectContributableFiles(repoPath, baseRef, forkRef, config, forkTerritory = []) {
|
|
3876
4035
|
const mergeBase = (await git(["merge-base", baseRef, forkRef], repoPath, { ignoreErrors: true })).trim() || baseRef;
|
|
3877
4036
|
const predicates = {
|
|
3878
|
-
isIgnored: (
|
|
3879
|
-
isPinned: (
|
|
4037
|
+
isIgnored: (path) => isIgnored(path, config) || isUnderAnyFolder(path, forkTerritory),
|
|
4038
|
+
isPinned: (path) => isPinned(path, config)
|
|
3880
4039
|
};
|
|
3881
4040
|
const analyzed = await analyzeRefs(repoPath, baseRef, forkRef, mergeBase, predicates);
|
|
3882
4041
|
await enrichChangeInfo(repoPath, analyzed, mergeBase, baseRef, forkRef);
|
|
@@ -3931,7 +4090,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
3931
4090
|
async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
|
|
3932
4091
|
const branch = `contrib/${forkName}`;
|
|
3933
4092
|
const gitDir = resolve5(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
3934
|
-
const tmpIndex =
|
|
4093
|
+
const tmpIndex = join14(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
3935
4094
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
3936
4095
|
try {
|
|
3937
4096
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -4004,20 +4163,14 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
|
4004
4163
|
if (patch.length === 0) return null;
|
|
4005
4164
|
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
4006
4165
|
}
|
|
4007
|
-
function shouldApplyContributionsUnstaged(repoPath) {
|
|
4008
|
-
return JSON.parse(readFileSync4(join16(repoPath, "package.json"), "utf8")).name === "cella";
|
|
4009
|
-
}
|
|
4010
4166
|
var contribPrompt = createPrompt((config, done) => {
|
|
4011
4167
|
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
4012
4168
|
const [items, setItems] = useState(config.items);
|
|
4013
4169
|
const [active, setActive] = useState(0);
|
|
4014
4170
|
const [statusMsg, setStatusMsg] = useState("");
|
|
4015
4171
|
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
4172
|
useKeypress(async (key) => {
|
|
4173
|
+
const last = items.length - 1;
|
|
4021
4174
|
if (items.length === 0) {
|
|
4022
4175
|
if (isEnterKey(key) || key.name === "q") {
|
|
4023
4176
|
setPromptStatus("done");
|
|
@@ -4041,22 +4194,22 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4041
4194
|
return;
|
|
4042
4195
|
}
|
|
4043
4196
|
if (key.ctrl && isUpKey(key)) {
|
|
4044
|
-
setActive(
|
|
4197
|
+
setActive(0);
|
|
4045
4198
|
setStatusMsg("");
|
|
4046
4199
|
return;
|
|
4047
4200
|
}
|
|
4048
4201
|
if (key.ctrl && isDownKey(key)) {
|
|
4049
|
-
setActive(
|
|
4202
|
+
setActive(last);
|
|
4050
4203
|
setStatusMsg("");
|
|
4051
4204
|
return;
|
|
4052
4205
|
}
|
|
4053
4206
|
if (isUpKey(key)) {
|
|
4054
|
-
setActive(active <=
|
|
4207
|
+
setActive(active <= 0 ? 0 : active - 1);
|
|
4055
4208
|
setStatusMsg("");
|
|
4056
4209
|
return;
|
|
4057
4210
|
}
|
|
4058
4211
|
if (isDownKey(key)) {
|
|
4059
|
-
setActive(active >=
|
|
4212
|
+
setActive(active >= last ? last : active + 1);
|
|
4060
4213
|
setStatusMsg("");
|
|
4061
4214
|
return;
|
|
4062
4215
|
}
|
|
@@ -4069,7 +4222,7 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4069
4222
|
const pagePath = await showContribDiff(items[active], baseRef, cwd, forkName);
|
|
4070
4223
|
setStatusMsg(pagePath ? `opened ${items[active].path} in browser` : `no changes for ${items[active].path}`);
|
|
4071
4224
|
} catch (error) {
|
|
4072
|
-
setStatusMsg(`diff failed: ${error
|
|
4225
|
+
setStatusMsg(`diff failed: ${errorMessage(error)}`);
|
|
4073
4226
|
}
|
|
4074
4227
|
return;
|
|
4075
4228
|
}
|
|
@@ -4083,12 +4236,12 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4083
4236
|
if (promptStatus === "done") {
|
|
4084
4237
|
const checked = items.filter((i) => i.checked);
|
|
4085
4238
|
if (checked.length > 0) {
|
|
4086
|
-
return `${
|
|
4239
|
+
return `${checkMark} ${checked.length} files accepted`;
|
|
4087
4240
|
}
|
|
4088
|
-
return `${
|
|
4241
|
+
return `${checkMark} done`;
|
|
4089
4242
|
}
|
|
4090
4243
|
if (items.length === 0) {
|
|
4091
|
-
return `${
|
|
4244
|
+
return `${checkMark} no contributions: press enter`;
|
|
4092
4245
|
}
|
|
4093
4246
|
const checkedCount = items.filter((i) => i.checked).length;
|
|
4094
4247
|
const page = usePagination({
|
|
@@ -4122,6 +4275,38 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4122
4275
|
const lines = [header, page, statusLine, helpLine].filter(Boolean).join("\n").trimEnd();
|
|
4123
4276
|
return `${lines}\x1B[?25l`;
|
|
4124
4277
|
});
|
|
4278
|
+
async function selectFork(config, validated) {
|
|
4279
|
+
if (config.fork) {
|
|
4280
|
+
const match = validated.find((v) => v.fork.name === config.fork);
|
|
4281
|
+
if (!match?.valid) {
|
|
4282
|
+
throw new Error(`fork '${config.fork}' not found or invalid in config`);
|
|
4283
|
+
}
|
|
4284
|
+
return match;
|
|
4285
|
+
}
|
|
4286
|
+
const validForks = validated.filter((v) => v.valid);
|
|
4287
|
+
if (config.list || config.json || config.diff) {
|
|
4288
|
+
if (validForks.length === 0) {
|
|
4289
|
+
throw new Error("no valid forks configured");
|
|
4290
|
+
}
|
|
4291
|
+
if (validForks.length > 1) {
|
|
4292
|
+
throw new Error("multiple forks configured; pass --fork <name> to choose one");
|
|
4293
|
+
}
|
|
4294
|
+
return validForks[0];
|
|
4295
|
+
}
|
|
4296
|
+
const choices = validated.map((v) => ({
|
|
4297
|
+
value: v.fork.name,
|
|
4298
|
+
name: v.valid ? `${v.fork.name} ${colors_default.dim(`[${v.fork.pullBranch}] ${v.fork.localPath}`)}` : `${v.fork.name} ${colors_default.dim(v.fork.localPath)}`,
|
|
4299
|
+
disabled: v.valid ? false : v.error ?? "invalid"
|
|
4300
|
+
}));
|
|
4301
|
+
const picked = await select2({
|
|
4302
|
+
message: "select fork to pull contributions from:",
|
|
4303
|
+
choices,
|
|
4304
|
+
loop: false
|
|
4305
|
+
});
|
|
4306
|
+
const selected = validated.find((v) => v.fork.name === picked);
|
|
4307
|
+
if (!selected) console.info(colors_default.dim("no fork selected."));
|
|
4308
|
+
return selected;
|
|
4309
|
+
}
|
|
4125
4310
|
async function fetchForkBranch(cellaPath, fetchSource, pullBranch) {
|
|
4126
4311
|
await git(["fetch", fetchSource, pullBranch], cellaPath);
|
|
4127
4312
|
return git(["rev-parse", "FETCH_HEAD"], cellaPath);
|
|
@@ -4149,42 +4334,8 @@ async function runContributions(config) {
|
|
|
4149
4334
|
}
|
|
4150
4335
|
const forkBasePath = await resolveForkBasePath(config.forkPath);
|
|
4151
4336
|
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
|
-
}
|
|
4337
|
+
const selectedFork = await selectFork(config, validated);
|
|
4338
|
+
if (!selectedFork) return;
|
|
4188
4339
|
const { fork, resolvedPath } = selectedFork;
|
|
4189
4340
|
const forkName = fork.name;
|
|
4190
4341
|
createSpinner("pulling fork contributions...");
|
|
@@ -4207,40 +4358,28 @@ async function runContributions(config) {
|
|
|
4207
4358
|
if (appliedFiles.length > 0) {
|
|
4208
4359
|
const stat = await getDiffStat(config.forkPath, baseRef, branch);
|
|
4209
4360
|
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,
|
|
4361
|
+
const toItem = (path, deleted) => {
|
|
4362
|
+
const fileMeta = metaByPath.get(path);
|
|
4363
|
+
return {
|
|
4364
|
+
path,
|
|
4229
4365
|
ref: branch,
|
|
4230
|
-
deleted
|
|
4366
|
+
deleted,
|
|
4367
|
+
...deleted ? {} : { created: fileMeta?.kind === "created" },
|
|
4231
4368
|
status: fileMeta?.status,
|
|
4232
4369
|
changedAt: fileMeta?.changedAt,
|
|
4233
4370
|
changedTs: fileMeta?.changedTs,
|
|
4234
|
-
additions: stat.get(
|
|
4235
|
-
deletions: stat.get(
|
|
4371
|
+
additions: stat.get(path)?.additions ?? null,
|
|
4372
|
+
deletions: stat.get(path)?.deletions ?? null,
|
|
4236
4373
|
checked: false
|
|
4237
|
-
}
|
|
4238
|
-
}
|
|
4374
|
+
};
|
|
4375
|
+
};
|
|
4376
|
+
for (const path of [...detection.modified, ...detection.created]) allItems.push(toItem(path, false));
|
|
4377
|
+
for (const path of detection.deleted) allItems.push(toItem(path, true));
|
|
4239
4378
|
}
|
|
4240
4379
|
}
|
|
4241
4380
|
} catch (error) {
|
|
4242
4381
|
spinnerFail(`failed to pull ${forkName}`);
|
|
4243
|
-
console.info(colors_default.red(` \u2717 ${error
|
|
4382
|
+
console.info(colors_default.red(` \u2717 ${errorMessage(error)}`));
|
|
4244
4383
|
return;
|
|
4245
4384
|
}
|
|
4246
4385
|
if (allItems.length === 0) {
|
|
@@ -4251,15 +4390,13 @@ async function runContributions(config) {
|
|
|
4251
4390
|
spinnerSuccess(`${allItems.length} files from ${forkName}`);
|
|
4252
4391
|
if (!config.list && !config.diff && forkBanner) {
|
|
4253
4392
|
console.info(
|
|
4254
|
-
` ${colors_default.dim(`${forkName}: comparing committed '${forkBanner.pullBranch}' @ ${forkBanner.sha} (${forkBanner.date})
|
|
4393
|
+
` ${colors_default.dim(`${forkName}: comparing committed '${forkBanner.pullBranch}' @ ${forkBanner.sha} (${forkBanner.date}); uncommitted fork changes are not included`)}`
|
|
4255
4394
|
);
|
|
4256
4395
|
}
|
|
4257
4396
|
if (config.diff) {
|
|
4258
4397
|
const matches = allItems.filter((i) => i.path === config.diff);
|
|
4259
4398
|
if (matches.length === 0) {
|
|
4260
|
-
|
|
4261
|
-
process.exitCode = 1;
|
|
4262
|
-
return;
|
|
4399
|
+
throw new Error(`no contribution found for path: ${config.diff}`);
|
|
4263
4400
|
}
|
|
4264
4401
|
for (const item of matches) {
|
|
4265
4402
|
const diff = gitDiffFile(config.forkPath, `${baseRef}..${item.ref}`, item.path, { dstPrefix: forkName });
|
|
@@ -4286,7 +4423,7 @@ async function runContributions(config) {
|
|
|
4286
4423
|
const status = item.status ?? "behind";
|
|
4287
4424
|
const kind = item.deleted ? "deleted" : "modified";
|
|
4288
4425
|
const changedAt = item.changedAt ?? "-";
|
|
4289
|
-
|
|
4426
|
+
writeStdout(`${forkName} ${status} ${kind} ${changedAt} ${item.path}`);
|
|
4290
4427
|
}
|
|
4291
4428
|
return;
|
|
4292
4429
|
}
|
|
@@ -4305,7 +4442,7 @@ async function runContributions(config) {
|
|
|
4305
4442
|
return;
|
|
4306
4443
|
}
|
|
4307
4444
|
createSpinner(`applying ${selected.length} files...`);
|
|
4308
|
-
const applyUnstaged =
|
|
4445
|
+
const applyUnstaged = isUpstreamRepo(config.forkPath);
|
|
4309
4446
|
let applied = 0;
|
|
4310
4447
|
const errors = [];
|
|
4311
4448
|
for (const item of selected) {
|
|
@@ -4329,7 +4466,7 @@ async function runContributions(config) {
|
|
|
4329
4466
|
}
|
|
4330
4467
|
applied++;
|
|
4331
4468
|
} catch (error) {
|
|
4332
|
-
errors.push(`${item.path}: ${error
|
|
4469
|
+
errors.push(`${item.path}: ${errorMessage(error)}`);
|
|
4333
4470
|
}
|
|
4334
4471
|
}
|
|
4335
4472
|
if (errors.length > 0) {
|
|
@@ -4343,7 +4480,7 @@ async function runContributions(config) {
|
|
|
4343
4480
|
console.info();
|
|
4344
4481
|
console.info(
|
|
4345
4482
|
colors_default.dim(
|
|
4346
|
-
applyUnstaged ? " files are unstaged
|
|
4483
|
+
applyUnstaged ? " files are unstaged: review, stage what you want, and commit when ready" : " files are staged: review and commit when ready"
|
|
4347
4484
|
)
|
|
4348
4485
|
);
|
|
4349
4486
|
console.info();
|
|
@@ -4355,16 +4492,16 @@ import process8 from "process";
|
|
|
4355
4492
|
import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
|
|
4356
4493
|
|
|
4357
4494
|
// src/services/sync.ts
|
|
4358
|
-
import { spawnSync as
|
|
4495
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4359
4496
|
import { select as select3 } from "@inquirer/prompts";
|
|
4360
4497
|
|
|
4361
4498
|
// src/utils/gh.ts
|
|
4362
|
-
import { spawnSync as
|
|
4499
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4363
4500
|
function ghAvailable() {
|
|
4364
|
-
return
|
|
4501
|
+
return spawnSync3("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
4365
4502
|
}
|
|
4366
4503
|
function runGh(cwd, args) {
|
|
4367
|
-
const result =
|
|
4504
|
+
const result = spawnSync3("gh", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4368
4505
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
4369
4506
|
return { ok: result.status === 0, output };
|
|
4370
4507
|
}
|
|
@@ -4404,12 +4541,12 @@ function closePr(cwd, ref) {
|
|
|
4404
4541
|
}
|
|
4405
4542
|
|
|
4406
4543
|
// src/services/packages.ts
|
|
4407
|
-
import { existsSync as existsSync11, readFileSync as
|
|
4408
|
-
import { join as
|
|
4544
|
+
import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
4545
|
+
import { join as join16 } from "path";
|
|
4409
4546
|
|
|
4410
4547
|
// src/services/package-usage.ts
|
|
4411
|
-
import { existsSync as existsSync10, readFileSync as
|
|
4412
|
-
import { join as
|
|
4548
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4, statSync } from "fs";
|
|
4549
|
+
import { join as join15 } from "path";
|
|
4413
4550
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4414
4551
|
function escapeRegExp(value) {
|
|
4415
4552
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4417,41 +4554,41 @@ function escapeRegExp(value) {
|
|
|
4417
4554
|
function readSearchable(filePath) {
|
|
4418
4555
|
try {
|
|
4419
4556
|
if (statSync(filePath).size > MAX_FILE_BYTES) return "";
|
|
4420
|
-
const text =
|
|
4557
|
+
const text = readFileSync4(filePath, "utf8");
|
|
4421
4558
|
return text.includes("\0") ? "" : text;
|
|
4422
4559
|
} catch {
|
|
4423
4560
|
return "";
|
|
4424
4561
|
}
|
|
4425
4562
|
}
|
|
4426
|
-
function fileSource(forkPath,
|
|
4563
|
+
function fileSource(forkPath, path) {
|
|
4427
4564
|
let text;
|
|
4428
|
-
return { path
|
|
4565
|
+
return { path, label: path, text: () => text ??= readSearchable(join15(forkPath, path)) };
|
|
4429
4566
|
}
|
|
4430
|
-
async function readScriptsAtRef(forkPath, ref,
|
|
4567
|
+
async function readScriptsAtRef(forkPath, ref, path) {
|
|
4431
4568
|
try {
|
|
4432
|
-
return JSON.parse(await git(["show", `${ref}:${
|
|
4569
|
+
return JSON.parse(await git(["show", `${ref}:${path}`], forkPath)).scripts ?? {};
|
|
4433
4570
|
} catch {
|
|
4434
4571
|
return {};
|
|
4435
4572
|
}
|
|
4436
4573
|
}
|
|
4437
|
-
async function scriptSources(forkPath, upstreamRef, baseRef,
|
|
4574
|
+
async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
4438
4575
|
let forkScripts;
|
|
4439
4576
|
try {
|
|
4440
|
-
forkScripts = JSON.parse(
|
|
4577
|
+
forkScripts = JSON.parse(readFileSync4(join15(forkPath, path), "utf8")).scripts ?? {};
|
|
4441
4578
|
} catch {
|
|
4442
4579
|
return [];
|
|
4443
4580
|
}
|
|
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
|
|
4581
|
+
const baseScripts = await readScriptsAtRef(forkPath, baseRef, path);
|
|
4582
|
+
const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef, path);
|
|
4583
|
+
return Object.entries(forkScripts).filter(([name, value]) => value !== baseScripts[name] && value !== upstreamScripts[name]).map(([name, value]) => ({ path, label: `${path} (scripts.${name})`, text: () => value }));
|
|
4447
4584
|
}
|
|
4448
4585
|
function binNames(forkPath, location, name) {
|
|
4449
4586
|
const bareName = name.split("/").pop();
|
|
4450
|
-
for (const dir of [
|
|
4451
|
-
const manifest =
|
|
4587
|
+
for (const dir of [join15(forkPath, location), forkPath]) {
|
|
4588
|
+
const manifest = join15(dir, "node_modules", name, "package.json");
|
|
4452
4589
|
if (!existsSync10(manifest)) continue;
|
|
4453
4590
|
try {
|
|
4454
|
-
const { bin } = JSON.parse(
|
|
4591
|
+
const { bin } = JSON.parse(readFileSync4(manifest, "utf8"));
|
|
4455
4592
|
if (typeof bin === "string") return [bareName];
|
|
4456
4593
|
return bin && typeof bin === "object" ? Object.keys(bin) : [];
|
|
4457
4594
|
} catch {
|
|
@@ -4476,9 +4613,9 @@ function scriptPattern(name) {
|
|
|
4476
4613
|
async function loadForkUsage(forkPath, upstreamRef, baseRef) {
|
|
4477
4614
|
const diff = await git(["diff", "--name-only", "-z", "--diff-filter=d", upstreamRef], forkPath);
|
|
4478
4615
|
const sources = [];
|
|
4479
|
-
for (const
|
|
4480
|
-
if (isPackageJson(
|
|
4481
|
-
else if (!isManagedFile(
|
|
4616
|
+
for (const path of diff.split("\0").filter(Boolean)) {
|
|
4617
|
+
if (isPackageJson(path)) sources.push(...await scriptSources(forkPath, upstreamRef, baseRef, path));
|
|
4618
|
+
else if (!isManagedFile(path)) sources.push(fileSource(forkPath, path));
|
|
4482
4619
|
}
|
|
4483
4620
|
const findIn = (scoped, patterns) => scoped.find((source) => patterns.some((pattern) => pattern.test(source.text())))?.label;
|
|
4484
4621
|
return {
|
|
@@ -4631,14 +4768,14 @@ function setTopLevelKey(pkg, key, value) {
|
|
|
4631
4768
|
function readPackageJson(filePath) {
|
|
4632
4769
|
if (!existsSync11(filePath)) return null;
|
|
4633
4770
|
try {
|
|
4634
|
-
return JSON.parse(
|
|
4771
|
+
return JSON.parse(readFileSync5(filePath, "utf8"));
|
|
4635
4772
|
} catch {
|
|
4636
4773
|
return null;
|
|
4637
4774
|
}
|
|
4638
4775
|
}
|
|
4639
4776
|
function writePackageJson(filePath, data) {
|
|
4640
4777
|
writeFileSync4(filePath, `${JSON.stringify(data, null, 2)}
|
|
4641
|
-
`, "
|
|
4778
|
+
`, "utf8");
|
|
4642
4779
|
}
|
|
4643
4780
|
function packageJsonPath(relativePath) {
|
|
4644
4781
|
return relativePath ? `${relativePath}/package.json` : "package.json";
|
|
@@ -4667,16 +4804,16 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4667
4804
|
const changes = [];
|
|
4668
4805
|
const kept = [];
|
|
4669
4806
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4670
|
-
const pkgPath =
|
|
4807
|
+
const pkgPath = join16(forkPath, pkgRelPath);
|
|
4671
4808
|
const forkPkg = readPackageJson(pkgPath);
|
|
4672
4809
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4673
4810
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4674
4811
|
if (!upstream) return { updated: false, changes, kept };
|
|
4675
4812
|
if (!forkPkg) {
|
|
4676
|
-
const workspaceArrived = relativePath !== "" && existsSync11(
|
|
4813
|
+
const workspaceArrived = relativePath !== "" && existsSync11(join16(forkPath, relativePath));
|
|
4677
4814
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4678
4815
|
writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4679
|
-
`, "
|
|
4816
|
+
`, "utf8");
|
|
4680
4817
|
return { updated: true, changes: ["copied from upstream (new workspace)"], kept };
|
|
4681
4818
|
}
|
|
4682
4819
|
const upstreamPkg = upstream.data;
|
|
@@ -4748,7 +4885,7 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4748
4885
|
}
|
|
4749
4886
|
async function runPackages(config, options = {}) {
|
|
4750
4887
|
createSpinner("syncing package.json files...");
|
|
4751
|
-
const keysToSync = config.settings.packageJsonSync ||
|
|
4888
|
+
const keysToSync = config.settings.packageJsonSync || DEFAULT_PACKAGE_JSON_SYNC;
|
|
4752
4889
|
const conflictedSet = new Set(options.conflictedFiles ?? []);
|
|
4753
4890
|
const baseRef = await resolveMergeBase(config.forkPath, config.upstreamRef);
|
|
4754
4891
|
const usage = baseRef ? await loadForkUsage(config.forkPath, config.upstreamRef, baseRef) : null;
|
|
@@ -4770,26 +4907,26 @@ async function runPackages(config, options = {}) {
|
|
|
4770
4907
|
"package sync complete",
|
|
4771
4908
|
`${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
|
|
4772
4909
|
);
|
|
4773
|
-
for (const { path
|
|
4910
|
+
for (const { path, changes } of reports) {
|
|
4774
4911
|
if (changes.length === 0) continue;
|
|
4775
|
-
console.info(` ${
|
|
4912
|
+
console.info(` ${path}`);
|
|
4776
4913
|
for (const change of changes) console.info(` ${colors_default.dim(change)}`);
|
|
4777
4914
|
}
|
|
4778
|
-
const kept = reports.flatMap(({ path
|
|
4915
|
+
const kept = reports.flatMap(({ path, kept: kept2 }) => kept2.map((entry) => `${path} ${entry}`));
|
|
4779
4916
|
if (kept.length > 0) {
|
|
4780
4917
|
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"}:`
|
|
4918
|
+
console.info(
|
|
4919
|
+
`${warningMark} ${colors_default.yellow(`kept ${kept.length} ${kept.length === 1 ? "entry" : "entries"} upstream dropped, the fork still uses ${kept.length === 1 ? "it" : "them"}:`)}`
|
|
4783
4920
|
);
|
|
4784
|
-
for (const entry of kept) console.
|
|
4921
|
+
for (const entry of kept) console.info(` ${colors_default.dim("\u2192")} ${entry}`);
|
|
4785
4922
|
}
|
|
4786
4923
|
if (skipped.length > 0) {
|
|
4787
4924
|
console.info();
|
|
4788
|
-
console.
|
|
4789
|
-
`${warningMark} skipped ${skipped.length} conflicted package.json file(s)
|
|
4925
|
+
console.info(
|
|
4926
|
+
`${warningMark} ${colors_default.yellow(`skipped ${skipped.length} conflicted package.json file(s): resolve the conflicts, commit the merge, then rerun \`pnpm cella sync\`:`)}`
|
|
4790
4927
|
);
|
|
4791
|
-
for (const
|
|
4792
|
-
console.
|
|
4928
|
+
for (const path of skipped) {
|
|
4929
|
+
console.info(` ${colors_default.dim("\u2192")} ${path}`);
|
|
4793
4930
|
}
|
|
4794
4931
|
}
|
|
4795
4932
|
}
|
|
@@ -4797,14 +4934,14 @@ async function runPackages(config, options = {}) {
|
|
|
4797
4934
|
// src/services/sync.ts
|
|
4798
4935
|
async function resolveCycleStart(forkPath, base, currentBranch) {
|
|
4799
4936
|
if (!await branchExists(forkPath, base)) {
|
|
4800
|
-
await
|
|
4937
|
+
await fetchRemote(forkPath, "origin").catch(() => {
|
|
4801
4938
|
});
|
|
4802
|
-
console.info(colors_default.dim(`'${base}' has no local branch
|
|
4939
|
+
console.info(colors_default.dim(`'${base}' has no local branch: cutting from 'origin/${base}'.`));
|
|
4803
4940
|
return `origin/${base}`;
|
|
4804
4941
|
}
|
|
4805
4942
|
const { upstream, ahead, behind } = await getUpstreamStatus(forkPath, base);
|
|
4806
4943
|
if (!upstream) {
|
|
4807
|
-
console.info(colors_default.dim(`'${base}' has no upstream
|
|
4944
|
+
console.info(colors_default.dim(`'${base}' has no upstream: skipping the up-to-date check.`));
|
|
4808
4945
|
return base;
|
|
4809
4946
|
}
|
|
4810
4947
|
if (ahead > 0 && behind > 0) {
|
|
@@ -4817,7 +4954,7 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
|
|
|
4817
4954
|
const worktree = currentBranch === base ? null : await getBranchWorktree(forkPath, base);
|
|
4818
4955
|
if (worktree) {
|
|
4819
4956
|
console.info(
|
|
4820
|
-
colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree}
|
|
4957
|
+
colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree}; leaving it as it is.`)
|
|
4821
4958
|
);
|
|
4822
4959
|
return upstream;
|
|
4823
4960
|
}
|
|
@@ -4827,7 +4964,7 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
|
|
|
4827
4964
|
return base;
|
|
4828
4965
|
}
|
|
4829
4966
|
if (ahead > 0) {
|
|
4830
|
-
console.info(colors_default.dim(`'${base}' is ${ahead} commit(s) ahead of '${upstream}' (unpushed)
|
|
4967
|
+
console.info(colors_default.dim(`'${base}' is ${ahead} commit(s) ahead of '${upstream}' (unpushed); continuing.`));
|
|
4831
4968
|
}
|
|
4832
4969
|
return base;
|
|
4833
4970
|
}
|
|
@@ -4852,7 +4989,7 @@ async function resolveCutBase(forkPath, base) {
|
|
|
4852
4989
|
async function returnToBase(forkPath, base) {
|
|
4853
4990
|
const worktree = await getBranchWorktree(forkPath, base);
|
|
4854
4991
|
if (worktree) {
|
|
4855
|
-
console.info(colors_default.dim(`'${base}' is checked out at ${worktree}
|
|
4992
|
+
console.info(colors_default.dim(`'${base}' is checked out at ${worktree}; detaching at '${base}' here instead...`));
|
|
4856
4993
|
await switchDetached(forkPath, base);
|
|
4857
4994
|
return `detached at '${base}'`;
|
|
4858
4995
|
}
|
|
@@ -4864,30 +5001,24 @@ async function returnToBase(forkPath, base) {
|
|
|
4864
5001
|
}
|
|
4865
5002
|
async function runSync(config, options) {
|
|
4866
5003
|
createSpinner("starting sync...");
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
5004
|
+
let result;
|
|
5005
|
+
try {
|
|
5006
|
+
result = await runEngineWithSpinner(config, true);
|
|
5007
|
+
} catch (error) {
|
|
5008
|
+
spinnerSuccess();
|
|
5009
|
+
if (error instanceof UpstreamConfigChangedError) {
|
|
5010
|
+
printUpstreamOverrideChanges({ upstreamOverrides: error.report });
|
|
5011
|
+
console.info();
|
|
4875
5012
|
}
|
|
4876
|
-
|
|
5013
|
+
throw error;
|
|
5014
|
+
}
|
|
4877
5015
|
if (result.success) {
|
|
4878
5016
|
spinnerSuccess();
|
|
4879
5017
|
} else {
|
|
4880
5018
|
spinnerFail("sync completed with conflicts");
|
|
4881
5019
|
}
|
|
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
|
-
}
|
|
5020
|
+
printEngineReports(result, "merge summary");
|
|
5021
|
+
printLogFileReport(config, result.files);
|
|
4891
5022
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
|
|
4892
5023
|
printSyncComplete(result, { stagedBranch });
|
|
4893
5024
|
printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
|
|
@@ -4895,7 +5026,6 @@ async function runSync(config, options) {
|
|
|
4895
5026
|
return result;
|
|
4896
5027
|
}
|
|
4897
5028
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
4898
|
-
var PR_BODY_COMMIT_MAX = 50;
|
|
4899
5029
|
async function buildSyncCommitMessage(forkPath, upstreamRef) {
|
|
4900
5030
|
const shortSha = await getShortSha(forkPath, upstreamRef).catch(() => "");
|
|
4901
5031
|
if (!shortSha) return SYNC_PR_TITLE;
|
|
@@ -4947,9 +5077,8 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
4947
5077
|
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
4948
5078
|
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
4949
5079
|
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, {
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
limit: PR_BODY_COMMIT_MAX
|
|
5080
|
+
skip: totalCount > COMMIT_LIST_MAX ? totalCount - COMMIT_LIST_MAX : 0,
|
|
5081
|
+
limit: COMMIT_LIST_MAX
|
|
4953
5082
|
}) : [];
|
|
4954
5083
|
const notes = await readPrBodyNotes(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
4955
5084
|
return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount, notes });
|
|
@@ -4973,11 +5102,8 @@ function printShipSteps(temporaryBranch, base, title) {
|
|
|
4973
5102
|
console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
|
|
4974
5103
|
printPrCreateStep(temporaryBranch, base, title);
|
|
4975
5104
|
}
|
|
4976
|
-
function printFinishSteps() {
|
|
4977
|
-
console.info(colors_default.dim(" pnpm cella sync"));
|
|
4978
|
-
}
|
|
4979
5105
|
function hasStagedSyncChanges(result) {
|
|
4980
|
-
return result.files.some((file) =>
|
|
5106
|
+
return result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
4981
5107
|
}
|
|
4982
5108
|
function extractFirstUrl(output) {
|
|
4983
5109
|
return output.match(/https?:\/\/\S+/)?.[0];
|
|
@@ -4988,7 +5114,7 @@ async function flattenSyncBranch(forkPath, branch, base) {
|
|
|
4988
5114
|
const message = await buildSyncCommitMessage(forkPath, `${mergeCommits[0]}^2`);
|
|
4989
5115
|
console.info(
|
|
4990
5116
|
colors_default.yellow(
|
|
4991
|
-
`'${branch}' contains ${mergeCommits.length} merge commit(s)
|
|
5117
|
+
`'${branch}' contains ${mergeCommits.length} merge commit(s): the PR would list the entire upstream history.`
|
|
4992
5118
|
)
|
|
4993
5119
|
);
|
|
4994
5120
|
console.info(colors_default.dim(`flattening '${branch}' to a single commit (same content)...`));
|
|
@@ -5011,8 +5137,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
5011
5137
|
try {
|
|
5012
5138
|
await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
|
|
5013
5139
|
} catch (error) {
|
|
5014
|
-
|
|
5015
|
-
console.info(colors_default.yellow(`push failed (${detail.split("\n")[0]}). finish manually:`));
|
|
5140
|
+
console.info(colors_default.yellow(`push failed (${errorMessage(error).split("\n")[0]}). finish manually:`));
|
|
5016
5141
|
printShipSteps(branch, base, prTitle);
|
|
5017
5142
|
return;
|
|
5018
5143
|
}
|
|
@@ -5020,9 +5145,9 @@ async function shipSyncBranch(config, branch) {
|
|
|
5020
5145
|
console.info(colors_default.dim("opening a pull request..."));
|
|
5021
5146
|
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
5022
5147
|
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
5023
|
-
const pr =
|
|
5148
|
+
const pr = spawnSync4("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
5024
5149
|
cwd: forkPath,
|
|
5025
|
-
encoding: "
|
|
5150
|
+
encoding: "utf8",
|
|
5026
5151
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5027
5152
|
});
|
|
5028
5153
|
prUrl = extractFirstUrl(`${pr.stdout ?? ""}
|
|
@@ -5034,16 +5159,16 @@ ${pr.stderr ?? ""}`);
|
|
|
5034
5159
|
printPrCreateStep(branch, base, prTitle);
|
|
5035
5160
|
}
|
|
5036
5161
|
} else {
|
|
5037
|
-
console.info(colors_default.yellow("`gh` not found
|
|
5162
|
+
console.info(colors_default.yellow("`gh` not found, open the PR manually:"));
|
|
5038
5163
|
printPrCreateStep(branch, base, prTitle);
|
|
5039
5164
|
}
|
|
5040
5165
|
const position = await returnToBase(forkPath, base);
|
|
5041
5166
|
console.info();
|
|
5042
5167
|
if (prUrl) {
|
|
5043
|
-
console.info(`${
|
|
5168
|
+
console.info(`${checkMark} sync pull request ${prOpened ? "opened" : "ready"}`);
|
|
5044
5169
|
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, ${position}`));
|
|
5045
5170
|
} else {
|
|
5046
|
-
console.info(`${
|
|
5171
|
+
console.info(`${checkMark} sync branch pushed`);
|
|
5047
5172
|
console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
|
|
5048
5173
|
}
|
|
5049
5174
|
await printMigrationNotesLine(config);
|
|
@@ -5051,23 +5176,43 @@ ${pr.stderr ?? ""}`);
|
|
|
5051
5176
|
function finalizeWorkspace(forkPath) {
|
|
5052
5177
|
for (const args of [["install"], ["check"]]) {
|
|
5053
5178
|
console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
|
|
5054
|
-
const result =
|
|
5179
|
+
const result = spawnSync4("pnpm", args, { cwd: forkPath, stdio: "inherit" });
|
|
5055
5180
|
if (result.status !== 0) return false;
|
|
5056
5181
|
}
|
|
5057
5182
|
return true;
|
|
5058
5183
|
}
|
|
5184
|
+
async function discardTemporarySyncBranch(forkPath, branch) {
|
|
5185
|
+
if (branch.startBranch === "HEAD") await switchDetached(forkPath, branch.startCommit);
|
|
5186
|
+
else await switchBranch(forkPath, branch.startBranch);
|
|
5187
|
+
await deleteBranch(forkPath, branch.temporaryBranch);
|
|
5188
|
+
}
|
|
5189
|
+
async function discardUnusedSyncBranch(forkPath, branch) {
|
|
5190
|
+
const unused = await getCurrentBranch(forkPath) === branch.temporaryBranch && !await mergeInProgress(forkPath) && await isClean(forkPath);
|
|
5191
|
+
if (!unused) return false;
|
|
5192
|
+
await discardTemporarySyncBranch(forkPath, branch);
|
|
5193
|
+
const start = branch.startBranch === "HEAD" ? `at ${branch.startCommit.slice(0, 7)}` : `on '${branch.startBranch}'`;
|
|
5194
|
+
console.info(colors_default.dim(`removed the unused '${branch.temporaryBranch}', back ${start}.`));
|
|
5195
|
+
return true;
|
|
5196
|
+
}
|
|
5059
5197
|
async function runSyncCycle(config) {
|
|
5060
5198
|
const { forkPath } = config;
|
|
5061
5199
|
const branch = await setupTemporarySyncBranch(config);
|
|
5062
|
-
|
|
5200
|
+
let result;
|
|
5201
|
+
try {
|
|
5202
|
+
result = await runSync(config, { stagedBranch: branch.temporaryBranch });
|
|
5203
|
+
} catch (error) {
|
|
5204
|
+
const discarded = await discardUnusedSyncBranch(forkPath, branch).catch(() => false);
|
|
5205
|
+
if (discarded && error instanceof BehindSyncPointError && !config.ref) {
|
|
5206
|
+
return { status: "noop", behind: error.message };
|
|
5207
|
+
}
|
|
5208
|
+
throw error;
|
|
5209
|
+
}
|
|
5063
5210
|
if (config.settings.syncWithPackages !== false) {
|
|
5064
5211
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
5065
5212
|
}
|
|
5066
5213
|
if (result.conflicts.length > 0) return { status: "conflicts", branch };
|
|
5067
5214
|
if (!await mergeInProgress(forkPath)) {
|
|
5068
|
-
|
|
5069
|
-
else await switchBranch(forkPath, branch.startBranch);
|
|
5070
|
-
await deleteBranch(forkPath, branch.temporaryBranch);
|
|
5215
|
+
await discardTemporarySyncBranch(forkPath, branch);
|
|
5071
5216
|
return { status: "noop" };
|
|
5072
5217
|
}
|
|
5073
5218
|
return { status: "staged", branch };
|
|
@@ -5112,15 +5257,15 @@ async function mergeOpenSyncPrs(config, open) {
|
|
|
5112
5257
|
const merged = mergePrSquash(forkPath, newest.number, { deleteBranch: true });
|
|
5113
5258
|
if (!merged.ok) {
|
|
5114
5259
|
console.info();
|
|
5115
|
-
console.info(colors_default.yellow(`could not merge #${newest.number}
|
|
5260
|
+
console.info(colors_default.yellow(`could not merge #${newest.number}: resolve it first, then re-run \`pnpm cella sync\`:`));
|
|
5116
5261
|
if (merged.output) console.info(colors_default.dim(indentLines(merged.output)));
|
|
5117
5262
|
console.info(colors_default.dim(` ${newest.url}`));
|
|
5118
5263
|
return "cancel";
|
|
5119
5264
|
}
|
|
5120
|
-
console.info(`${
|
|
5265
|
+
console.info(`${checkMark} merged #${newest.number}`);
|
|
5121
5266
|
await deleteBranch(forkPath, newest.headRefName);
|
|
5122
5267
|
for (const pr of superseded) {
|
|
5123
|
-
console.info(colors_default.dim(`closing #${pr.number}
|
|
5268
|
+
console.info(colors_default.dim(`closing #${pr.number}: its changes are included in #${newest.number}...`));
|
|
5124
5269
|
closePr(forkPath, pr.number);
|
|
5125
5270
|
await deleteBranch(forkPath, pr.headRefName);
|
|
5126
5271
|
}
|
|
@@ -5185,15 +5330,20 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5185
5330
|
const outcome = await runSyncCycle(config);
|
|
5186
5331
|
console.info();
|
|
5187
5332
|
if (outcome.status === "noop") {
|
|
5188
|
-
|
|
5333
|
+
if (outcome.behind) {
|
|
5334
|
+
console.info(colors_default.green("nothing to sync: the last sync already went past this upstream point."));
|
|
5335
|
+
for (const line of outcome.behind.split("\n")) console.info(colors_default.dim(` ${line}`));
|
|
5336
|
+
} else {
|
|
5337
|
+
console.info(colors_default.green("already up to date with upstream, nothing to sync."));
|
|
5338
|
+
}
|
|
5189
5339
|
return;
|
|
5190
5340
|
}
|
|
5191
5341
|
const { temporaryBranch } = outcome.branch;
|
|
5192
5342
|
if (outcome.status === "conflicts") {
|
|
5193
5343
|
console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve and stage them, then:`)}`);
|
|
5194
|
-
|
|
5344
|
+
console.info(colors_default.dim(" pnpm cella sync"));
|
|
5195
5345
|
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
|
|
5346
|
+
console.info(colors_default.dim(" let the rerun commit: a manual `git commit` records a merge commit that bloats the PR."));
|
|
5197
5347
|
return;
|
|
5198
5348
|
}
|
|
5199
5349
|
await commitSyncMerge(config, temporaryBranch);
|
|
@@ -5201,12 +5351,11 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5201
5351
|
|
|
5202
5352
|
// src/services/forks.ts
|
|
5203
5353
|
async function gatherForkStatus(forkPath) {
|
|
5204
|
-
const [branch,
|
|
5354
|
+
const [branch, dirty, syncRef] = await Promise.all([
|
|
5205
5355
|
getCurrentBranch(forkPath).catch(() => "unknown"),
|
|
5206
|
-
|
|
5356
|
+
getWorkingTreeChangeCount(forkPath).catch(() => 0),
|
|
5207
5357
|
getStoredSyncRef(forkPath)
|
|
5208
5358
|
]);
|
|
5209
|
-
const dirty = porcelain ? porcelain.split("\n").filter(Boolean).length : 0;
|
|
5210
5359
|
let lastSync = null;
|
|
5211
5360
|
if (syncRef) {
|
|
5212
5361
|
const commitInfo = await getCommitInfo(forkPath, syncRef).catch(() => null);
|
|
@@ -5269,7 +5418,8 @@ async function syncFork(config, fork, forkPath) {
|
|
|
5269
5418
|
list: false,
|
|
5270
5419
|
json: false,
|
|
5271
5420
|
verbose: config.verbose,
|
|
5272
|
-
hard: config.hard
|
|
5421
|
+
hard: config.hard,
|
|
5422
|
+
keepConfig: config.keepConfig
|
|
5273
5423
|
};
|
|
5274
5424
|
await runSyncCommand(forkRuntimeConfig);
|
|
5275
5425
|
}
|
|
@@ -5282,8 +5432,7 @@ async function runForks(config) {
|
|
|
5282
5432
|
if (config.fork) {
|
|
5283
5433
|
const match = forks.find((f) => f.name === config.fork);
|
|
5284
5434
|
if (!match) {
|
|
5285
|
-
|
|
5286
|
-
return;
|
|
5435
|
+
throw new Error(`fork '${config.fork}' not found in config`);
|
|
5287
5436
|
}
|
|
5288
5437
|
const resolvedPath = resolve7(await resolveForkBasePath(config.forkPath), match.localPath);
|
|
5289
5438
|
await syncFork(config, match, resolvedPath);
|
|
@@ -5292,7 +5441,7 @@ async function runForks(config) {
|
|
|
5292
5441
|
const forkBasePath = await resolveForkBasePath(config.forkPath);
|
|
5293
5442
|
while (true) {
|
|
5294
5443
|
const choices = await buildForkChoices(forks, forkBasePath);
|
|
5295
|
-
const forkChoices = [...choices, new Separator2(
|
|
5444
|
+
const forkChoices = [...choices, new Separator2(MENU_DIVIDER), { value: "_exit", name: colors_default.dim("exit") }];
|
|
5296
5445
|
const selectedPath = await select4({
|
|
5297
5446
|
message: "select fork to sync:",
|
|
5298
5447
|
choices: forkChoices,
|
|
@@ -5314,15 +5463,15 @@ async function runForks(config) {
|
|
|
5314
5463
|
|
|
5315
5464
|
// src/services/stats.ts
|
|
5316
5465
|
import { readFile as readFile3 } from "fs/promises";
|
|
5317
|
-
import { join as
|
|
5466
|
+
import { join as join18 } from "path";
|
|
5318
5467
|
|
|
5319
5468
|
// src/utils/coverage-utils.ts
|
|
5320
|
-
import { spawnSync as
|
|
5321
|
-
import
|
|
5322
|
-
import
|
|
5469
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
5470
|
+
import { existsSync as existsSync12, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
5471
|
+
import { join as join17, relative as relative3, sep as sep2 } from "path";
|
|
5323
5472
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5324
5473
|
function summaryPath(forkPath) {
|
|
5325
|
-
return
|
|
5474
|
+
return join17(forkPath, ".coverage", "coverage-summary.json");
|
|
5326
5475
|
}
|
|
5327
5476
|
function colorPct(pct) {
|
|
5328
5477
|
if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
|
|
@@ -5341,7 +5490,7 @@ function formatAge(ms) {
|
|
|
5341
5490
|
}
|
|
5342
5491
|
function refreshCoverage(forkPath) {
|
|
5343
5492
|
console.info(colors_default.dim("running `pnpm test` (this spins up Docker and runs the full suite)..."));
|
|
5344
|
-
const result =
|
|
5493
|
+
const result = spawnSync5("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
|
|
5345
5494
|
return result.status === 0;
|
|
5346
5495
|
}
|
|
5347
5496
|
function printCoverageSummary(forkPath, options = {}) {
|
|
@@ -5349,24 +5498,24 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5349
5498
|
if (options.refresh) {
|
|
5350
5499
|
const ok = refreshCoverage(forkPath);
|
|
5351
5500
|
if (!ok) {
|
|
5352
|
-
console.info(colors_default.red("coverage run failed
|
|
5501
|
+
console.info(colors_default.red("coverage run failed: showing last cached results (if any)"));
|
|
5353
5502
|
}
|
|
5354
5503
|
console.info();
|
|
5355
5504
|
}
|
|
5356
|
-
if (!
|
|
5505
|
+
if (!existsSync12(file)) {
|
|
5357
5506
|
console.info(colors_default.bold("coverage"));
|
|
5358
|
-
console.info(colors_default.yellow(" no coverage data found
|
|
5507
|
+
console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to generate it"));
|
|
5359
5508
|
console.info();
|
|
5360
5509
|
return;
|
|
5361
5510
|
}
|
|
5362
5511
|
let summary;
|
|
5363
5512
|
let mtimeMs;
|
|
5364
5513
|
try {
|
|
5365
|
-
summary = JSON.parse(
|
|
5366
|
-
mtimeMs =
|
|
5514
|
+
summary = JSON.parse(readFileSync6(file, "utf8"));
|
|
5515
|
+
mtimeMs = statSync2(file).mtimeMs;
|
|
5367
5516
|
} catch {
|
|
5368
5517
|
console.info(colors_default.bold("coverage"));
|
|
5369
|
-
console.info(colors_default.yellow(" could not read coverage-summary.json
|
|
5518
|
+
console.info(colors_default.yellow(" could not read coverage-summary.json: run `pnpm test` to regenerate"));
|
|
5370
5519
|
console.info();
|
|
5371
5520
|
return;
|
|
5372
5521
|
}
|
|
@@ -5375,9 +5524,9 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5375
5524
|
const perPackage = /* @__PURE__ */ new Map();
|
|
5376
5525
|
for (const [key, entry] of Object.entries(summary)) {
|
|
5377
5526
|
if (key === "total") continue;
|
|
5378
|
-
const rel =
|
|
5527
|
+
const rel = relative3(forkPath, key);
|
|
5379
5528
|
if (rel.startsWith("..")) continue;
|
|
5380
|
-
const pkg = rel.split(
|
|
5529
|
+
const pkg = rel.split(sep2)[0];
|
|
5381
5530
|
const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
|
|
5382
5531
|
acc.covered += entry.lines.covered;
|
|
5383
5532
|
acc.total += entry.lines.total;
|
|
@@ -5386,10 +5535,10 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5386
5535
|
const ageLabel = formatAge(ageMs);
|
|
5387
5536
|
console.info(colors_default.bold("coverage"), colors_default.dim(`(updated ${ageLabel})`));
|
|
5388
5537
|
if (stale) {
|
|
5389
|
-
console.info(colors_default.yellow(" \u26A0 data is older than 7 days
|
|
5538
|
+
console.info(colors_default.yellow(" \u26A0 data is older than 7 days: run `pnpm test` to refresh"));
|
|
5390
5539
|
}
|
|
5391
5540
|
if (!Object.keys(summary).some((key) => key !== "total")) {
|
|
5392
|
-
console.info(colors_default.yellow(" no coverage data found
|
|
5541
|
+
console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to regenerate"));
|
|
5393
5542
|
console.info();
|
|
5394
5543
|
return;
|
|
5395
5544
|
}
|
|
@@ -5408,7 +5557,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5408
5557
|
// src/services/stats.ts
|
|
5409
5558
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
5410
5559
|
async function getWorkspacePackages(forkPath) {
|
|
5411
|
-
const content = await readFile3(
|
|
5560
|
+
const content = await readFile3(join18(forkPath, "pnpm-workspace.yaml"), "utf8");
|
|
5412
5561
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
5413
5562
|
if (pattern.endsWith("/*")) {
|
|
5414
5563
|
const base = pattern.slice(0, -2);
|
|
@@ -5472,12 +5621,12 @@ async function collectStats(forkPath) {
|
|
|
5472
5621
|
const categories = emptyCat();
|
|
5473
5622
|
const pkgStats = /* @__PURE__ */ new Map();
|
|
5474
5623
|
let totalLoc = 0;
|
|
5475
|
-
for (const { path
|
|
5476
|
-
const category = classifyFile(
|
|
5624
|
+
for (const { path, loc } of entries) {
|
|
5625
|
+
const category = classifyFile(path);
|
|
5477
5626
|
categories[category].files++;
|
|
5478
5627
|
categories[category].loc += loc;
|
|
5479
5628
|
totalLoc += loc;
|
|
5480
|
-
const pkg = matchPackage(
|
|
5629
|
+
const pkg = matchPackage(path, packages);
|
|
5481
5630
|
let entry = pkgStats.get(pkg);
|
|
5482
5631
|
if (!entry) {
|
|
5483
5632
|
entry = { total: 0, loc: 0, categories: emptyCat() };
|
|
@@ -5554,17 +5703,237 @@ function printStats(stats, verbose) {
|
|
|
5554
5703
|
}
|
|
5555
5704
|
console.info();
|
|
5556
5705
|
}
|
|
5557
|
-
async function runStats(
|
|
5706
|
+
async function runStats(config) {
|
|
5707
|
+
const { forkPath } = config;
|
|
5558
5708
|
createSpinner("counting files...");
|
|
5559
5709
|
const stats = await collectStats(forkPath);
|
|
5560
|
-
spinnerSuccess("
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5710
|
+
spinnerSuccess("counted raw lines of code");
|
|
5711
|
+
printStats(stats, config.verbose);
|
|
5712
|
+
console.info();
|
|
5713
|
+
printCoverageSummary(forkPath, { refresh: config.coverage });
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5716
|
+
// src/services/stats-since.ts
|
|
5717
|
+
var kindLabels = {
|
|
5718
|
+
source: "source",
|
|
5719
|
+
test: "tests",
|
|
5720
|
+
stories: "stories",
|
|
5721
|
+
generated: "generated",
|
|
5722
|
+
json: "json",
|
|
5723
|
+
docs: "docs",
|
|
5724
|
+
other: "other"
|
|
5725
|
+
};
|
|
5726
|
+
var docExtensions = /* @__PURE__ */ new Set(["md", "mdx"]);
|
|
5727
|
+
var lockFiles = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"]);
|
|
5728
|
+
var codeExtensions = [...sourceExtensions].filter((ext) => !ext.startsWith("json"));
|
|
5729
|
+
function classifyChange(filePath) {
|
|
5730
|
+
const name = filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
5731
|
+
if (lockFiles.has(name)) return "generated";
|
|
5732
|
+
const category = classifyFile(filePath);
|
|
5733
|
+
if (category !== "other") return category;
|
|
5734
|
+
const ext = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : "";
|
|
5735
|
+
if (docExtensions.has(ext)) return "docs";
|
|
5736
|
+
return sourceExtensions.has(ext) ? "source" : "other";
|
|
5737
|
+
}
|
|
5738
|
+
function classifyLine(text) {
|
|
5739
|
+
const line = text.trim();
|
|
5740
|
+
if (!line) return "blank";
|
|
5741
|
+
if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*") || line.startsWith("{/*")) {
|
|
5742
|
+
return "comments";
|
|
5743
|
+
}
|
|
5744
|
+
return "code";
|
|
5745
|
+
}
|
|
5746
|
+
function parseNumstat(output) {
|
|
5747
|
+
const files = [];
|
|
5748
|
+
for (const record of output.split("\0")) {
|
|
5749
|
+
if (!record) continue;
|
|
5750
|
+
const [added, removed, ...rest] = record.split(" ");
|
|
5751
|
+
const path = rest.join(" ");
|
|
5752
|
+
if (!path) continue;
|
|
5753
|
+
files.push({
|
|
5754
|
+
path,
|
|
5755
|
+
kind: classifyChange(path),
|
|
5756
|
+
added: Number.parseInt(added, 10) || 0,
|
|
5757
|
+
removed: Number.parseInt(removed, 10) || 0
|
|
5758
|
+
});
|
|
5759
|
+
}
|
|
5760
|
+
return files;
|
|
5761
|
+
}
|
|
5762
|
+
function countLineContent(patch, isCounted) {
|
|
5763
|
+
const content = {
|
|
5764
|
+
code: { added: 0, removed: 0 },
|
|
5765
|
+
comments: { added: 0, removed: 0 },
|
|
5766
|
+
blank: { added: 0, removed: 0 }
|
|
5767
|
+
};
|
|
5768
|
+
let counted = false;
|
|
5769
|
+
let remaining = 0;
|
|
5770
|
+
for (const line of patch.split("\n")) {
|
|
5771
|
+
if (remaining > 0) {
|
|
5772
|
+
if (line.startsWith("\\")) continue;
|
|
5773
|
+
remaining--;
|
|
5774
|
+
if (!counted) continue;
|
|
5775
|
+
const change = line.startsWith("+") ? "added" : "removed";
|
|
5776
|
+
content[classifyLine(line.slice(1))][change]++;
|
|
5777
|
+
continue;
|
|
5778
|
+
}
|
|
5779
|
+
if (line.startsWith("diff --git ")) {
|
|
5780
|
+
counted = false;
|
|
5781
|
+
continue;
|
|
5782
|
+
}
|
|
5783
|
+
if (line.startsWith("--- a/") || line.startsWith("+++ b/")) {
|
|
5784
|
+
counted = isCounted(line.slice(6));
|
|
5785
|
+
continue;
|
|
5786
|
+
}
|
|
5787
|
+
const hunk = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line);
|
|
5788
|
+
if (hunk) remaining = Number(hunk[1] ?? 1) + Number(hunk[2] ?? 1);
|
|
5789
|
+
}
|
|
5790
|
+
return content;
|
|
5791
|
+
}
|
|
5792
|
+
async function collectBranchStats(forkPath, since) {
|
|
5793
|
+
const sinceSha = await git(["rev-parse", "--verify", "--quiet", `${since}^{commit}`], forkPath, {
|
|
5794
|
+
ignoreErrors: true
|
|
5795
|
+
});
|
|
5796
|
+
if (!sinceSha) throw new Error(`unknown ref '${since}'. pass a branch, tag or commit, such as origin/main`);
|
|
5797
|
+
const mergeBase = await git(["merge-base", sinceSha, "HEAD"], forkPath, { ignoreErrors: true });
|
|
5798
|
+
if (!mergeBase) throw new Error(`the current branch shares no history with '${since}'`);
|
|
5799
|
+
const range = [mergeBase, "HEAD"];
|
|
5800
|
+
const files = parseNumstat(await git(["diff", "--numstat", "-z", "--no-renames", ...range], forkPath));
|
|
5801
|
+
const kindOf = new Map(files.map((file) => [file.path, file.kind]));
|
|
5802
|
+
const emptyKind = () => ({ files: 0, added: 0, removed: 0 });
|
|
5803
|
+
const kinds = {
|
|
5804
|
+
source: emptyKind(),
|
|
5805
|
+
test: emptyKind(),
|
|
5806
|
+
stories: emptyKind(),
|
|
5807
|
+
generated: emptyKind(),
|
|
5808
|
+
json: emptyKind(),
|
|
5809
|
+
docs: emptyKind(),
|
|
5810
|
+
other: emptyKind()
|
|
5811
|
+
};
|
|
5812
|
+
const total = emptyKind();
|
|
5813
|
+
const packages = await getWorkspacePackages(forkPath).catch(() => []);
|
|
5814
|
+
const byPackage = /* @__PURE__ */ new Map();
|
|
5815
|
+
const add = (target, file) => {
|
|
5816
|
+
target.files++;
|
|
5817
|
+
target.added += file.added;
|
|
5818
|
+
target.removed += file.removed;
|
|
5819
|
+
};
|
|
5820
|
+
for (const file of files) {
|
|
5821
|
+
add(total, file);
|
|
5822
|
+
add(kinds[file.kind], file);
|
|
5823
|
+
if (file.kind !== "source") continue;
|
|
5824
|
+
const pkg = matchPackage(file.path, packages);
|
|
5825
|
+
const entry = byPackage.get(pkg) ?? emptyKind();
|
|
5826
|
+
add(entry, file);
|
|
5827
|
+
byPackage.set(pkg, entry);
|
|
5828
|
+
}
|
|
5829
|
+
const patch = await git(
|
|
5830
|
+
["diff", "-U0", "--no-renames", "--no-color", ...range, "--", ...codeExtensions.map((ext) => `*.${ext}`)],
|
|
5831
|
+
forkPath,
|
|
5832
|
+
{ ignoreErrors: true }
|
|
5833
|
+
);
|
|
5834
|
+
const sourceContent = kinds.source.files > 0 && !patch ? null : countLineContent(patch, (path) => kindOf.get(path) === "source");
|
|
5835
|
+
const sourceByPackage = Object.fromEntries(
|
|
5836
|
+
[...byPackage.entries()].sort((a, b) => net(b[1]) - net(a[1]) || a[0].localeCompare(b[0]))
|
|
5837
|
+
);
|
|
5838
|
+
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], forkPath);
|
|
5839
|
+
const status = await git(["status", "--porcelain"], forkPath, { ignoreErrors: true });
|
|
5840
|
+
return {
|
|
5841
|
+
since,
|
|
5842
|
+
mergeBase: mergeBase.slice(0, 7),
|
|
5843
|
+
head: branch === "HEAD" ? await git(["rev-parse", "--short", "HEAD"], forkPath) : branch,
|
|
5844
|
+
total,
|
|
5845
|
+
kinds,
|
|
5846
|
+
sourceContent,
|
|
5847
|
+
sourceByPackage,
|
|
5848
|
+
files: files.sort((a, b) => net(b) - net(a) || a.path.localeCompare(b.path)),
|
|
5849
|
+
uncommitted: status.split("\n").filter(Boolean).length
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
function net(change) {
|
|
5853
|
+
return change.added - change.removed;
|
|
5854
|
+
}
|
|
5855
|
+
function signed(n) {
|
|
5856
|
+
return n > 0 ? `+${n}` : String(n);
|
|
5857
|
+
}
|
|
5858
|
+
function changedKinds(stats) {
|
|
5859
|
+
return Object.entries(stats.kinds).filter(([, change]) => change.files > 0).sort((a, b) => net(b[1]) - net(a[1]));
|
|
5860
|
+
}
|
|
5861
|
+
function formatNetList(parts) {
|
|
5862
|
+
return parts.filter(([, change]) => change.added > 0 || change.removed > 0).map(([label, change]) => `${label} ${signed(net(change))}`).join(", ");
|
|
5863
|
+
}
|
|
5864
|
+
function formatMarkdown(stats) {
|
|
5865
|
+
const row = (label, change) => `| ${label} | ${change.files} | ${signed(change.added)} | ${signed(-change.removed)} | ${signed(net(change))} |`;
|
|
5866
|
+
const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
5867
|
+
const lines = [
|
|
5868
|
+
"| Kind | Files | Added | Removed | Net |",
|
|
5869
|
+
"| --- | ---: | ---: | ---: | ---: |",
|
|
5870
|
+
...changedKinds(stats).map(
|
|
5871
|
+
([kind, change]) => row(kind === "json" ? "JSON" : capitalize(kindLabels[kind]), change)
|
|
5872
|
+
),
|
|
5873
|
+
`| **Total** | **${stats.total.files}** | **${signed(stats.total.added)}** | **${signed(-stats.total.removed)}** | **${signed(net(stats.total))}** |`
|
|
5874
|
+
];
|
|
5875
|
+
const notes = [];
|
|
5876
|
+
if (stats.sourceContent) {
|
|
5877
|
+
const content = formatNetList(Object.entries(stats.sourceContent));
|
|
5878
|
+
if (content) notes.push(`Source lines by content: ${content}.`);
|
|
5879
|
+
}
|
|
5880
|
+
const byPackage = formatNetList(Object.entries(stats.sourceByPackage));
|
|
5881
|
+
if (byPackage) notes.push(`Source lines by package: ${byPackage}.`);
|
|
5882
|
+
return notes.length > 0 ? [...lines, "", ...notes].join("\n") : lines.join("\n");
|
|
5883
|
+
}
|
|
5884
|
+
function printBranchStats(stats, verbose) {
|
|
5885
|
+
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}`;
|
|
5886
|
+
console.info();
|
|
5887
|
+
console.info(colors_default.dim(` ${"files".padStart(5)} ${"added".padStart(8)} ${"removed".padStart(8)} ${"net".padStart(8)}`));
|
|
5888
|
+
for (const [kind, change] of changedKinds(stats)) {
|
|
5889
|
+
console.info(columns(String(change.files), change, kindLabels[kind]));
|
|
5890
|
+
}
|
|
5891
|
+
console.info(columns(String(stats.total.files), stats.total, colors_default.bold("total")));
|
|
5892
|
+
if (stats.sourceContent && stats.kinds.source.files > 0) {
|
|
5893
|
+
console.info();
|
|
5894
|
+
console.info(colors_default.dim(" source lines by content"));
|
|
5895
|
+
for (const [content, change] of Object.entries(stats.sourceContent)) {
|
|
5896
|
+
console.info(columns("", change, content));
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
const packages = Object.entries(stats.sourceByPackage);
|
|
5900
|
+
if (packages.length > 0) {
|
|
5565
5901
|
console.info();
|
|
5566
|
-
|
|
5902
|
+
console.info(colors_default.dim(" source lines by package"));
|
|
5903
|
+
for (const [name, change] of packages) {
|
|
5904
|
+
console.info(columns(String(change.files), change, name));
|
|
5905
|
+
}
|
|
5567
5906
|
}
|
|
5907
|
+
if (verbose) {
|
|
5908
|
+
console.info();
|
|
5909
|
+
console.info(DIVIDER);
|
|
5910
|
+
console.info();
|
|
5911
|
+
for (const file of stats.files) {
|
|
5912
|
+
console.info(columns("", file, `${file.path} ${colors_default.dim(kindLabels[file.kind])}`));
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
if (stats.uncommitted > 0) {
|
|
5916
|
+
console.info();
|
|
5917
|
+
console.info(
|
|
5918
|
+
colors_default.dim(` ${stats.uncommitted} uncommitted file${stats.uncommitted === 1 ? " is" : "s are"} not counted`)
|
|
5919
|
+
);
|
|
5920
|
+
}
|
|
5921
|
+
console.info();
|
|
5922
|
+
}
|
|
5923
|
+
async function runBranchStats(forkPath, options) {
|
|
5924
|
+
createSpinner("counting changed lines...");
|
|
5925
|
+
const stats = await collectBranchStats(forkPath, options.since);
|
|
5926
|
+
spinnerSuccess(`Counted what ${stats.head} changed since ${stats.since} (${stats.mergeBase})`);
|
|
5927
|
+
if (stats.total.files === 0) {
|
|
5928
|
+
console.info(colors_default.dim(` nothing committed since ${stats.since}`));
|
|
5929
|
+
return;
|
|
5930
|
+
}
|
|
5931
|
+
if (options.markdown) {
|
|
5932
|
+
writeStdout(formatMarkdown(stats));
|
|
5933
|
+
if (stats.uncommitted > 0) console.info(colors_default.dim(`${stats.uncommitted} uncommitted file(s) are not counted`));
|
|
5934
|
+
return;
|
|
5935
|
+
}
|
|
5936
|
+
printBranchStats(stats, options.verbose ?? false);
|
|
5568
5937
|
}
|
|
5569
5938
|
|
|
5570
5939
|
// src/cella-cli.ts
|
|
@@ -5583,8 +5952,8 @@ function getForkPath() {
|
|
|
5583
5952
|
}
|
|
5584
5953
|
return cwd;
|
|
5585
5954
|
}
|
|
5586
|
-
|
|
5587
|
-
if (!existsSync13(
|
|
5955
|
+
function preflight(forkPath) {
|
|
5956
|
+
if (!existsSync13(join19(forkPath, ".git"))) {
|
|
5588
5957
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5589
5958
|
}
|
|
5590
5959
|
}
|
|
@@ -5602,7 +5971,7 @@ async function main() {
|
|
|
5602
5971
|
const userConfig = await loadConfig(forkPath);
|
|
5603
5972
|
const config = await parseCli(userConfig, forkPath);
|
|
5604
5973
|
if (!["audit", "forks", "contributions", "stats"].includes(config.service)) {
|
|
5605
|
-
|
|
5974
|
+
preflight(forkPath);
|
|
5606
5975
|
}
|
|
5607
5976
|
switch (config.service) {
|
|
5608
5977
|
case "analyze": {
|
|
@@ -5613,26 +5982,36 @@ async function main() {
|
|
|
5613
5982
|
await runSyncCommand(config);
|
|
5614
5983
|
break;
|
|
5615
5984
|
}
|
|
5616
|
-
case "migrate":
|
|
5985
|
+
case "migrate": {
|
|
5617
5986
|
await runMigrate(config);
|
|
5618
5987
|
break;
|
|
5619
|
-
|
|
5620
|
-
|
|
5988
|
+
}
|
|
5989
|
+
case "audit": {
|
|
5990
|
+
await runAudit(config);
|
|
5621
5991
|
break;
|
|
5622
|
-
|
|
5992
|
+
}
|
|
5993
|
+
case "forks": {
|
|
5623
5994
|
await runForks(config);
|
|
5624
5995
|
break;
|
|
5625
|
-
|
|
5996
|
+
}
|
|
5997
|
+
case "contributions": {
|
|
5626
5998
|
await runContributions(config);
|
|
5627
5999
|
break;
|
|
5628
|
-
|
|
5629
|
-
|
|
6000
|
+
}
|
|
6001
|
+
case "stats": {
|
|
6002
|
+
if (config.md && !config.since) throw new Error("--md goes with --since <ref>");
|
|
6003
|
+
if (config.since) {
|
|
6004
|
+
await runBranchStats(config.forkPath, { since: config.since, markdown: config.md, verbose: config.verbose });
|
|
6005
|
+
} else {
|
|
6006
|
+
await runStats(config);
|
|
6007
|
+
}
|
|
5630
6008
|
break;
|
|
6009
|
+
}
|
|
5631
6010
|
}
|
|
5632
6011
|
console.info();
|
|
5633
6012
|
} catch (error) {
|
|
5634
6013
|
console.error();
|
|
5635
|
-
console.error(`${colors_default.red("\u2717")} ${error
|
|
6014
|
+
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
5636
6015
|
process9.exit(1);
|
|
5637
6016
|
}
|
|
5638
6017
|
}
|