@cellajs/cli 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,10 +179,18 @@ packageJsonSync: ['dependencies', 'devDependencies', 'scripts']
179
179
 
180
180
  **Supported keys:** `dependencies`, `devDependencies`, `peerDependencies`, `optionalDependencies`, `scripts`, `engines`, `packageManager`, `overrides`, `exports`, `pnpm`
181
181
 
182
- It **adds** new keys and **updates** existing ones to match upstream, but never **removes** keys
183
- that only exist in your fork — so extra dependencies and scripts survive each sync. `scripts` and
184
- `exports` are add-only: an entry your fork already defines is never rewritten. `exports` only
185
- merges when both sides are subpath maps (`{ ".": …, "./config": … }`).
182
+ Each section merges three-way against the merge-base package.json (upstream as of your last
183
+ sync). An entry your fork never touched follows upstream: it is **added**, **updated** (a rewritten
184
+ script, a changed range) or **removed** when upstream dropped it. An entry your fork added or
185
+ changed stays as it is; only a strictly higher upstream version still bumps it, and never for
186
+ `scripts`. An entry your fork removed is not re-added. A dependency or script upstream dropped
187
+ stays while your own code still uses it: files that differ from upstream and scripts your fork
188
+ added or changed are searched for an import of the package, a run of its CLI, or a package manager
189
+ command that runs the script. The sync lists every change and warns about each entry it kept this
190
+ way, so you can remove it once your code no longer needs it. `exports` are add-only: an entry your fork
191
+ already defines is never rewritten, and `exports` only merges when both sides are subpath maps
192
+ (`{ ".": …, "./config": … }`). A workspace upstream added since your last sync arrives with its
193
+ package.json copied verbatim.
186
194
 
187
195
  ## Contributions (pull from forks)
188
196
 
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
@@ -216,7 +216,7 @@ import process4 from "process";
216
216
  // package.json
217
217
  var package_default = {
218
218
  name: "@cellajs/cli",
219
- version: "0.2.1",
219
+ version: "0.2.2",
220
220
  private: false,
221
221
  license: "MIT",
222
222
  description: "CLI for managing your monorepo and syncing with upstream Cella.",
@@ -707,7 +707,7 @@ function printProtectedConflicts(result) {
707
707
  }
708
708
  function findMaskingPins(files) {
709
709
  return files.filter(
710
- (file) => file.isPinned && file.status === "behind" && file.existsInFork && file.existsInUpstream
710
+ (file) => file.isPinned && !isManagedFile(file.path) && file.status === "behind" && file.existsInFork && file.existsInUpstream
711
711
  );
712
712
  }
713
713
  function printMaskingPinWarning(files) {
@@ -2893,11 +2893,11 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
2893
2893
  const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
2894
2894
  const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
2895
2895
  for (const [dependentName, { location, count }] of sortedDependents) {
2896
- const packageJsonPath = path2.join(location, "package.json");
2897
- const relativePath = path2.relative(forkPath, packageJsonPath);
2896
+ const packageJsonPath2 = path2.join(location, "package.json");
2897
+ const relativePath = path2.relative(forkPath, packageJsonPath2);
2898
2898
  const paddedName = dependentName.padEnd(maxDependentNameLen);
2899
2899
  console.info(
2900
- ` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath}`)} ${colors_default.dim(`${count}`)}`
2900
+ ` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count}`)}`
2901
2901
  );
2902
2902
  }
2903
2903
  }
@@ -3768,8 +3768,93 @@ function closePr(cwd, ref) {
3768
3768
  }
3769
3769
 
3770
3770
  // src/services/packages.ts
3771
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
3771
+ import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
3772
+ import { join as join15 } from "path";
3773
+
3774
+ // src/services/package-usage.ts
3775
+ import { existsSync as existsSync8, readFileSync as readFileSync5, statSync } from "fs";
3772
3776
  import { join as join14 } from "path";
3777
+ var MAX_FILE_BYTES = 1024 * 1024;
3778
+ function escapeRegExp(value) {
3779
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3780
+ }
3781
+ function readSearchable(filePath) {
3782
+ try {
3783
+ if (statSync(filePath).size > MAX_FILE_BYTES) return "";
3784
+ const text = readFileSync5(filePath, "utf-8");
3785
+ return text.includes("\0") ? "" : text;
3786
+ } catch {
3787
+ return "";
3788
+ }
3789
+ }
3790
+ function fileSource(forkPath, path4) {
3791
+ let text;
3792
+ return { path: path4, label: path4, text: () => text ??= readSearchable(join14(forkPath, path4)) };
3793
+ }
3794
+ async function readScriptsAtRef(forkPath, ref, path4) {
3795
+ try {
3796
+ return JSON.parse(await git(["show", `${ref}:${path4}`], forkPath)).scripts ?? {};
3797
+ } catch {
3798
+ return {};
3799
+ }
3800
+ }
3801
+ async function scriptSources(forkPath, upstreamRef, baseRef, path4) {
3802
+ let forkScripts;
3803
+ try {
3804
+ forkScripts = JSON.parse(readFileSync5(join14(forkPath, path4), "utf-8")).scripts ?? {};
3805
+ } catch {
3806
+ return [];
3807
+ }
3808
+ const baseScripts = await readScriptsAtRef(forkPath, baseRef, path4);
3809
+ const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef, path4);
3810
+ return Object.entries(forkScripts).filter(([name, value]) => value !== baseScripts[name] && value !== upstreamScripts[name]).map(([name, value]) => ({ path: path4, label: `${path4} (scripts.${name})`, text: () => value }));
3811
+ }
3812
+ function binNames(forkPath, location, name) {
3813
+ const bareName = name.split("/").pop();
3814
+ for (const dir of [join14(forkPath, location), forkPath]) {
3815
+ const manifest = join14(dir, "node_modules", name, "package.json");
3816
+ if (!existsSync8(manifest)) continue;
3817
+ try {
3818
+ const { bin } = JSON.parse(readFileSync5(manifest, "utf-8"));
3819
+ if (typeof bin === "string") return [bareName];
3820
+ return bin && typeof bin === "object" ? Object.keys(bin) : [];
3821
+ } catch {
3822
+ return [];
3823
+ }
3824
+ }
3825
+ return [bareName];
3826
+ }
3827
+ function packagePatterns(forkPath, location, name) {
3828
+ const specifiers = [name];
3829
+ const typed = name.match(/^@types\/(.+)$/);
3830
+ if (typed) specifiers.push(typed[1].includes("__") ? `@${typed[1].replace("__", "/")}` : typed[1]);
3831
+ const patterns = specifiers.map((specifier) => new RegExp(`(['"\`])${escapeRegExp(specifier)}(?:/[^'"\`\\s]*)?\\1`));
3832
+ for (const bin of binNames(forkPath, location, name)) {
3833
+ patterns.push(new RegExp(`(?:^|[\\s'"\`(;&|=])${escapeRegExp(bin)}(?=$|[\\s'"\`);&|])`, "m"));
3834
+ }
3835
+ return patterns;
3836
+ }
3837
+ function scriptPattern(name) {
3838
+ return new RegExp(`\\b(?:pnpm|npm|yarn|bun)\\s+(?:[^\\n;&|]*?\\s)?${escapeRegExp(name)}(?=$|[\\s'"\`);&|])`, "m");
3839
+ }
3840
+ async function loadForkUsage(forkPath, upstreamRef, baseRef) {
3841
+ const diff = await git(["diff", "--name-only", "-z", "--diff-filter=d", upstreamRef], forkPath);
3842
+ const sources = [];
3843
+ for (const path4 of diff.split("\0").filter(Boolean)) {
3844
+ if (isPackageJson(path4)) sources.push(...await scriptSources(forkPath, upstreamRef, baseRef, path4));
3845
+ else if (!isManagedFile(path4)) sources.push(fileSource(forkPath, path4));
3846
+ }
3847
+ const findIn = (scoped, patterns) => scoped.find((source) => patterns.some((pattern) => pattern.test(source.text())))?.label;
3848
+ return {
3849
+ findPackage: (location, name) => findIn(
3850
+ location ? sources.filter((source) => isUnderAnyFolder(source.path, [location])) : sources,
3851
+ packagePatterns(forkPath, location, name)
3852
+ ),
3853
+ findScript: (name) => findIn(sources, [scriptPattern(name)])
3854
+ };
3855
+ }
3856
+
3857
+ // src/services/packages.ts
3773
3858
  function parseComparableVersion(version) {
3774
3859
  const trimmed = version.trim();
3775
3860
  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 +3882,49 @@ function isHigherVersion(upstreamVersion, forkVersion) {
3797
3882
  if (!upCoerced || !forkCoerced) return false;
3798
3883
  return compareVersions(upCoerced, forkCoerced) > 0;
3799
3884
  }
3800
- function safeMergeRecord(forkRecord, upstreamRecord, addOnly = false) {
3885
+ var DEPENDENCY_KEYS = [
3886
+ "dependencies",
3887
+ "devDependencies",
3888
+ "peerDependencies",
3889
+ "optionalDependencies"
3890
+ ];
3891
+ function mergeRecord(forkRecord, upstreamRecord, baseRecord, { bumpHigher = true, findUse } = {}) {
3801
3892
  if (!upstreamRecord) return void 0;
3802
3893
  const merged = { ...forkRecord || {} };
3894
+ const base = baseRecord || {};
3803
3895
  const added = [];
3804
- let changed = false;
3896
+ const updated = [];
3897
+ const removed = [];
3898
+ const kept = [];
3805
3899
  for (const [name, upstreamValue] of Object.entries(upstreamRecord)) {
3806
3900
  const forkValue = merged[name];
3807
3901
  if (forkValue === void 0) {
3902
+ if (name in base) continue;
3808
3903
  merged[name] = upstreamValue;
3809
3904
  added.push(name);
3810
- changed = true;
3811
- } else if (!addOnly && forkValue !== upstreamValue && isHigherVersion(upstreamValue, forkValue)) {
3905
+ } else if (forkValue === upstreamValue) {
3906
+ } else if (base[name] === forkValue) {
3812
3907
  merged[name] = upstreamValue;
3813
- changed = true;
3908
+ updated.push(name);
3909
+ } else if (bumpHigher && isHigherVersion(upstreamValue, forkValue)) {
3910
+ merged[name] = upstreamValue;
3911
+ updated.push(name);
3912
+ }
3913
+ }
3914
+ for (const [name, forkValue] of Object.entries(merged)) {
3915
+ if (name in upstreamRecord) continue;
3916
+ if (base[name] === void 0 || base[name] !== forkValue) continue;
3917
+ const usedAt = findUse?.(name);
3918
+ if (usedAt) {
3919
+ kept.push({ name, usedAt });
3920
+ continue;
3814
3921
  }
3922
+ delete merged[name];
3923
+ removed.push(name);
3815
3924
  }
3816
3925
  const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
3817
- return { merged: sorted, changed, added };
3926
+ const changed = added.length + updated.length + removed.length > 0;
3927
+ return { merged: sorted, changed, added, updated, removed, kept };
3818
3928
  }
3819
3929
  function isSubpathMap(value) {
3820
3930
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
@@ -3828,14 +3938,15 @@ function safeMergeExports(forkExports, upstreamExports) {
3828
3938
  for (const name of added) merged[name] = upstreamExports[name];
3829
3939
  return { merged, changed: added.length > 0, added };
3830
3940
  }
3831
- function safeMergePnpm(forkPnpm, upstreamPnpm) {
3941
+ function safeMergePnpm(forkPnpm, upstreamPnpm, basePnpm) {
3832
3942
  if (!upstreamPnpm) return void 0;
3833
3943
  const merged = { ...forkPnpm || {} };
3834
3944
  let changed = false;
3835
3945
  if (upstreamPnpm.overrides) {
3836
- const result = safeMergeRecord(
3946
+ const result = mergeRecord(
3837
3947
  merged.overrides,
3838
- upstreamPnpm.overrides
3948
+ upstreamPnpm.overrides,
3949
+ basePnpm?.overrides
3839
3950
  );
3840
3951
  if (result?.changed) {
3841
3952
  merged.overrides = result.merged;
@@ -3843,18 +3954,15 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
3843
3954
  }
3844
3955
  }
3845
3956
  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
- );
3957
+ const result = mergeRecord(
3958
+ merged.patchedDependencies,
3959
+ upstreamPnpm.patchedDependencies,
3960
+ basePnpm?.patchedDependencies,
3961
+ { bumpHigher: false }
3962
+ );
3963
+ if (result?.changed) {
3964
+ merged.patchedDependencies = result.merged;
3965
+ changed = true;
3858
3966
  }
3859
3967
  }
3860
3968
  if (upstreamPnpm.packageExtensions) {
@@ -3885,9 +3993,9 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
3885
3993
  return { merged, changed };
3886
3994
  }
3887
3995
  function readPackageJson(filePath) {
3888
- if (!existsSync8(filePath)) return null;
3996
+ if (!existsSync9(filePath)) return null;
3889
3997
  try {
3890
- return JSON.parse(readFileSync5(filePath, "utf-8"));
3998
+ return JSON.parse(readFileSync6(filePath, "utf-8"));
3891
3999
  } catch {
3892
4000
  return null;
3893
4001
  }
@@ -3896,31 +4004,50 @@ function writePackageJson(filePath, data) {
3896
4004
  writeFileSync4(filePath, `${JSON.stringify(data, null, 2)}
3897
4005
  `, "utf-8");
3898
4006
  }
3899
- async function getUpstreamPackageJson(forkPath, upstreamRef, relativePath) {
3900
- const filePath = relativePath ? `${relativePath}/package.json` : "package.json";
4007
+ function packageJsonPath(relativePath) {
4008
+ return relativePath ? `${relativePath}/package.json` : "package.json";
4009
+ }
4010
+ async function readPackageJsonAtRef(forkPath, ref, relativePath) {
3901
4011
  try {
3902
- return JSON.parse(await git(["show", `${upstreamRef}:${filePath}`], forkPath));
4012
+ const raw = await git(["show", `${ref}:${packageJsonPath(relativePath)}`], forkPath);
4013
+ return { raw, data: JSON.parse(raw) };
3903
4014
  } catch {
3904
4015
  return null;
3905
4016
  }
3906
4017
  }
3907
4018
  async function discoverPackageLocations(forkPath, upstreamRef) {
3908
4019
  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")));
4020
+ return stdout.split("\n").filter((p) => p.endsWith("/package.json") || p === "package.json").map((p) => p === "package.json" ? "" : p.replace("/package.json", ""));
4021
+ }
4022
+ async function resolveMergeBase(forkPath, upstreamRef) {
4023
+ try {
4024
+ return (await getEffectiveMergeBase(forkPath, "HEAD", upstreamRef)).base || null;
4025
+ } catch {
4026
+ return null;
4027
+ }
3911
4028
  }
3912
- async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync) {
4029
+ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync) {
4030
+ const { forkPath, upstreamRef } = config;
3913
4031
  const changes = [];
3914
- const pkgPath = join14(forkPath, relativePath, "package.json");
4032
+ const kept = [];
4033
+ const pkgRelPath = packageJsonPath(relativePath);
4034
+ const pkgPath = join15(forkPath, pkgRelPath);
3915
4035
  const forkPkg = readPackageJson(pkgPath);
3916
- const upstreamPkg = await getUpstreamPackageJson(forkPath, upstreamRef, relativePath);
3917
- if (!forkPkg || !upstreamPkg) {
3918
- return { updated: false, changes };
4036
+ const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
4037
+ const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
4038
+ if (!upstream) return { updated: false, changes, kept };
4039
+ if (!forkPkg) {
4040
+ const workspaceArrived = relativePath !== "" && existsSync9(join15(forkPath, relativePath));
4041
+ if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
4042
+ writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
4043
+ `, "utf-8");
4044
+ return { updated: true, changes: ["copied from upstream (new workspace)"], kept };
3919
4045
  }
4046
+ const upstreamPkg = upstream.data;
3920
4047
  let updated = false;
3921
4048
  for (const key of keysToSync) {
3922
4049
  if (key === "pnpm") {
3923
- const result2 = safeMergePnpm(forkPkg.pnpm, upstreamPkg.pnpm);
4050
+ const result2 = safeMergePnpm(forkPkg.pnpm, upstreamPkg.pnpm, basePkg?.pnpm);
3924
4051
  if (result2?.changed) {
3925
4052
  forkPkg.pnpm = result2.merged;
3926
4053
  updated = true;
@@ -3931,14 +4058,16 @@ async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync)
3931
4058
  if (key === "packageManager") {
3932
4059
  const upstreamValue2 = upstreamPkg[key];
3933
4060
  const forkValue2 = forkPkg[key];
4061
+ const baseValue2 = basePkg?.[key];
3934
4062
  if (upstreamValue2 && !forkValue2) {
4063
+ if (baseValue2) continue;
3935
4064
  forkPkg[key] = upstreamValue2;
3936
4065
  updated = true;
3937
4066
  changes.push(`${key}: added`);
3938
- } else if (upstreamValue2 && forkValue2 && upstreamValue2 !== forkValue2 && isHigherVersion(upstreamValue2, forkValue2)) {
4067
+ } else if (upstreamValue2 && forkValue2 && upstreamValue2 !== forkValue2 && (forkValue2 === baseValue2 || isHigherVersion(upstreamValue2, forkValue2))) {
3939
4068
  forkPkg[key] = upstreamValue2;
3940
4069
  updated = true;
3941
- changes.push(`${key}: bumped`);
4070
+ changes.push(`${key}: updated`);
3942
4071
  }
3943
4072
  continue;
3944
4073
  }
@@ -3953,46 +4082,64 @@ async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync)
3953
4082
  }
3954
4083
  const forkValue = forkPkg[key];
3955
4084
  const upstreamValue = upstreamPkg[key];
3956
- const addOnly = key === "scripts";
3957
- const result = safeMergeRecord(forkValue, upstreamValue, addOnly);
4085
+ const baseValue = basePkg?.[key];
4086
+ let findUse;
4087
+ if (usage && key === "scripts") findUse = (name) => usage.findScript(name);
4088
+ else if (usage && DEPENDENCY_KEYS.includes(key)) findUse = (name) => usage.findPackage(relativePath, name);
4089
+ const result = mergeRecord(forkValue, upstreamValue, baseValue, { bumpHigher: key !== "scripts", findUse });
4090
+ kept.push(...(result?.kept ?? []).map(({ name, usedAt }) => `${key}.${name}: used in ${usedAt}`));
3958
4091
  if (result?.changed) {
3959
4092
  forkPkg[key] = result.merged;
3960
4093
  updated = true;
3961
- if (addOnly) {
3962
- changes.push(...result.added.map((name) => `scripts.${name}: added`));
3963
- } else {
3964
- changes.push(`${key}: merged`);
3965
- }
4094
+ changes.push(
4095
+ ...result.added.map((name) => `${key}.${name}: added`),
4096
+ ...result.updated.map((name) => `${key}.${name}: updated`),
4097
+ ...result.removed.map((name) => `${key}.${name}: removed`)
4098
+ );
3966
4099
  }
3967
4100
  }
3968
4101
  if (updated) {
3969
4102
  writePackageJson(pkgPath, forkPkg);
3970
4103
  }
3971
- return { updated, changes };
4104
+ return { updated, changes, kept };
3972
4105
  }
3973
4106
  async function runPackages(config, options = {}) {
3974
4107
  createSpinner("syncing package.json files...");
3975
4108
  const keysToSync = config.settings.packageJsonSync || ["dependencies", "devDependencies"];
3976
4109
  const conflictedSet = new Set(options.conflictedFiles ?? []);
4110
+ const baseRef = await resolveMergeBase(config.forkPath, config.upstreamRef);
4111
+ const usage = baseRef ? await loadForkUsage(config.forkPath, config.upstreamRef, baseRef) : null;
3977
4112
  const locations = await discoverPackageLocations(config.forkPath, config.upstreamRef);
3978
- let changedCount = 0;
4113
+ const reports = [];
3979
4114
  const skipped = [];
3980
4115
  for (const location of locations) {
3981
- const pkgRelPath = location ? `${location}/package.json` : "package.json";
4116
+ const pkgRelPath = packageJsonPath(location);
3982
4117
  if (conflictedSet.has(pkgRelPath)) {
3983
4118
  skipped.push(pkgRelPath);
3984
4119
  continue;
3985
4120
  }
3986
4121
  spinnerText(`syncing ${location || "root"}/package.json...`);
3987
- const { updated } = await syncPackageJson(config.forkPath, config.upstreamRef, location, keysToSync);
3988
- if (updated) {
3989
- changedCount += 1;
3990
- }
4122
+ const { updated, changes, kept: kept2 } = await syncPackageJson(config, baseRef, usage, location, keysToSync);
4123
+ if (updated || kept2.length > 0) reports.push({ path: pkgRelPath, changes, kept: kept2 });
3991
4124
  }
4125
+ const changedCount = reports.filter(({ changes }) => changes.length > 0).length;
3992
4126
  spinnerSuccess(
3993
4127
  "package sync complete",
3994
4128
  `${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
3995
4129
  );
4130
+ for (const { path: path4, changes } of reports) {
4131
+ if (changes.length === 0) continue;
4132
+ console.info(` ${path4}`);
4133
+ for (const change of changes) console.info(` ${colors_default.dim(change)}`);
4134
+ }
4135
+ const kept = reports.flatMap(({ path: path4, kept: kept2 }) => kept2.map((entry) => `${path4} ${entry}`));
4136
+ if (kept.length > 0) {
4137
+ console.info();
4138
+ console.warn(
4139
+ `${warningMark} kept ${kept.length} ${kept.length === 1 ? "entry" : "entries"} upstream dropped, the fork still uses ${kept.length === 1 ? "it" : "them"}:`
4140
+ );
4141
+ for (const entry of kept) console.warn(` ${colors_default.dim("\u2192")} ${entry}`);
4142
+ }
3996
4143
  if (skipped.length > 0) {
3997
4144
  console.info();
3998
4145
  console.warn(
@@ -4477,7 +4624,7 @@ async function runForks(config) {
4477
4624
 
4478
4625
  // src/services/stats.ts
4479
4626
  import { readFile as readFile2 } from "fs/promises";
4480
- import { join as join16 } from "path";
4627
+ import { join as join17 } from "path";
4481
4628
 
4482
4629
  // src/utils/coverage-utils.ts
4483
4630
  import { spawnSync as spawnSync4 } from "child_process";
@@ -4488,6 +4635,7 @@ function summaryPath(forkPath) {
4488
4635
  return path3.join(forkPath, ".coverage", "coverage-summary.json");
4489
4636
  }
4490
4637
  function colorPct(pct) {
4638
+ if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
4491
4639
  const label = `${pct.toFixed(2)}%`;
4492
4640
  if (pct >= 80) return colors_default.green(label);
4493
4641
  if (pct >= 50) return colors_default.yellow(label);
@@ -4550,6 +4698,11 @@ function printCoverageSummary(forkPath, options = {}) {
4550
4698
  if (stale) {
4551
4699
  console.info(colors_default.yellow(" \u26A0 data is older than 7 days \u2014 run `pnpm test` to refresh"));
4552
4700
  }
4701
+ if (!Object.keys(summary).some((key) => key !== "total")) {
4702
+ console.info(colors_default.yellow(" no coverage data found \u2014 run `pnpm test` to regenerate"));
4703
+ console.info();
4704
+ return;
4705
+ }
4553
4706
  const total = summary.total;
4554
4707
  console.info(
4555
4708
  ` ${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 +4718,7 @@ function printCoverageSummary(forkPath, options = {}) {
4565
4718
  // src/services/stats.ts
4566
4719
  var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
4567
4720
  async function getWorkspacePackages(forkPath) {
4568
- const content = await readFile2(join16(forkPath, "pnpm-workspace.yaml"), "utf-8");
4721
+ const content = await readFile2(join17(forkPath, "pnpm-workspace.yaml"), "utf-8");
4569
4722
  return parseYamlBlockList(content, "packages").map((pattern) => {
4570
4723
  if (pattern.endsWith("/*")) {
4571
4724
  const base = pattern.slice(0, -2);
@@ -4728,7 +4881,7 @@ function getForkPath() {
4728
4881
  const envPath = getEnv("CELLA_FORK_PATH");
4729
4882
  if (envPath) {
4730
4883
  const resolved = resolve6(envPath);
4731
- if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
4884
+ if (!existsSync11(resolved) || !statSync3(resolved).isDirectory()) {
4732
4885
  throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
4733
4886
  }
4734
4887
  return resolved;
@@ -4740,7 +4893,7 @@ function getForkPath() {
4740
4893
  return cwd;
4741
4894
  }
4742
4895
  async function preflight(forkPath) {
4743
- if (!existsSync10(join17(forkPath, ".git"))) {
4896
+ if (!existsSync11(join18(forkPath, ".git"))) {
4744
4897
  throw new Error(`not a git repository: ${forkPath}`);
4745
4898
  }
4746
4899
  }