@cellajs/cli 0.1.1 → 0.2.1
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 +34 -12
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +191 -65
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -75,6 +75,7 @@ var cellaConfigSchema = z.object({
|
|
|
75
75
|
"engines",
|
|
76
76
|
"packageManager",
|
|
77
77
|
"overrides",
|
|
78
|
+
"exports",
|
|
78
79
|
"pnpm"
|
|
79
80
|
])
|
|
80
81
|
).optional(),
|
|
@@ -215,7 +216,7 @@ import process4 from "process";
|
|
|
215
216
|
// package.json
|
|
216
217
|
var package_default = {
|
|
217
218
|
name: "@cellajs/cli",
|
|
218
|
-
version: "0.
|
|
219
|
+
version: "0.2.1",
|
|
219
220
|
private: false,
|
|
220
221
|
license: "MIT",
|
|
221
222
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -226,7 +227,7 @@ var package_default = {
|
|
|
226
227
|
homepage: "https://cellajs.com",
|
|
227
228
|
author: "CellaJS <info@cellajs.com>",
|
|
228
229
|
engines: {
|
|
229
|
-
node: "
|
|
230
|
+
node: "26.x"
|
|
230
231
|
},
|
|
231
232
|
type: "module",
|
|
232
233
|
main: "./dist/index.js",
|
|
@@ -266,24 +267,24 @@ var package_default = {
|
|
|
266
267
|
},
|
|
267
268
|
dependencies: {
|
|
268
269
|
"@inquirer/core": "^11.2.1",
|
|
269
|
-
"@inquirer/prompts": "^8.
|
|
270
|
-
"@pierre/diffs": "^1.
|
|
270
|
+
"@inquirer/prompts": "^8.7.2",
|
|
271
|
+
"@pierre/diffs": "^1.4.3",
|
|
271
272
|
commander: "^15.0.0",
|
|
272
273
|
"ts-morph": "^28.0.0",
|
|
273
|
-
zod: "^4.
|
|
274
|
+
zod: "^4.6.5"
|
|
274
275
|
},
|
|
275
276
|
devDependencies: {
|
|
276
|
-
"@biomejs/biome": "^2.5.
|
|
277
|
-
"@commitlint/cli": "^21.2.
|
|
278
|
-
"@commitlint/config-conventional": "^21.2.
|
|
279
|
-
"@evilmartians/lefthook": "^2.1.
|
|
280
|
-
"@types/node": "
|
|
277
|
+
"@biomejs/biome": "^2.5.14",
|
|
278
|
+
"@commitlint/cli": "^21.2.3",
|
|
279
|
+
"@commitlint/config-conventional": "^21.2.3",
|
|
280
|
+
"@evilmartians/lefthook": "^2.1.14",
|
|
281
|
+
"@types/node": "26.5.0",
|
|
281
282
|
"@typescript/native-preview": "7.0.0-dev.20260702.3",
|
|
282
283
|
rimraf: "^6.1.3",
|
|
283
284
|
tsup: "^8.5.1",
|
|
284
|
-
tsx: "^4.
|
|
285
|
+
tsx: "^4.23.15",
|
|
285
286
|
typescript: "^6.0.3",
|
|
286
|
-
vitest: "^4.1.
|
|
287
|
+
vitest: "^4.1.11"
|
|
287
288
|
},
|
|
288
289
|
publishConfig: {
|
|
289
290
|
access: "public"
|
|
@@ -567,37 +568,74 @@ function printSummary(summary, title = "summary") {
|
|
|
567
568
|
console.info();
|
|
568
569
|
printLine("behind", summary.behind);
|
|
569
570
|
}
|
|
570
|
-
function
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
const title = options?.title ?? `${config.icon} ${config.label}`;
|
|
575
|
-
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${filtered.length} files`)}`);
|
|
576
|
-
const useUpstream = options?.dateSource === "upstream";
|
|
571
|
+
function printFileSection(files, title, linkOptions, options) {
|
|
572
|
+
if (files.length === 0) return;
|
|
573
|
+
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${files.length} files`)}`);
|
|
574
|
+
const useUpstream = options.dateSource === "upstream";
|
|
577
575
|
const maxLines = 100;
|
|
578
|
-
const shown =
|
|
576
|
+
const shown = files.length > maxLines ? files.slice(0, maxLines) : files;
|
|
579
577
|
for (const file of shown) {
|
|
578
|
+
const icon = typeof options.icon === "string" ? options.icon : options.icon(file);
|
|
580
579
|
const commit = useUpstream ? file.upstreamCommit : file.changedCommit;
|
|
581
580
|
const date = useUpstream ? file.upstreamChangedAt : file.changedAt;
|
|
582
581
|
const dateInfo = formatFileDateInfo(file.path, commit, date, linkOptions);
|
|
583
|
-
console.info(` ${
|
|
582
|
+
console.info(` ${icon} ${file.path}${dateInfo}${options.suffix?.(file) ?? ""}`);
|
|
584
583
|
}
|
|
585
|
-
if (
|
|
586
|
-
console.info(colors_default.dim(` ... + ${
|
|
584
|
+
if (files.length > maxLines) {
|
|
585
|
+
console.info(colors_default.dim(` ... + ${files.length - maxLines} more`));
|
|
587
586
|
}
|
|
588
|
-
if (options
|
|
587
|
+
if (options.hint) {
|
|
589
588
|
console.info();
|
|
590
|
-
|
|
589
|
+
for (const line of Array.isArray(options.hint) ? options.hint : [options.hint]) {
|
|
590
|
+
console.info(colors_default.dim(` ${line}`));
|
|
591
|
+
}
|
|
591
592
|
}
|
|
592
593
|
}
|
|
594
|
+
function printFileGroup(files, status, linkOptions, options) {
|
|
595
|
+
const filtered = files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
596
|
+
const config = statusConfig[status];
|
|
597
|
+
const title = options?.title ?? `${config.icon} ${config.label}`;
|
|
598
|
+
printFileSection(filtered, title, linkOptions, { ...options, icon: config.icon });
|
|
599
|
+
}
|
|
600
|
+
function findProtectedBehind(files) {
|
|
601
|
+
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path));
|
|
602
|
+
}
|
|
603
|
+
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so diff each against upstream (analyze --open-diff <path>) and adopt what you need.";
|
|
604
|
+
function protectionIcon(file) {
|
|
605
|
+
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
606
|
+
}
|
|
607
|
+
function formatUpstreamChangedLines(file) {
|
|
608
|
+
if (file.upstreamChangedLines === void 0) return "";
|
|
609
|
+
const n = file.upstreamChangedLines;
|
|
610
|
+
return colors_default.yellow(` \xB7 ${n} ${n === 1 ? "line" : "lines"} changed upstream`);
|
|
611
|
+
}
|
|
612
|
+
function formatUpstreamLinesAbsent(file) {
|
|
613
|
+
const n = file.upstreamLinesAbsent ?? 0;
|
|
614
|
+
return n > 0 ? colors_default.yellow(` \xB7 ${n} upstream ${n === 1 ? "line" : "lines"} absent`) : "";
|
|
615
|
+
}
|
|
593
616
|
function printAnalysisFileGroups(files, linkOptions) {
|
|
594
617
|
printFileGroup(files, "behind", linkOptions, {
|
|
595
618
|
title: colors_default.cyan("\u2193 behind on upstream")
|
|
596
619
|
});
|
|
597
620
|
printFileGroup(files, "ahead", linkOptions, {
|
|
598
621
|
title: `${colors_default.blue("\u2191 protected in fork")}`,
|
|
599
|
-
|
|
622
|
+
suffix: formatUpstreamLinesAbsent,
|
|
623
|
+
hint: [
|
|
624
|
+
"these files have fork changes but are protected (pinned); upstream did not change them since the last sync.",
|
|
625
|
+
"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."
|
|
626
|
+
]
|
|
600
627
|
});
|
|
628
|
+
printFileSection(
|
|
629
|
+
findProtectedBehind(files),
|
|
630
|
+
`${warningMark} ${colors_default.yellow("protected but behind upstream")}`,
|
|
631
|
+
linkOptions,
|
|
632
|
+
{
|
|
633
|
+
icon: protectionIcon,
|
|
634
|
+
suffix: formatUpstreamChangedLines,
|
|
635
|
+
dateSource: "upstream",
|
|
636
|
+
hint: PROTECTED_BEHIND_HINT
|
|
637
|
+
}
|
|
638
|
+
);
|
|
601
639
|
printFileGroup(files, "drifted", linkOptions, {
|
|
602
640
|
title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
603
641
|
hint: "these files have fork changes but are not pinned or ignored."
|
|
@@ -607,11 +645,6 @@ function printAnalysisFileGroups(files, linkOptions) {
|
|
|
607
645
|
hint: "both fork and upstream changed.",
|
|
608
646
|
dateSource: "upstream"
|
|
609
647
|
});
|
|
610
|
-
printFileGroup(files, "pinned", linkOptions, {
|
|
611
|
-
title: `${colors_default.green("\u2A00 pinned")}`,
|
|
612
|
-
hint: "both changed, fork wins (pinned in cella/cella.config.ts).",
|
|
613
|
-
dateSource: "upstream"
|
|
614
|
-
});
|
|
615
648
|
}
|
|
616
649
|
function writeLogFile(forkPath, files) {
|
|
617
650
|
const logPath = join3(forkPath, "cella-sync.log");
|
|
@@ -646,16 +679,32 @@ function printSyncComplete(result, options = {}) {
|
|
|
646
679
|
if (options.stagedBranch) {
|
|
647
680
|
console.info(`${colors_default.green("\u2713")} Sync merge staged on '${options.stagedBranch}'`);
|
|
648
681
|
console.info(
|
|
649
|
-
colors_default.dim(
|
|
650
|
-
` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. Review, then rerun \`pnpm cella sync\` to finish.`
|
|
651
|
-
)
|
|
682
|
+
colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. Committing next...`)
|
|
652
683
|
);
|
|
653
684
|
} else {
|
|
654
685
|
console.info(`${colors_default.green("\u2713")} sync complete`);
|
|
655
686
|
console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
|
|
656
687
|
}
|
|
688
|
+
printProtectedConflicts(result);
|
|
657
689
|
console.info();
|
|
658
690
|
}
|
|
691
|
+
function printProtectedConflicts(result) {
|
|
692
|
+
const paths = result.protectedConflicts ?? [];
|
|
693
|
+
if (paths.length === 0) return;
|
|
694
|
+
const byPath = new Map(result.files.map((file) => [file.path, file]));
|
|
695
|
+
console.info();
|
|
696
|
+
console.info(
|
|
697
|
+
`${warningMark} ${colors_default.yellow(
|
|
698
|
+
`${paths.length} protected ${paths.length === 1 ? "file kept" : "files kept"} the fork version, dropping upstream changes:`
|
|
699
|
+
)}`
|
|
700
|
+
);
|
|
701
|
+
for (const path4 of paths) {
|
|
702
|
+
const file = byPath.get(path4);
|
|
703
|
+
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
704
|
+
console.info(` ${icon} ${path4}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
705
|
+
}
|
|
706
|
+
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
707
|
+
}
|
|
659
708
|
function findMaskingPins(files) {
|
|
660
709
|
return files.filter(
|
|
661
710
|
(file) => file.isPinned && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
@@ -1379,6 +1428,23 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1379
1428
|
}
|
|
1380
1429
|
return changes;
|
|
1381
1430
|
}
|
|
1431
|
+
async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
1432
|
+
const args = ["diff", "--numstat", "--no-renames", fromRef, toRef];
|
|
1433
|
+
if (paths.length > 0) args.push("--", ...paths);
|
|
1434
|
+
const out = await git(args, cwd, { ignoreErrors: true });
|
|
1435
|
+
const stat = /* @__PURE__ */ new Map();
|
|
1436
|
+
for (const line of out.split("\n")) {
|
|
1437
|
+
if (!line.trim()) continue;
|
|
1438
|
+
const [a, d, ...rest] = line.split(" ");
|
|
1439
|
+
const path4 = rest.join(" ");
|
|
1440
|
+
if (!path4) continue;
|
|
1441
|
+
stat.set(path4, {
|
|
1442
|
+
additions: a === "-" ? null : Number(a),
|
|
1443
|
+
deletions: d === "-" ? null : Number(d)
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
return stat;
|
|
1447
|
+
}
|
|
1382
1448
|
async function getFileHashesAtRef(cwd, ref) {
|
|
1383
1449
|
const output = await git(["ls-tree", "-r", ref], cwd);
|
|
1384
1450
|
const hashes = /* @__PURE__ */ new Map();
|
|
@@ -1749,6 +1815,7 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1749
1815
|
);
|
|
1750
1816
|
const analyzedFiles = [];
|
|
1751
1817
|
const handledOldPaths = /* @__PURE__ */ new Set();
|
|
1818
|
+
const renamedProtected = /* @__PURE__ */ new Set();
|
|
1752
1819
|
let processed = 0;
|
|
1753
1820
|
for (const filePath of allFiles) {
|
|
1754
1821
|
if (handledOldPaths.has(filePath)) continue;
|
|
@@ -1787,8 +1854,13 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1787
1854
|
handledOldPaths.add(oldPath);
|
|
1788
1855
|
const forkModifiedOld = forkOldHash !== baseOldHash;
|
|
1789
1856
|
const oldPathPinned = predicates.isPinned(oldPath);
|
|
1857
|
+
let upstreamChanged2;
|
|
1790
1858
|
if (fileIsPinned || oldPathPinned || predicates.isIgnored(oldPath)) {
|
|
1791
1859
|
status = "pinned";
|
|
1860
|
+
if (forkModifiedOld && upstreamHash !== baseOldHash) {
|
|
1861
|
+
upstreamChanged2 = true;
|
|
1862
|
+
renamedProtected.add(filePath);
|
|
1863
|
+
}
|
|
1792
1864
|
} else if (!oldPathInFork) {
|
|
1793
1865
|
status = "behind";
|
|
1794
1866
|
} else if (forkModifiedOld) {
|
|
@@ -1805,7 +1877,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1805
1877
|
isPinned: fileIsPinned,
|
|
1806
1878
|
existsInFork: inFork,
|
|
1807
1879
|
existsInUpstream: true,
|
|
1808
|
-
renamedFrom
|
|
1880
|
+
renamedFrom,
|
|
1881
|
+
upstreamChanged: upstreamChanged2
|
|
1809
1882
|
});
|
|
1810
1883
|
continue;
|
|
1811
1884
|
}
|
|
@@ -1828,26 +1901,58 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1828
1901
|
status = "identical";
|
|
1829
1902
|
} else {
|
|
1830
1903
|
const forkChanged = forkHash !== baseHash;
|
|
1831
|
-
const
|
|
1832
|
-
if (forkChanged &&
|
|
1904
|
+
const upstreamChanged2 = upstreamHash !== baseHash;
|
|
1905
|
+
if (forkChanged && upstreamChanged2) {
|
|
1833
1906
|
status = fileIsPinned ? "pinned" : "diverged";
|
|
1834
|
-
} else if (forkChanged && !
|
|
1907
|
+
} else if (forkChanged && !upstreamChanged2) {
|
|
1835
1908
|
status = fileIsPinned ? "ahead" : predicates.hard ? "behind" : "drifted";
|
|
1836
|
-
} else if (!forkChanged &&
|
|
1909
|
+
} else if (!forkChanged && upstreamChanged2) {
|
|
1837
1910
|
status = "behind";
|
|
1838
1911
|
} else {
|
|
1839
1912
|
status = "identical";
|
|
1840
1913
|
}
|
|
1841
1914
|
}
|
|
1915
|
+
const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
|
|
1842
1916
|
analyzedFiles.push({
|
|
1843
1917
|
path: filePath,
|
|
1844
1918
|
status,
|
|
1845
1919
|
isIgnored: fileIsIgnored,
|
|
1846
1920
|
isPinned: fileIsPinned,
|
|
1847
1921
|
existsInFork: inFork,
|
|
1848
|
-
existsInUpstream: inUpstream
|
|
1922
|
+
existsInUpstream: inUpstream,
|
|
1923
|
+
upstreamChanged: upstreamChanged || void 0
|
|
1849
1924
|
});
|
|
1850
1925
|
}
|
|
1926
|
+
const flagged = analyzedFiles.filter((file) => file.upstreamChanged && !renamedProtected.has(file.path));
|
|
1927
|
+
if (flagged.length > 0) {
|
|
1928
|
+
const stat = await getDiffStat(
|
|
1929
|
+
repoPath,
|
|
1930
|
+
mergeBaseRef,
|
|
1931
|
+
incomingRef,
|
|
1932
|
+
flagged.map((file) => file.path)
|
|
1933
|
+
);
|
|
1934
|
+
for (const file of flagged) {
|
|
1935
|
+
const entry = stat.get(file.path);
|
|
1936
|
+
if (entry && entry.additions !== null && entry.deletions !== null) {
|
|
1937
|
+
file.upstreamChangedLines = entry.additions + entry.deletions;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const stale = analyzedFiles.filter(
|
|
1942
|
+
(file) => file.status === "ahead" && file.isPinned && !file.isIgnored && file.existsInFork && file.existsInUpstream && forkHashes.get(file.path) !== upstreamHashes.get(file.path)
|
|
1943
|
+
);
|
|
1944
|
+
if (stale.length > 0) {
|
|
1945
|
+
const stat = await getDiffStat(
|
|
1946
|
+
repoPath,
|
|
1947
|
+
incomingRef,
|
|
1948
|
+
localRef,
|
|
1949
|
+
stale.map((file) => file.path)
|
|
1950
|
+
);
|
|
1951
|
+
for (const file of stale) {
|
|
1952
|
+
const deletions = stat.get(file.path)?.deletions;
|
|
1953
|
+
if (deletions !== null && deletions !== void 0) file.upstreamLinesAbsent = deletions;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1851
1956
|
return analyzedFiles;
|
|
1852
1957
|
}
|
|
1853
1958
|
async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomingRef) {
|
|
@@ -1869,7 +1974,7 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1869
1974
|
file.changedTs = info.timestamp;
|
|
1870
1975
|
file.changedCommit = info.hash;
|
|
1871
1976
|
}
|
|
1872
|
-
} else if (file.status === "diverged" || file.status === "pinned") {
|
|
1977
|
+
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged) {
|
|
1873
1978
|
const forkFileInfo = forkInfo.get(file.path);
|
|
1874
1979
|
const upstreamFileInfo = upstreamInfo.get(file.path);
|
|
1875
1980
|
if (forkFileInfo) {
|
|
@@ -2015,6 +2120,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2015
2120
|
}
|
|
2016
2121
|
}
|
|
2017
2122
|
}
|
|
2123
|
+
const protectedConflicts = analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path);
|
|
2018
2124
|
const gitConflicts = await getConflictedFiles(forkPath);
|
|
2019
2125
|
for (const filePath of gitConflicts) {
|
|
2020
2126
|
const isProtected = isIgnored(filePath, config) || isPinnedForSync(filePath, config, config.unpinned);
|
|
@@ -2022,6 +2128,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2022
2128
|
if (await fileExistsAtRef(forkPath, "HEAD", filePath)) {
|
|
2023
2129
|
onProgress?.(`\u2192 ${filePath}: keeping fork (protected conflict)`);
|
|
2024
2130
|
await restoreToHead(forkPath, filePath);
|
|
2131
|
+
if (!protectedConflicts.includes(filePath)) protectedConflicts.push(filePath);
|
|
2025
2132
|
} else {
|
|
2026
2133
|
onProgress?.(`\u2192 ${filePath}: removing (protected conflict, not in fork)`);
|
|
2027
2134
|
await removeFileFully(forkPath, filePath);
|
|
@@ -2039,7 +2146,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2039
2146
|
await removeFileFromWorktree(forkPath, filePath);
|
|
2040
2147
|
}
|
|
2041
2148
|
}
|
|
2042
|
-
return { remainingConflicts, analyzedFiles, autoMergedFiles };
|
|
2149
|
+
return { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts };
|
|
2043
2150
|
}
|
|
2044
2151
|
function calculateSummary(files) {
|
|
2045
2152
|
const summary = {
|
|
@@ -2132,7 +2239,7 @@ function resultMeta(config, ctx) {
|
|
|
2132
2239
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2133
2240
|
const { forkPath } = config;
|
|
2134
2241
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2135
|
-
const { remainingConflicts, analyzedFiles, autoMergedFiles } = await applyDirectMerge(
|
|
2242
|
+
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2136
2243
|
forkPath,
|
|
2137
2244
|
upstreamRef,
|
|
2138
2245
|
mergeBase,
|
|
@@ -2179,6 +2286,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2179
2286
|
summary,
|
|
2180
2287
|
conflicts: remainingConflicts,
|
|
2181
2288
|
autoMergedFiles,
|
|
2289
|
+
protectedConflicts,
|
|
2182
2290
|
...resultMeta(config, ctx)
|
|
2183
2291
|
};
|
|
2184
2292
|
}
|
|
@@ -2215,6 +2323,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2215
2323
|
summary: calculateSummary(analyzedFiles),
|
|
2216
2324
|
// For analyze mode, count diverged files as potential conflicts
|
|
2217
2325
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2326
|
+
protectedConflicts: analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path),
|
|
2218
2327
|
...resultMeta(config, ctx)
|
|
2219
2328
|
};
|
|
2220
2329
|
}
|
|
@@ -2241,7 +2350,8 @@ var scopeStatuses = {
|
|
|
2241
2350
|
};
|
|
2242
2351
|
function filterByScope(files, scope) {
|
|
2243
2352
|
const statuses = scopeStatuses[scope];
|
|
2244
|
-
|
|
2353
|
+
const includeProtectedBehind = scope !== "risk";
|
|
2354
|
+
return files.filter((f) => statuses.has(f.status) || includeProtectedBehind && f.upstreamChanged === true);
|
|
2245
2355
|
}
|
|
2246
2356
|
function findTargetFile(files, targetPath) {
|
|
2247
2357
|
const exact = files.find((f) => f.path === targetPath);
|
|
@@ -2282,7 +2392,10 @@ async function runAnalyze(config) {
|
|
|
2282
2392
|
changedAt: f.changedAt ?? null,
|
|
2283
2393
|
changedCommit: f.changedCommit ?? null,
|
|
2284
2394
|
upstreamChangedAt: f.upstreamChangedAt ?? null,
|
|
2285
|
-
upstreamCommit: f.upstreamCommit ?? null
|
|
2395
|
+
upstreamCommit: f.upstreamCommit ?? null,
|
|
2396
|
+
upstreamChanged: f.upstreamChanged ?? false,
|
|
2397
|
+
upstreamChangedLines: f.upstreamChangedLines ?? null,
|
|
2398
|
+
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null
|
|
2286
2399
|
}));
|
|
2287
2400
|
writeStdout(JSON.stringify(out, null, 2));
|
|
2288
2401
|
return result;
|
|
@@ -3381,21 +3494,6 @@ async function forkRefMeta(cellaPath, forkRef) {
|
|
|
3381
3494
|
const [sha = forkRef.slice(0, 7), date = "unknown"] = out.split(" ");
|
|
3382
3495
|
return { sha, date };
|
|
3383
3496
|
}
|
|
3384
|
-
async function diffStat(cellaPath, baseRef, ref) {
|
|
3385
|
-
const out = await git(["diff", "--numstat", `${baseRef}..${ref}`], cellaPath, { ignoreErrors: true });
|
|
3386
|
-
const stat = /* @__PURE__ */ new Map();
|
|
3387
|
-
for (const line of out.split("\n")) {
|
|
3388
|
-
if (!line.trim()) continue;
|
|
3389
|
-
const [a, d, ...rest] = line.split(" ");
|
|
3390
|
-
const path4 = rest.join(" ");
|
|
3391
|
-
if (!path4) continue;
|
|
3392
|
-
stat.set(path4, {
|
|
3393
|
-
additions: a === "-" ? null : Number(a),
|
|
3394
|
-
deletions: d === "-" ? null : Number(d)
|
|
3395
|
-
});
|
|
3396
|
-
}
|
|
3397
|
-
return stat;
|
|
3398
|
-
}
|
|
3399
3497
|
async function runContributions(config) {
|
|
3400
3498
|
const forks = config.forks ?? [];
|
|
3401
3499
|
if (forks.length === 0) {
|
|
@@ -3470,7 +3568,7 @@ async function runContributions(config) {
|
|
|
3470
3568
|
if (countDetection(detection) > 0) {
|
|
3471
3569
|
const { branch, appliedFiles } = await buildContribBranch(config.forkPath, baseRef, forkRef, detection, forkName);
|
|
3472
3570
|
if (appliedFiles.length > 0) {
|
|
3473
|
-
const stat = await
|
|
3571
|
+
const stat = await getDiffStat(config.forkPath, baseRef, branch);
|
|
3474
3572
|
const metaByPath = new Map(detection.files.map((f) => [f.path, f]));
|
|
3475
3573
|
for (const path4 of [...detection.modified, ...detection.created]) {
|
|
3476
3574
|
const fileMeta = metaByPath.get(path4);
|
|
@@ -3718,6 +3816,18 @@ function safeMergeRecord(forkRecord, upstreamRecord, addOnly = false) {
|
|
|
3718
3816
|
const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
|
|
3719
3817
|
return { merged: sorted, changed, added };
|
|
3720
3818
|
}
|
|
3819
|
+
function isSubpathMap(value) {
|
|
3820
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3821
|
+
return Object.keys(value).every((name) => name.startsWith("."));
|
|
3822
|
+
}
|
|
3823
|
+
function safeMergeExports(forkExports, upstreamExports) {
|
|
3824
|
+
if (!isSubpathMap(upstreamExports)) return void 0;
|
|
3825
|
+
if (forkExports !== void 0 && !isSubpathMap(forkExports)) return void 0;
|
|
3826
|
+
const merged = { ...forkExports || {} };
|
|
3827
|
+
const added = Object.keys(upstreamExports).filter((name) => !(name in merged));
|
|
3828
|
+
for (const name of added) merged[name] = upstreamExports[name];
|
|
3829
|
+
return { merged, changed: added.length > 0, added };
|
|
3830
|
+
}
|
|
3721
3831
|
function safeMergePnpm(forkPnpm, upstreamPnpm) {
|
|
3722
3832
|
if (!upstreamPnpm) return void 0;
|
|
3723
3833
|
const merged = { ...forkPnpm || {} };
|
|
@@ -3832,6 +3942,15 @@ async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync)
|
|
|
3832
3942
|
}
|
|
3833
3943
|
continue;
|
|
3834
3944
|
}
|
|
3945
|
+
if (key === "exports") {
|
|
3946
|
+
const result2 = safeMergeExports(forkPkg.exports, upstreamPkg.exports);
|
|
3947
|
+
if (result2?.changed) {
|
|
3948
|
+
forkPkg.exports = result2.merged;
|
|
3949
|
+
updated = true;
|
|
3950
|
+
changes.push(...result2.added.map((name) => `exports.${name}: added`));
|
|
3951
|
+
}
|
|
3952
|
+
continue;
|
|
3953
|
+
}
|
|
3835
3954
|
const forkValue = forkPkg[key];
|
|
3836
3955
|
const upstreamValue = upstreamPkg[key];
|
|
3837
3956
|
const addOnly = key === "scripts";
|
|
@@ -4114,7 +4233,7 @@ async function runSyncCycle(config) {
|
|
|
4114
4233
|
}
|
|
4115
4234
|
return { status: "staged", branch };
|
|
4116
4235
|
}
|
|
4117
|
-
async function
|
|
4236
|
+
async function commitSyncMerge(config, branch) {
|
|
4118
4237
|
const { forkPath } = config;
|
|
4119
4238
|
const conflicts = await getConflictedFiles(forkPath);
|
|
4120
4239
|
console.info();
|
|
@@ -4137,7 +4256,12 @@ async function resumeSyncMerge(config, branch) {
|
|
|
4137
4256
|
await commitSquash(forkPath, message);
|
|
4138
4257
|
console.info();
|
|
4139
4258
|
console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
|
|
4140
|
-
|
|
4259
|
+
printTriageSteps(branch);
|
|
4260
|
+
}
|
|
4261
|
+
function printTriageSteps(branch) {
|
|
4262
|
+
console.info(colors_default.dim(`staying on '${branch}' without pushing.`));
|
|
4263
|
+
console.info(colors_default.dim(" run drift triage (`pnpm cella analyze`), commit any follow-ups, then:"));
|
|
4264
|
+
console.info(colors_default.dim(" pnpm cella sync (pushes the branch and opens the PR)"));
|
|
4141
4265
|
}
|
|
4142
4266
|
async function mergeOpenSyncPrs(config, open) {
|
|
4143
4267
|
const { forkPath, settings } = config;
|
|
@@ -4202,7 +4326,7 @@ async function runSyncCommand(config) {
|
|
|
4202
4326
|
const currentBranch = await getCurrentBranch(forkPath);
|
|
4203
4327
|
const onSyncBranch = isTemporarySyncBranch(currentBranch);
|
|
4204
4328
|
if (onSyncBranch && mergeInProgress(forkPath)) {
|
|
4205
|
-
await
|
|
4329
|
+
await commitSyncMerge(config, currentBranch);
|
|
4206
4330
|
return;
|
|
4207
4331
|
}
|
|
4208
4332
|
if (onSyncBranch) {
|
|
@@ -4231,9 +4355,11 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
4231
4355
|
if (outcome.status === "conflicts") {
|
|
4232
4356
|
console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve and stage them, then:`)}`);
|
|
4233
4357
|
printFinishSteps();
|
|
4234
|
-
console.info(colors_default.dim(" rerun commits the sync
|
|
4358
|
+
console.info(colors_default.dim(" rerun commits the sync and stops for drift triage; a further rerun ships (push + PR)."));
|
|
4235
4359
|
console.info(colors_default.dim(" let the rerun commit \u2014 a manual `git commit` records a merge commit that bloats the PR."));
|
|
4360
|
+
return;
|
|
4236
4361
|
}
|
|
4362
|
+
await commitSyncMerge(config, temporaryBranch);
|
|
4237
4363
|
}
|
|
4238
4364
|
|
|
4239
4365
|
// src/services/forks.ts
|