@cellajs/cli 0.1.1 → 0.2.1

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