@cellajs/cli 0.2.1 → 0.2.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 +43 -5
- package/dist/config.js.map +1 -1
- package/dist/index.js +441 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
// src/cella-cli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { existsSync as existsSync11, statSync as statSync3 } from "fs";
|
|
5
|
+
import { join as join18, resolve as resolve6 } from "path";
|
|
6
6
|
import process9 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
@@ -106,6 +106,10 @@ function isConfigFile(filePath) {
|
|
|
106
106
|
function isPackageJson(filePath) {
|
|
107
107
|
return filePath === "package.json" || filePath.endsWith("/package.json");
|
|
108
108
|
}
|
|
109
|
+
function isGeneratedFile(filePath) {
|
|
110
|
+
const segments = filePath.split("/");
|
|
111
|
+
return segments.includes("gen") || (segments.at(-1) ?? "").includes(".gen.");
|
|
112
|
+
}
|
|
109
113
|
function isManagedFile(filePath) {
|
|
110
114
|
return isPackageJson(filePath) || filePath === "pnpm-lock.yaml" || isConfigFile(filePath);
|
|
111
115
|
}
|
|
@@ -216,7 +220,7 @@ import process4 from "process";
|
|
|
216
220
|
// package.json
|
|
217
221
|
var package_default = {
|
|
218
222
|
name: "@cellajs/cli",
|
|
219
|
-
version: "0.2.
|
|
223
|
+
version: "0.2.3",
|
|
220
224
|
private: false,
|
|
221
225
|
license: "MIT",
|
|
222
226
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -705,9 +709,58 @@ function printProtectedConflicts(result) {
|
|
|
705
709
|
}
|
|
706
710
|
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
707
711
|
}
|
|
712
|
+
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
713
|
+
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
714
|
+
}
|
|
715
|
+
function shellPath(path4) {
|
|
716
|
+
return /^[\w./@+-]+$/.test(path4) ? path4 : `'${path4.replace(/'/g, `'\\''`)}'`;
|
|
717
|
+
}
|
|
718
|
+
function printIgnoredUpstreamChanges(result) {
|
|
719
|
+
const groups = result.ignoredUpstreamChanges ?? [];
|
|
720
|
+
if (groups.length === 0) return;
|
|
721
|
+
const total = groups.reduce((count, group) => count + group.paths.length, 0);
|
|
722
|
+
console.info();
|
|
723
|
+
console.info(
|
|
724
|
+
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
725
|
+
);
|
|
726
|
+
for (const group of groups) {
|
|
727
|
+
const counts = [plural(group.paths.length, "file")];
|
|
728
|
+
if (group.added > 0) counts.push(`${group.added} new`);
|
|
729
|
+
if (group.deleted > 0) counts.push(`${group.deleted} deleted`);
|
|
730
|
+
console.info(` ${statusConfig.ignored.icon} ${group.entry} ${colors_default.dim(`\xB7 ${counts.join(", ")}`)}`);
|
|
731
|
+
if (result.upstreamDiffRange) {
|
|
732
|
+
console.info(colors_default.dim(` git diff ${result.upstreamDiffRange} -- ${shellPath(group.entry)}`));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
console.info(colors_default.dim(" the fork left these untouched and ignored paths never sync: diff and adopt what you need."));
|
|
736
|
+
}
|
|
737
|
+
function printUpstreamOverrideChanges(result) {
|
|
738
|
+
const report = result.upstreamOverrides;
|
|
739
|
+
if (!report) return;
|
|
740
|
+
console.info();
|
|
741
|
+
if (report.kind === "unreadable") {
|
|
742
|
+
const side = report.side === "base" ? "the last sync point" : "the incoming ref";
|
|
743
|
+
console.info(
|
|
744
|
+
colors_default.dim(`upstream overrides not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
|
|
745
|
+
);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const lines = [];
|
|
749
|
+
for (const list of ["pinned", "ignored"]) {
|
|
750
|
+
for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
|
|
751
|
+
for (const entry of report[list].removed) {
|
|
752
|
+
lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
console.info(
|
|
756
|
+
`${warningMark} ${colors_default.yellow(`upstream changed its sync overrides \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
|
|
757
|
+
);
|
|
758
|
+
for (const line of lines) console.info(line);
|
|
759
|
+
console.info(colors_default.dim(` ${CONFIG_FILE} never syncs: add or drop these by hand where they fit your app.`));
|
|
760
|
+
}
|
|
708
761
|
function findMaskingPins(files) {
|
|
709
762
|
return files.filter(
|
|
710
|
-
(file) => file.isPinned && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
763
|
+
(file) => file.isPinned && !isManagedFile(file.path) && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
711
764
|
);
|
|
712
765
|
}
|
|
713
766
|
function printMaskingPinWarning(files) {
|
|
@@ -762,6 +815,23 @@ function isPinned(filePath, config) {
|
|
|
762
815
|
if (isManagedFile(filePath)) return true;
|
|
763
816
|
return isUnderAnyFolder(filePath, config.overrides?.pinned || []);
|
|
764
817
|
}
|
|
818
|
+
function groupIgnoredUpstreamChanges(files, ignored) {
|
|
819
|
+
const entries = [...new Set(ignored.map((entry) => entry.replace(/\/+$/, "")).filter(Boolean))];
|
|
820
|
+
const mostSpecificFirst = [...entries].sort((a, b) => b.length - a.length);
|
|
821
|
+
const groups = /* @__PURE__ */ new Map();
|
|
822
|
+
for (const file of files) {
|
|
823
|
+
if (!file.upstreamOnly || isManagedFile(file.path) || isGeneratedFile(file.path)) continue;
|
|
824
|
+
const entry = mostSpecificFirst.find((candidate) => isUnderAnyFolder(file.path, [candidate]));
|
|
825
|
+
if (!entry) continue;
|
|
826
|
+
const group = groups.get(entry) ?? { entry, paths: [], added: 0, deleted: 0 };
|
|
827
|
+
group.paths.push(file.path);
|
|
828
|
+
if (!file.existsInFork) group.added++;
|
|
829
|
+
if (!file.existsInUpstream) group.deleted++;
|
|
830
|
+
groups.set(entry, group);
|
|
831
|
+
}
|
|
832
|
+
for (const group of groups.values()) group.paths.sort();
|
|
833
|
+
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
834
|
+
}
|
|
765
835
|
function isPinnedForSync(filePath, config, unpinned) {
|
|
766
836
|
if (isManagedFile(filePath)) return true;
|
|
767
837
|
if (unpinned) return false;
|
|
@@ -1791,7 +1861,115 @@ function registerSignalHandlers() {
|
|
|
1791
1861
|
cleanupRegistered = true;
|
|
1792
1862
|
}
|
|
1793
1863
|
|
|
1864
|
+
// src/utils/upstream-overrides.ts
|
|
1865
|
+
import { Node, Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1866
|
+
var LIST_KEYS = ["pinned", "ignored"];
|
|
1867
|
+
function resolveExpression(node, sourceFile, depth = 0) {
|
|
1868
|
+
if (!node || depth > 8) return node;
|
|
1869
|
+
if (Node.isParenthesizedExpression(node) || Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) {
|
|
1870
|
+
return resolveExpression(node.getExpression(), sourceFile, depth + 1);
|
|
1871
|
+
}
|
|
1872
|
+
if (Node.isIdentifier(node)) {
|
|
1873
|
+
return resolveExpression(
|
|
1874
|
+
sourceFile.getVariableDeclaration(node.getText())?.getInitializer(),
|
|
1875
|
+
sourceFile,
|
|
1876
|
+
depth + 1
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
return node;
|
|
1880
|
+
}
|
|
1881
|
+
function findConfigObject(sourceFile) {
|
|
1882
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression)) {
|
|
1883
|
+
if (call.getExpression().getText() !== "defineConfig") continue;
|
|
1884
|
+
const arg = resolveExpression(call.getArguments()[0], sourceFile);
|
|
1885
|
+
if (Node.isObjectLiteralExpression(arg)) return arg;
|
|
1886
|
+
}
|
|
1887
|
+
const exported = sourceFile.getExportAssignment((assignment) => !assignment.isExportEquals());
|
|
1888
|
+
const value = resolveExpression(exported?.getExpression(), sourceFile);
|
|
1889
|
+
return Node.isObjectLiteralExpression(value) ? value : void 0;
|
|
1890
|
+
}
|
|
1891
|
+
function readProperty(object, name, sourceFile) {
|
|
1892
|
+
for (const property of object.getProperties()) {
|
|
1893
|
+
if (Node.isPropertyAssignment(property)) {
|
|
1894
|
+
if (property.getName().replace(/^['"`]|['"`]$/g, "") !== name) continue;
|
|
1895
|
+
return resolveExpression(property.getInitializer(), sourceFile) ?? null;
|
|
1896
|
+
}
|
|
1897
|
+
if (Node.isShorthandPropertyAssignment(property) && property.getName() === name) {
|
|
1898
|
+
return resolveExpression(property.getNameNode(), sourceFile) ?? null;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
return void 0;
|
|
1902
|
+
}
|
|
1903
|
+
function parseOverrideLists(source) {
|
|
1904
|
+
try {
|
|
1905
|
+
const project = new Project2({ useInMemoryFileSystem: true, skipLoadingLibFiles: true });
|
|
1906
|
+
const sourceFile = project.createSourceFile("cella.config.ts", source);
|
|
1907
|
+
const config = findConfigObject(sourceFile);
|
|
1908
|
+
if (!config) return null;
|
|
1909
|
+
const overrides = readProperty(config, "overrides", sourceFile);
|
|
1910
|
+
if (overrides === void 0) return { pinned: [], ignored: [] };
|
|
1911
|
+
if (!overrides || !Node.isObjectLiteralExpression(overrides)) return null;
|
|
1912
|
+
const lists = { pinned: [], ignored: [] };
|
|
1913
|
+
for (const key of LIST_KEYS) {
|
|
1914
|
+
const list = readProperty(overrides, key, sourceFile);
|
|
1915
|
+
if (list === void 0) continue;
|
|
1916
|
+
if (!list || !Node.isArrayLiteralExpression(list)) return null;
|
|
1917
|
+
for (const element of list.getElements()) {
|
|
1918
|
+
if (Node.isStringLiteral(element) || Node.isNoSubstitutionTemplateLiteral(element)) {
|
|
1919
|
+
lists[key].push(element.getLiteralValue());
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return lists;
|
|
1924
|
+
} catch {
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function normalizeEntry(entry) {
|
|
1929
|
+
return entry.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1930
|
+
}
|
|
1931
|
+
function diffOverrideLists(base, incoming, fork) {
|
|
1932
|
+
const diff = (key) => {
|
|
1933
|
+
const baseSet = new Set(base[key].map(normalizeEntry).filter(Boolean));
|
|
1934
|
+
const incomingSet = new Set(incoming[key].map(normalizeEntry).filter(Boolean));
|
|
1935
|
+
const forkEntries = fork[key].map(normalizeEntry).filter(Boolean);
|
|
1936
|
+
return {
|
|
1937
|
+
added: [...incomingSet].filter((entry) => !baseSet.has(entry) && !isUnderAnyFolder(entry, forkEntries)),
|
|
1938
|
+
removed: [...baseSet].filter((entry) => !incomingSet.has(entry) && forkEntries.includes(entry))
|
|
1939
|
+
};
|
|
1940
|
+
};
|
|
1941
|
+
return { pinned: diff("pinned"), ignored: diff("ignored") };
|
|
1942
|
+
}
|
|
1943
|
+
async function readConfigSourceAtRef(repoPath, ref) {
|
|
1944
|
+
for (const path4 of CONFIG_FILE_PATHS) {
|
|
1945
|
+
const source = await git(["show", `${ref}:${path4}`], repoPath, { ignoreErrors: true });
|
|
1946
|
+
if (source) return source;
|
|
1947
|
+
}
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1950
|
+
async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, forkConfig) {
|
|
1951
|
+
const [baseSource, incomingSource] = await Promise.all([
|
|
1952
|
+
readConfigSourceAtRef(repoPath, mergeBaseRef),
|
|
1953
|
+
readConfigSourceAtRef(repoPath, incomingRef)
|
|
1954
|
+
]);
|
|
1955
|
+
if (baseSource === incomingSource) return void 0;
|
|
1956
|
+
const base = baseSource === null ? null : parseOverrideLists(baseSource);
|
|
1957
|
+
if (!base) return { kind: "unreadable", side: "base" };
|
|
1958
|
+
const incoming = incomingSource === null ? null : parseOverrideLists(incomingSource);
|
|
1959
|
+
if (!incoming) return { kind: "unreadable", side: "incoming" };
|
|
1960
|
+
const fork = {
|
|
1961
|
+
pinned: forkConfig.overrides?.pinned ?? [],
|
|
1962
|
+
ignored: forkConfig.overrides?.ignored ?? []
|
|
1963
|
+
};
|
|
1964
|
+
const changes = diffOverrideLists(base, incoming, fork);
|
|
1965
|
+
const count = LIST_KEYS.reduce((n, key) => n + changes[key].added.length + changes[key].removed.length, 0);
|
|
1966
|
+
return count > 0 ? { kind: "changes", ...changes } : void 0;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1794
1969
|
// src/services/analyze-core.ts
|
|
1970
|
+
function isReportableIgnored(filePath) {
|
|
1971
|
+
return !isManagedFile(filePath) && !isGeneratedFile(filePath);
|
|
1972
|
+
}
|
|
1795
1973
|
async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predicates, onProgress) {
|
|
1796
1974
|
onProgress?.("collecting file hashes (batch)...");
|
|
1797
1975
|
const [forkHashes, upstreamHashes, baseHashes] = await Promise.all([
|
|
@@ -1870,6 +2048,7 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1870
2048
|
status = "renamed";
|
|
1871
2049
|
renamedFrom = oldPath;
|
|
1872
2050
|
}
|
|
2051
|
+
const upstreamOnly2 = fileIsIgnored && !forkModifiedOld && isReportableIgnored(filePath);
|
|
1873
2052
|
analyzedFiles.push({
|
|
1874
2053
|
path: filePath,
|
|
1875
2054
|
status,
|
|
@@ -1878,7 +2057,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1878
2057
|
existsInFork: inFork,
|
|
1879
2058
|
existsInUpstream: true,
|
|
1880
2059
|
renamedFrom,
|
|
1881
|
-
upstreamChanged: upstreamChanged2
|
|
2060
|
+
upstreamChanged: upstreamChanged2,
|
|
2061
|
+
upstreamOnly: upstreamOnly2 || void 0
|
|
1882
2062
|
});
|
|
1883
2063
|
continue;
|
|
1884
2064
|
}
|
|
@@ -1913,6 +2093,7 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1913
2093
|
}
|
|
1914
2094
|
}
|
|
1915
2095
|
const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
|
|
2096
|
+
const upstreamOnly = fileIsIgnored && isReportableIgnored(filePath) && upstreamHash !== baseHash && forkHash === baseHash;
|
|
1916
2097
|
analyzedFiles.push({
|
|
1917
2098
|
path: filePath,
|
|
1918
2099
|
status,
|
|
@@ -1920,7 +2101,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1920
2101
|
isPinned: fileIsPinned,
|
|
1921
2102
|
existsInFork: inFork,
|
|
1922
2103
|
existsInUpstream: inUpstream,
|
|
1923
|
-
upstreamChanged: upstreamChanged || void 0
|
|
2104
|
+
upstreamChanged: upstreamChanged || void 0,
|
|
2105
|
+
upstreamOnly: upstreamOnly || void 0
|
|
1924
2106
|
});
|
|
1925
2107
|
}
|
|
1926
2108
|
const flagged = analyzedFiles.filter((file) => file.upstreamChanged && !renamedProtected.has(file.path));
|
|
@@ -2236,6 +2418,24 @@ function resultMeta(config, ctx) {
|
|
|
2236
2418
|
upstreamCommits: ctx.upstreamCommits
|
|
2237
2419
|
};
|
|
2238
2420
|
}
|
|
2421
|
+
async function upstreamOnlyReports(config, ctx, files) {
|
|
2422
|
+
const { forkPath } = config;
|
|
2423
|
+
try {
|
|
2424
|
+
const [baseSha, upstreamSha, upstreamOverrides] = await Promise.all([
|
|
2425
|
+
getShortSha(forkPath, ctx.mergeBase),
|
|
2426
|
+
// the commit, not an annotated release tag object
|
|
2427
|
+
getShortSha(forkPath, ctx.upstreamCommit.hash),
|
|
2428
|
+
compareUpstreamOverrides(forkPath, ctx.mergeBase, ctx.upstreamRef, config)
|
|
2429
|
+
]);
|
|
2430
|
+
return {
|
|
2431
|
+
upstreamDiffRange: `${baseSha}..${upstreamSha}`,
|
|
2432
|
+
upstreamOverrides,
|
|
2433
|
+
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? [])
|
|
2434
|
+
};
|
|
2435
|
+
} catch {
|
|
2436
|
+
return {};
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2239
2439
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2240
2440
|
const { forkPath } = config;
|
|
2241
2441
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
@@ -2287,7 +2487,8 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2287
2487
|
conflicts: remainingConflicts,
|
|
2288
2488
|
autoMergedFiles,
|
|
2289
2489
|
protectedConflicts,
|
|
2290
|
-
...resultMeta(config, ctx)
|
|
2490
|
+
...resultMeta(config, ctx),
|
|
2491
|
+
...await upstreamOnlyReports(config, ctx, analyzedFiles)
|
|
2291
2492
|
};
|
|
2292
2493
|
}
|
|
2293
2494
|
async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
@@ -2314,6 +2515,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2314
2515
|
onProgress
|
|
2315
2516
|
);
|
|
2316
2517
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2518
|
+
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles);
|
|
2317
2519
|
onStep?.("analysis complete", `${analyzedFiles.length} files analyzed, dry run \u2014 no changes applied`);
|
|
2318
2520
|
onProgress?.("cleaning up worktree...");
|
|
2319
2521
|
await cleanupWorktree(forkPath, worktreePath);
|
|
@@ -2324,7 +2526,8 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2324
2526
|
// For analyze mode, count diverged files as potential conflicts
|
|
2325
2527
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2326
2528
|
protectedConflicts: analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path),
|
|
2327
|
-
...resultMeta(config, ctx)
|
|
2529
|
+
...resultMeta(config, ctx),
|
|
2530
|
+
...reports
|
|
2328
2531
|
};
|
|
2329
2532
|
}
|
|
2330
2533
|
async function runMergeEngine(config, options) {
|
|
@@ -2348,10 +2551,13 @@ var scopeStatuses = {
|
|
|
2348
2551
|
risk: /* @__PURE__ */ new Set(["drifted", "diverged"]),
|
|
2349
2552
|
protected: /* @__PURE__ */ new Set(["ahead"])
|
|
2350
2553
|
};
|
|
2351
|
-
function filterByScope(
|
|
2554
|
+
function filterByScope(result, scope) {
|
|
2352
2555
|
const statuses = scopeStatuses[scope];
|
|
2353
|
-
|
|
2354
|
-
|
|
2556
|
+
if (scope === "risk") return result.files.filter((f) => statuses.has(f.status));
|
|
2557
|
+
const overridesChanged = result.upstreamOverrides?.kind === "changes";
|
|
2558
|
+
return result.files.filter(
|
|
2559
|
+
(f) => statuses.has(f.status) || f.upstreamChanged === true || f.upstreamOnly === true || overridesChanged && f.path === CONFIG_FILE
|
|
2560
|
+
);
|
|
2355
2561
|
}
|
|
2356
2562
|
function findTargetFile(files, targetPath) {
|
|
2357
2563
|
const exact = files.find((f) => f.path === targetPath);
|
|
@@ -2378,7 +2584,7 @@ async function runAnalyze(config) {
|
|
|
2378
2584
|
}
|
|
2379
2585
|
});
|
|
2380
2586
|
spinnerSuccess();
|
|
2381
|
-
const scopedFiles = filterByScope(result
|
|
2587
|
+
const scopedFiles = filterByScope(result, config.scope ?? "all");
|
|
2382
2588
|
if (config.diff) {
|
|
2383
2589
|
const file = findTargetFile(scopedFiles, config.diff) ?? findTargetFile(result.files, config.diff);
|
|
2384
2590
|
if (!file) throw new Error(`file not found in analysis results: ${config.diff}`);
|
|
@@ -2386,6 +2592,7 @@ async function runAnalyze(config) {
|
|
|
2386
2592
|
return result;
|
|
2387
2593
|
}
|
|
2388
2594
|
if (config.json) {
|
|
2595
|
+
const overrides = result.upstreamOverrides?.kind === "changes" ? result.upstreamOverrides : void 0;
|
|
2389
2596
|
const out = scopedFiles.map((f) => ({
|
|
2390
2597
|
path: f.path,
|
|
2391
2598
|
status: f.status,
|
|
@@ -2395,7 +2602,10 @@ async function runAnalyze(config) {
|
|
|
2395
2602
|
upstreamCommit: f.upstreamCommit ?? null,
|
|
2396
2603
|
upstreamChanged: f.upstreamChanged ?? false,
|
|
2397
2604
|
upstreamChangedLines: f.upstreamChangedLines ?? null,
|
|
2398
|
-
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null
|
|
2605
|
+
upstreamLinesAbsent: f.upstreamLinesAbsent ?? null,
|
|
2606
|
+
upstreamOnly: f.upstreamOnly ?? false,
|
|
2607
|
+
// Only on the sync config: upstream override entries the fork config does not follow
|
|
2608
|
+
upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored } : null
|
|
2399
2609
|
}));
|
|
2400
2610
|
writeStdout(JSON.stringify(out, null, 2));
|
|
2401
2611
|
return result;
|
|
@@ -2436,6 +2646,8 @@ async function runAnalyze(config) {
|
|
|
2436
2646
|
};
|
|
2437
2647
|
printAnalysisFileGroups(result.files, linkOptions);
|
|
2438
2648
|
printSummary(result.summary, "analysis summary");
|
|
2649
|
+
printIgnoredUpstreamChanges(result);
|
|
2650
|
+
printUpstreamOverrideChanges(result);
|
|
2439
2651
|
printMaskingPinWarning(result.files);
|
|
2440
2652
|
if (config.logFile) {
|
|
2441
2653
|
const logPath = writeLogFile(config.forkPath, result.files);
|
|
@@ -2893,11 +3105,11 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
2893
3105
|
const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
2894
3106
|
const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
|
|
2895
3107
|
for (const [dependentName, { location, count }] of sortedDependents) {
|
|
2896
|
-
const
|
|
2897
|
-
const relativePath = path2.relative(forkPath,
|
|
3108
|
+
const packageJsonPath2 = path2.join(location, "package.json");
|
|
3109
|
+
const relativePath = path2.relative(forkPath, packageJsonPath2);
|
|
2898
3110
|
const paddedName = dependentName.padEnd(maxDependentNameLen);
|
|
2899
3111
|
console.info(
|
|
2900
|
-
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${
|
|
3112
|
+
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count}`)}`
|
|
2901
3113
|
);
|
|
2902
3114
|
}
|
|
2903
3115
|
}
|
|
@@ -3768,8 +3980,93 @@ function closePr(cwd, ref) {
|
|
|
3768
3980
|
}
|
|
3769
3981
|
|
|
3770
3982
|
// src/services/packages.ts
|
|
3771
|
-
import { existsSync as
|
|
3983
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
3984
|
+
import { join as join15 } from "path";
|
|
3985
|
+
|
|
3986
|
+
// src/services/package-usage.ts
|
|
3987
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, statSync } from "fs";
|
|
3772
3988
|
import { join as join14 } from "path";
|
|
3989
|
+
var MAX_FILE_BYTES = 1024 * 1024;
|
|
3990
|
+
function escapeRegExp(value) {
|
|
3991
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3992
|
+
}
|
|
3993
|
+
function readSearchable(filePath) {
|
|
3994
|
+
try {
|
|
3995
|
+
if (statSync(filePath).size > MAX_FILE_BYTES) return "";
|
|
3996
|
+
const text = readFileSync5(filePath, "utf-8");
|
|
3997
|
+
return text.includes("\0") ? "" : text;
|
|
3998
|
+
} catch {
|
|
3999
|
+
return "";
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
function fileSource(forkPath, path4) {
|
|
4003
|
+
let text;
|
|
4004
|
+
return { path: path4, label: path4, text: () => text ??= readSearchable(join14(forkPath, path4)) };
|
|
4005
|
+
}
|
|
4006
|
+
async function readScriptsAtRef(forkPath, ref, path4) {
|
|
4007
|
+
try {
|
|
4008
|
+
return JSON.parse(await git(["show", `${ref}:${path4}`], forkPath)).scripts ?? {};
|
|
4009
|
+
} catch {
|
|
4010
|
+
return {};
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
async function scriptSources(forkPath, upstreamRef, baseRef, path4) {
|
|
4014
|
+
let forkScripts;
|
|
4015
|
+
try {
|
|
4016
|
+
forkScripts = JSON.parse(readFileSync5(join14(forkPath, path4), "utf-8")).scripts ?? {};
|
|
4017
|
+
} catch {
|
|
4018
|
+
return [];
|
|
4019
|
+
}
|
|
4020
|
+
const baseScripts = await readScriptsAtRef(forkPath, baseRef, path4);
|
|
4021
|
+
const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef, path4);
|
|
4022
|
+
return Object.entries(forkScripts).filter(([name, value]) => value !== baseScripts[name] && value !== upstreamScripts[name]).map(([name, value]) => ({ path: path4, label: `${path4} (scripts.${name})`, text: () => value }));
|
|
4023
|
+
}
|
|
4024
|
+
function binNames(forkPath, location, name) {
|
|
4025
|
+
const bareName = name.split("/").pop();
|
|
4026
|
+
for (const dir of [join14(forkPath, location), forkPath]) {
|
|
4027
|
+
const manifest = join14(dir, "node_modules", name, "package.json");
|
|
4028
|
+
if (!existsSync8(manifest)) continue;
|
|
4029
|
+
try {
|
|
4030
|
+
const { bin } = JSON.parse(readFileSync5(manifest, "utf-8"));
|
|
4031
|
+
if (typeof bin === "string") return [bareName];
|
|
4032
|
+
return bin && typeof bin === "object" ? Object.keys(bin) : [];
|
|
4033
|
+
} catch {
|
|
4034
|
+
return [];
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
return [bareName];
|
|
4038
|
+
}
|
|
4039
|
+
function packagePatterns(forkPath, location, name) {
|
|
4040
|
+
const specifiers = [name];
|
|
4041
|
+
const typed = name.match(/^@types\/(.+)$/);
|
|
4042
|
+
if (typed) specifiers.push(typed[1].includes("__") ? `@${typed[1].replace("__", "/")}` : typed[1]);
|
|
4043
|
+
const patterns = specifiers.map((specifier) => new RegExp(`(['"\`])${escapeRegExp(specifier)}(?:/[^'"\`\\s]*)?\\1`));
|
|
4044
|
+
for (const bin of binNames(forkPath, location, name)) {
|
|
4045
|
+
patterns.push(new RegExp(`(?:^|[\\s'"\`(;&|=])${escapeRegExp(bin)}(?=$|[\\s'"\`);&|])`, "m"));
|
|
4046
|
+
}
|
|
4047
|
+
return patterns;
|
|
4048
|
+
}
|
|
4049
|
+
function scriptPattern(name) {
|
|
4050
|
+
return new RegExp(`\\b(?:pnpm|npm|yarn|bun)\\s+(?:[^\\n;&|]*?\\s)?${escapeRegExp(name)}(?=$|[\\s'"\`);&|])`, "m");
|
|
4051
|
+
}
|
|
4052
|
+
async function loadForkUsage(forkPath, upstreamRef, baseRef) {
|
|
4053
|
+
const diff = await git(["diff", "--name-only", "-z", "--diff-filter=d", upstreamRef], forkPath);
|
|
4054
|
+
const sources = [];
|
|
4055
|
+
for (const path4 of diff.split("\0").filter(Boolean)) {
|
|
4056
|
+
if (isPackageJson(path4)) sources.push(...await scriptSources(forkPath, upstreamRef, baseRef, path4));
|
|
4057
|
+
else if (!isManagedFile(path4)) sources.push(fileSource(forkPath, path4));
|
|
4058
|
+
}
|
|
4059
|
+
const findIn = (scoped, patterns) => scoped.find((source) => patterns.some((pattern) => pattern.test(source.text())))?.label;
|
|
4060
|
+
return {
|
|
4061
|
+
findPackage: (location, name) => findIn(
|
|
4062
|
+
location ? sources.filter((source) => isUnderAnyFolder(source.path, [location])) : sources,
|
|
4063
|
+
packagePatterns(forkPath, location, name)
|
|
4064
|
+
),
|
|
4065
|
+
findScript: (name) => findIn(sources, [scriptPattern(name)])
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// src/services/packages.ts
|
|
3773
4070
|
function parseComparableVersion(version) {
|
|
3774
4071
|
const trimmed = version.trim();
|
|
3775
4072
|
if (trimmed === "" || trimmed === "*" || trimmed.includes("workspace:") || trimmed.includes("catalog:") || trimmed.includes("file:") || trimmed.includes("link:") || trimmed.includes("git+") || trimmed.includes("github:") || trimmed.includes("http://") || trimmed.includes("https://") || trimmed.includes("||")) {
|
|
@@ -3797,24 +4094,49 @@ function isHigherVersion(upstreamVersion, forkVersion) {
|
|
|
3797
4094
|
if (!upCoerced || !forkCoerced) return false;
|
|
3798
4095
|
return compareVersions(upCoerced, forkCoerced) > 0;
|
|
3799
4096
|
}
|
|
3800
|
-
|
|
4097
|
+
var DEPENDENCY_KEYS = [
|
|
4098
|
+
"dependencies",
|
|
4099
|
+
"devDependencies",
|
|
4100
|
+
"peerDependencies",
|
|
4101
|
+
"optionalDependencies"
|
|
4102
|
+
];
|
|
4103
|
+
function mergeRecord(forkRecord, upstreamRecord, baseRecord, { bumpHigher = true, findUse } = {}) {
|
|
3801
4104
|
if (!upstreamRecord) return void 0;
|
|
3802
4105
|
const merged = { ...forkRecord || {} };
|
|
4106
|
+
const base = baseRecord || {};
|
|
3803
4107
|
const added = [];
|
|
3804
|
-
|
|
4108
|
+
const updated = [];
|
|
4109
|
+
const removed = [];
|
|
4110
|
+
const kept = [];
|
|
3805
4111
|
for (const [name, upstreamValue] of Object.entries(upstreamRecord)) {
|
|
3806
4112
|
const forkValue = merged[name];
|
|
3807
4113
|
if (forkValue === void 0) {
|
|
4114
|
+
if (name in base) continue;
|
|
3808
4115
|
merged[name] = upstreamValue;
|
|
3809
4116
|
added.push(name);
|
|
3810
|
-
|
|
3811
|
-
} else if (
|
|
4117
|
+
} else if (forkValue === upstreamValue) {
|
|
4118
|
+
} else if (base[name] === forkValue) {
|
|
3812
4119
|
merged[name] = upstreamValue;
|
|
3813
|
-
|
|
4120
|
+
updated.push(name);
|
|
4121
|
+
} else if (bumpHigher && isHigherVersion(upstreamValue, forkValue)) {
|
|
4122
|
+
merged[name] = upstreamValue;
|
|
4123
|
+
updated.push(name);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
for (const [name, forkValue] of Object.entries(merged)) {
|
|
4127
|
+
if (name in upstreamRecord) continue;
|
|
4128
|
+
if (base[name] === void 0 || base[name] !== forkValue) continue;
|
|
4129
|
+
const usedAt = findUse?.(name);
|
|
4130
|
+
if (usedAt) {
|
|
4131
|
+
kept.push({ name, usedAt });
|
|
4132
|
+
continue;
|
|
3814
4133
|
}
|
|
4134
|
+
delete merged[name];
|
|
4135
|
+
removed.push(name);
|
|
3815
4136
|
}
|
|
3816
4137
|
const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
|
|
3817
|
-
|
|
4138
|
+
const changed = added.length + updated.length + removed.length > 0;
|
|
4139
|
+
return { merged: sorted, changed, added, updated, removed, kept };
|
|
3818
4140
|
}
|
|
3819
4141
|
function isSubpathMap(value) {
|
|
3820
4142
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
@@ -3828,14 +4150,15 @@ function safeMergeExports(forkExports, upstreamExports) {
|
|
|
3828
4150
|
for (const name of added) merged[name] = upstreamExports[name];
|
|
3829
4151
|
return { merged, changed: added.length > 0, added };
|
|
3830
4152
|
}
|
|
3831
|
-
function safeMergePnpm(forkPnpm, upstreamPnpm) {
|
|
4153
|
+
function safeMergePnpm(forkPnpm, upstreamPnpm, basePnpm) {
|
|
3832
4154
|
if (!upstreamPnpm) return void 0;
|
|
3833
4155
|
const merged = { ...forkPnpm || {} };
|
|
3834
4156
|
let changed = false;
|
|
3835
4157
|
if (upstreamPnpm.overrides) {
|
|
3836
|
-
const result =
|
|
4158
|
+
const result = mergeRecord(
|
|
3837
4159
|
merged.overrides,
|
|
3838
|
-
upstreamPnpm.overrides
|
|
4160
|
+
upstreamPnpm.overrides,
|
|
4161
|
+
basePnpm?.overrides
|
|
3839
4162
|
);
|
|
3840
4163
|
if (result?.changed) {
|
|
3841
4164
|
merged.overrides = result.merged;
|
|
@@ -3843,18 +4166,15 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
|
|
|
3843
4166
|
}
|
|
3844
4167
|
}
|
|
3845
4168
|
if (upstreamPnpm.patchedDependencies) {
|
|
3846
|
-
const
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
merged.patchedDependencies = Object.fromEntries(
|
|
3856
|
-
Object.entries(forkPatched).sort(([a], [b]) => a.localeCompare(b))
|
|
3857
|
-
);
|
|
4169
|
+
const result = mergeRecord(
|
|
4170
|
+
merged.patchedDependencies,
|
|
4171
|
+
upstreamPnpm.patchedDependencies,
|
|
4172
|
+
basePnpm?.patchedDependencies,
|
|
4173
|
+
{ bumpHigher: false }
|
|
4174
|
+
);
|
|
4175
|
+
if (result?.changed) {
|
|
4176
|
+
merged.patchedDependencies = result.merged;
|
|
4177
|
+
changed = true;
|
|
3858
4178
|
}
|
|
3859
4179
|
}
|
|
3860
4180
|
if (upstreamPnpm.packageExtensions) {
|
|
@@ -3885,9 +4205,9 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
|
|
|
3885
4205
|
return { merged, changed };
|
|
3886
4206
|
}
|
|
3887
4207
|
function readPackageJson(filePath) {
|
|
3888
|
-
if (!
|
|
4208
|
+
if (!existsSync9(filePath)) return null;
|
|
3889
4209
|
try {
|
|
3890
|
-
return JSON.parse(
|
|
4210
|
+
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
3891
4211
|
} catch {
|
|
3892
4212
|
return null;
|
|
3893
4213
|
}
|
|
@@ -3896,31 +4216,50 @@ function writePackageJson(filePath, data) {
|
|
|
3896
4216
|
writeFileSync4(filePath, `${JSON.stringify(data, null, 2)}
|
|
3897
4217
|
`, "utf-8");
|
|
3898
4218
|
}
|
|
3899
|
-
|
|
3900
|
-
|
|
4219
|
+
function packageJsonPath(relativePath) {
|
|
4220
|
+
return relativePath ? `${relativePath}/package.json` : "package.json";
|
|
4221
|
+
}
|
|
4222
|
+
async function readPackageJsonAtRef(forkPath, ref, relativePath) {
|
|
3901
4223
|
try {
|
|
3902
|
-
|
|
4224
|
+
const raw = await git(["show", `${ref}:${packageJsonPath(relativePath)}`], forkPath);
|
|
4225
|
+
return { raw, data: JSON.parse(raw) };
|
|
3903
4226
|
} catch {
|
|
3904
4227
|
return null;
|
|
3905
4228
|
}
|
|
3906
4229
|
}
|
|
3907
4230
|
async function discoverPackageLocations(forkPath, upstreamRef) {
|
|
3908
4231
|
const stdout = await git(["ls-tree", "-r", "--name-only", upstreamRef], forkPath, { ignoreErrors: true });
|
|
3909
|
-
|
|
3910
|
-
return upstreamPaths.filter((loc) => existsSync8(join14(forkPath, loc, "package.json")));
|
|
4232
|
+
return stdout.split("\n").filter((p) => p.endsWith("/package.json") || p === "package.json").map((p) => p === "package.json" ? "" : p.replace("/package.json", ""));
|
|
3911
4233
|
}
|
|
3912
|
-
async function
|
|
4234
|
+
async function resolveMergeBase(forkPath, upstreamRef) {
|
|
4235
|
+
try {
|
|
4236
|
+
return (await getEffectiveMergeBase(forkPath, "HEAD", upstreamRef)).base || null;
|
|
4237
|
+
} catch {
|
|
4238
|
+
return null;
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync) {
|
|
4242
|
+
const { forkPath, upstreamRef } = config;
|
|
3913
4243
|
const changes = [];
|
|
3914
|
-
const
|
|
4244
|
+
const kept = [];
|
|
4245
|
+
const pkgRelPath = packageJsonPath(relativePath);
|
|
4246
|
+
const pkgPath = join15(forkPath, pkgRelPath);
|
|
3915
4247
|
const forkPkg = readPackageJson(pkgPath);
|
|
3916
|
-
const
|
|
3917
|
-
|
|
3918
|
-
|
|
4248
|
+
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4249
|
+
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4250
|
+
if (!upstream) return { updated: false, changes, kept };
|
|
4251
|
+
if (!forkPkg) {
|
|
4252
|
+
const workspaceArrived = relativePath !== "" && existsSync9(join15(forkPath, relativePath));
|
|
4253
|
+
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4254
|
+
writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4255
|
+
`, "utf-8");
|
|
4256
|
+
return { updated: true, changes: ["copied from upstream (new workspace)"], kept };
|
|
3919
4257
|
}
|
|
4258
|
+
const upstreamPkg = upstream.data;
|
|
3920
4259
|
let updated = false;
|
|
3921
4260
|
for (const key of keysToSync) {
|
|
3922
4261
|
if (key === "pnpm") {
|
|
3923
|
-
const result2 = safeMergePnpm(forkPkg.pnpm, upstreamPkg.pnpm);
|
|
4262
|
+
const result2 = safeMergePnpm(forkPkg.pnpm, upstreamPkg.pnpm, basePkg?.pnpm);
|
|
3924
4263
|
if (result2?.changed) {
|
|
3925
4264
|
forkPkg.pnpm = result2.merged;
|
|
3926
4265
|
updated = true;
|
|
@@ -3931,14 +4270,16 @@ async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync)
|
|
|
3931
4270
|
if (key === "packageManager") {
|
|
3932
4271
|
const upstreamValue2 = upstreamPkg[key];
|
|
3933
4272
|
const forkValue2 = forkPkg[key];
|
|
4273
|
+
const baseValue2 = basePkg?.[key];
|
|
3934
4274
|
if (upstreamValue2 && !forkValue2) {
|
|
4275
|
+
if (baseValue2) continue;
|
|
3935
4276
|
forkPkg[key] = upstreamValue2;
|
|
3936
4277
|
updated = true;
|
|
3937
4278
|
changes.push(`${key}: added`);
|
|
3938
|
-
} else if (upstreamValue2 && forkValue2 && upstreamValue2 !== forkValue2 && isHigherVersion(upstreamValue2, forkValue2)) {
|
|
4279
|
+
} else if (upstreamValue2 && forkValue2 && upstreamValue2 !== forkValue2 && (forkValue2 === baseValue2 || isHigherVersion(upstreamValue2, forkValue2))) {
|
|
3939
4280
|
forkPkg[key] = upstreamValue2;
|
|
3940
4281
|
updated = true;
|
|
3941
|
-
changes.push(`${key}:
|
|
4282
|
+
changes.push(`${key}: updated`);
|
|
3942
4283
|
}
|
|
3943
4284
|
continue;
|
|
3944
4285
|
}
|
|
@@ -3953,46 +4294,64 @@ async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync)
|
|
|
3953
4294
|
}
|
|
3954
4295
|
const forkValue = forkPkg[key];
|
|
3955
4296
|
const upstreamValue = upstreamPkg[key];
|
|
3956
|
-
const
|
|
3957
|
-
|
|
4297
|
+
const baseValue = basePkg?.[key];
|
|
4298
|
+
let findUse;
|
|
4299
|
+
if (usage && key === "scripts") findUse = (name) => usage.findScript(name);
|
|
4300
|
+
else if (usage && DEPENDENCY_KEYS.includes(key)) findUse = (name) => usage.findPackage(relativePath, name);
|
|
4301
|
+
const result = mergeRecord(forkValue, upstreamValue, baseValue, { bumpHigher: key !== "scripts", findUse });
|
|
4302
|
+
kept.push(...(result?.kept ?? []).map(({ name, usedAt }) => `${key}.${name}: used in ${usedAt}`));
|
|
3958
4303
|
if (result?.changed) {
|
|
3959
4304
|
forkPkg[key] = result.merged;
|
|
3960
4305
|
updated = true;
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
4306
|
+
changes.push(
|
|
4307
|
+
...result.added.map((name) => `${key}.${name}: added`),
|
|
4308
|
+
...result.updated.map((name) => `${key}.${name}: updated`),
|
|
4309
|
+
...result.removed.map((name) => `${key}.${name}: removed`)
|
|
4310
|
+
);
|
|
3966
4311
|
}
|
|
3967
4312
|
}
|
|
3968
4313
|
if (updated) {
|
|
3969
4314
|
writePackageJson(pkgPath, forkPkg);
|
|
3970
4315
|
}
|
|
3971
|
-
return { updated, changes };
|
|
4316
|
+
return { updated, changes, kept };
|
|
3972
4317
|
}
|
|
3973
4318
|
async function runPackages(config, options = {}) {
|
|
3974
4319
|
createSpinner("syncing package.json files...");
|
|
3975
4320
|
const keysToSync = config.settings.packageJsonSync || ["dependencies", "devDependencies"];
|
|
3976
4321
|
const conflictedSet = new Set(options.conflictedFiles ?? []);
|
|
4322
|
+
const baseRef = await resolveMergeBase(config.forkPath, config.upstreamRef);
|
|
4323
|
+
const usage = baseRef ? await loadForkUsage(config.forkPath, config.upstreamRef, baseRef) : null;
|
|
3977
4324
|
const locations = await discoverPackageLocations(config.forkPath, config.upstreamRef);
|
|
3978
|
-
|
|
4325
|
+
const reports = [];
|
|
3979
4326
|
const skipped = [];
|
|
3980
4327
|
for (const location of locations) {
|
|
3981
|
-
const pkgRelPath = location
|
|
4328
|
+
const pkgRelPath = packageJsonPath(location);
|
|
3982
4329
|
if (conflictedSet.has(pkgRelPath)) {
|
|
3983
4330
|
skipped.push(pkgRelPath);
|
|
3984
4331
|
continue;
|
|
3985
4332
|
}
|
|
3986
4333
|
spinnerText(`syncing ${location || "root"}/package.json...`);
|
|
3987
|
-
const { updated } = await syncPackageJson(config
|
|
3988
|
-
if (updated) {
|
|
3989
|
-
changedCount += 1;
|
|
3990
|
-
}
|
|
4334
|
+
const { updated, changes, kept: kept2 } = await syncPackageJson(config, baseRef, usage, location, keysToSync);
|
|
4335
|
+
if (updated || kept2.length > 0) reports.push({ path: pkgRelPath, changes, kept: kept2 });
|
|
3991
4336
|
}
|
|
4337
|
+
const changedCount = reports.filter(({ changes }) => changes.length > 0).length;
|
|
3992
4338
|
spinnerSuccess(
|
|
3993
4339
|
"package sync complete",
|
|
3994
4340
|
`${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
|
|
3995
4341
|
);
|
|
4342
|
+
for (const { path: path4, changes } of reports) {
|
|
4343
|
+
if (changes.length === 0) continue;
|
|
4344
|
+
console.info(` ${path4}`);
|
|
4345
|
+
for (const change of changes) console.info(` ${colors_default.dim(change)}`);
|
|
4346
|
+
}
|
|
4347
|
+
const kept = reports.flatMap(({ path: path4, kept: kept2 }) => kept2.map((entry) => `${path4} ${entry}`));
|
|
4348
|
+
if (kept.length > 0) {
|
|
4349
|
+
console.info();
|
|
4350
|
+
console.warn(
|
|
4351
|
+
`${warningMark} kept ${kept.length} ${kept.length === 1 ? "entry" : "entries"} upstream dropped, the fork still uses ${kept.length === 1 ? "it" : "them"}:`
|
|
4352
|
+
);
|
|
4353
|
+
for (const entry of kept) console.warn(` ${colors_default.dim("\u2192")} ${entry}`);
|
|
4354
|
+
}
|
|
3996
4355
|
if (skipped.length > 0) {
|
|
3997
4356
|
console.info();
|
|
3998
4357
|
console.warn(
|
|
@@ -4057,6 +4416,8 @@ async function runSync(config, options) {
|
|
|
4057
4416
|
spinnerFail("sync completed with conflicts");
|
|
4058
4417
|
}
|
|
4059
4418
|
printSummary(result.summary, "merge summary");
|
|
4419
|
+
printIgnoredUpstreamChanges(result);
|
|
4420
|
+
printUpstreamOverrideChanges(result);
|
|
4060
4421
|
printMaskingPinWarning(result.files);
|
|
4061
4422
|
if (config.logFile) {
|
|
4062
4423
|
const logPath = writeLogFile(config.forkPath, result.files);
|
|
@@ -4477,7 +4838,7 @@ async function runForks(config) {
|
|
|
4477
4838
|
|
|
4478
4839
|
// src/services/stats.ts
|
|
4479
4840
|
import { readFile as readFile2 } from "fs/promises";
|
|
4480
|
-
import { join as
|
|
4841
|
+
import { join as join17 } from "path";
|
|
4481
4842
|
|
|
4482
4843
|
// src/utils/coverage-utils.ts
|
|
4483
4844
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -4488,6 +4849,7 @@ function summaryPath(forkPath) {
|
|
|
4488
4849
|
return path3.join(forkPath, ".coverage", "coverage-summary.json");
|
|
4489
4850
|
}
|
|
4490
4851
|
function colorPct(pct) {
|
|
4852
|
+
if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
|
|
4491
4853
|
const label = `${pct.toFixed(2)}%`;
|
|
4492
4854
|
if (pct >= 80) return colors_default.green(label);
|
|
4493
4855
|
if (pct >= 50) return colors_default.yellow(label);
|
|
@@ -4550,6 +4912,11 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
4550
4912
|
if (stale) {
|
|
4551
4913
|
console.info(colors_default.yellow(" \u26A0 data is older than 7 days \u2014 run `pnpm test` to refresh"));
|
|
4552
4914
|
}
|
|
4915
|
+
if (!Object.keys(summary).some((key) => key !== "total")) {
|
|
4916
|
+
console.info(colors_default.yellow(" no coverage data found \u2014 run `pnpm test` to regenerate"));
|
|
4917
|
+
console.info();
|
|
4918
|
+
return;
|
|
4919
|
+
}
|
|
4553
4920
|
const total = summary.total;
|
|
4554
4921
|
console.info(
|
|
4555
4922
|
` ${colors_default.bold("overall")} lines ${colorPct(total.lines.pct)} ${colors_default.dim(`(${total.lines.covered}/${total.lines.total})`)} funcs ${colorPct(total.functions.pct)} branches ${colorPct(total.branches.pct)}`
|
|
@@ -4565,7 +4932,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
4565
4932
|
// src/services/stats.ts
|
|
4566
4933
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
4567
4934
|
async function getWorkspacePackages(forkPath) {
|
|
4568
|
-
const content = await readFile2(
|
|
4935
|
+
const content = await readFile2(join17(forkPath, "pnpm-workspace.yaml"), "utf-8");
|
|
4569
4936
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
4570
4937
|
if (pattern.endsWith("/*")) {
|
|
4571
4938
|
const base = pattern.slice(0, -2);
|
|
@@ -4667,9 +5034,9 @@ function fmtPct(value, total) {
|
|
|
4667
5034
|
function printStats(stats, verbose) {
|
|
4668
5035
|
console.info();
|
|
4669
5036
|
console.info(
|
|
4670
|
-
` ${colors_default.bold("
|
|
5037
|
+
` ${colors_default.bold("code files".padEnd(13))} ${String(stats.total)} ${colors_default.dim(`(${[...sourceExtensions].join(" ")})`)}`
|
|
4671
5038
|
);
|
|
4672
|
-
console.info(` ${colors_default.bold("lines of code")}
|
|
5039
|
+
console.info(` ${colors_default.bold("lines of code")} ${colors_default.cyan(formatLoc(stats.totalLoc))}`);
|
|
4673
5040
|
console.info();
|
|
4674
5041
|
const catEntries = [
|
|
4675
5042
|
["source & config", stats.categories.other],
|
|
@@ -4699,6 +5066,7 @@ function printStats(stats, verbose) {
|
|
|
4699
5066
|
);
|
|
4700
5067
|
}
|
|
4701
5068
|
} else {
|
|
5069
|
+
console.info(colors_default.dim(" source & config by package"));
|
|
4702
5070
|
for (const [name, data] of Object.entries(stats.packages)) {
|
|
4703
5071
|
const srcFiles = data.categories.other.files;
|
|
4704
5072
|
const srcLoc = data.categories.other.loc;
|
|
@@ -4728,7 +5096,7 @@ function getForkPath() {
|
|
|
4728
5096
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
4729
5097
|
if (envPath) {
|
|
4730
5098
|
const resolved = resolve6(envPath);
|
|
4731
|
-
if (!
|
|
5099
|
+
if (!existsSync11(resolved) || !statSync3(resolved).isDirectory()) {
|
|
4732
5100
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
4733
5101
|
}
|
|
4734
5102
|
return resolved;
|
|
@@ -4740,7 +5108,7 @@ function getForkPath() {
|
|
|
4740
5108
|
return cwd;
|
|
4741
5109
|
}
|
|
4742
5110
|
async function preflight(forkPath) {
|
|
4743
|
-
if (!
|
|
5111
|
+
if (!existsSync11(join18(forkPath, ".git"))) {
|
|
4744
5112
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
4745
5113
|
}
|
|
4746
5114
|
}
|