@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/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 existsSync10, statSync as statSync2 } from "fs";
5
- import { join as join17, resolve as resolve6 } from "path";
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.1",
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(files, scope) {
2554
+ function filterByScope(result, scope) {
2352
2555
  const statuses = scopeStatuses[scope];
2353
- const includeProtectedBehind = scope !== "risk";
2354
- return files.filter((f) => statuses.has(f.status) || includeProtectedBehind && f.upstreamChanged === true);
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.files, config.scope ?? "all");
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 packageJsonPath = path2.join(location, "package.json");
2897
- const relativePath = path2.relative(forkPath, packageJsonPath);
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://${packageJsonPath}`)} ${colors_default.dim(`${count}`)}`
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 existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
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
- function safeMergeRecord(forkRecord, upstreamRecord, addOnly = false) {
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
- let changed = false;
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
- changed = true;
3811
- } else if (!addOnly && forkValue !== upstreamValue && isHigherVersion(upstreamValue, forkValue)) {
4117
+ } else if (forkValue === upstreamValue) {
4118
+ } else if (base[name] === forkValue) {
3812
4119
  merged[name] = upstreamValue;
3813
- changed = true;
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
- return { merged: sorted, changed, added };
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 = safeMergeRecord(
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 forkPatched = merged.patchedDependencies || {};
3847
- const upstreamPatched = upstreamPnpm.patchedDependencies;
3848
- for (const [name, value] of Object.entries(upstreamPatched)) {
3849
- if (!(name in forkPatched)) {
3850
- forkPatched[name] = value;
3851
- changed = true;
3852
- }
3853
- }
3854
- if (changed || !merged.patchedDependencies) {
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 (!existsSync8(filePath)) return null;
4208
+ if (!existsSync9(filePath)) return null;
3889
4209
  try {
3890
- return JSON.parse(readFileSync5(filePath, "utf-8"));
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
- async function getUpstreamPackageJson(forkPath, upstreamRef, relativePath) {
3900
- const filePath = relativePath ? `${relativePath}/package.json` : "package.json";
4219
+ function packageJsonPath(relativePath) {
4220
+ return relativePath ? `${relativePath}/package.json` : "package.json";
4221
+ }
4222
+ async function readPackageJsonAtRef(forkPath, ref, relativePath) {
3901
4223
  try {
3902
- return JSON.parse(await git(["show", `${upstreamRef}:${filePath}`], forkPath));
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
- const upstreamPaths = stdout.split("\n").filter((p) => p.endsWith("/package.json") || p === "package.json").map((p) => p === "package.json" ? "" : p.replace("/package.json", ""));
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 syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync) {
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 pkgPath = join14(forkPath, relativePath, "package.json");
4244
+ const kept = [];
4245
+ const pkgRelPath = packageJsonPath(relativePath);
4246
+ const pkgPath = join15(forkPath, pkgRelPath);
3915
4247
  const forkPkg = readPackageJson(pkgPath);
3916
- const upstreamPkg = await getUpstreamPackageJson(forkPath, upstreamRef, relativePath);
3917
- if (!forkPkg || !upstreamPkg) {
3918
- return { updated: false, changes };
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}: bumped`);
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 addOnly = key === "scripts";
3957
- const result = safeMergeRecord(forkValue, upstreamValue, addOnly);
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
- if (addOnly) {
3962
- changes.push(...result.added.map((name) => `scripts.${name}: added`));
3963
- } else {
3964
- changes.push(`${key}: merged`);
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
- let changedCount = 0;
4325
+ const reports = [];
3979
4326
  const skipped = [];
3980
4327
  for (const location of locations) {
3981
- const pkgRelPath = location ? `${location}/package.json` : "package.json";
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.forkPath, config.upstreamRef, location, keysToSync);
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 join16 } from "path";
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(join16(forkPath, "pnpm-workspace.yaml"), "utf-8");
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("source files")} ${String(stats.total)} ${colors_default.dim(`(${stats.skipped} non-source skipped)`)}`
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")} ${colors_default.cyan(formatLoc(stats.totalLoc))}`);
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 (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
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 (!existsSync10(join17(forkPath, ".git"))) {
5111
+ if (!existsSync11(join18(forkPath, ".git"))) {
4744
5112
  throw new Error(`not a git repository: ${forkPath}`);
4745
5113
  }
4746
5114
  }