@cellajs/cli 0.3.1 → 0.3.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
@@ -2,12 +2,10 @@
2
2
 
3
3
  // src/cella-cli.ts
4
4
  import { existsSync as existsSync13, statSync as statSync3 } from "fs";
5
- import { join as join21, resolve as resolve8 } from "path";
5
+ import { join as join19, resolve as resolve8 } from "path";
6
6
  import process9 from "process";
7
7
 
8
8
  // src/cli.ts
9
- import { readFileSync } from "fs";
10
- import { join as join8 } from "path";
11
9
  import process5 from "process";
12
10
  import { select } from "@inquirer/prompts";
13
11
  import { Command } from "commander";
@@ -53,32 +51,31 @@ var pc = {
53
51
  var colors_default = pc;
54
52
 
55
53
  // src/utils/config.ts
56
- import { existsSync as existsSync2, realpathSync } from "fs";
54
+ import { existsSync as existsSync2, readFileSync, realpathSync } from "fs";
57
55
  import { join as join2, resolve } from "path";
58
56
  import { z as z2 } from "zod";
59
57
 
60
58
  // src/config/types.ts
61
59
  import { z } from "zod";
60
+ var PACKAGE_JSON_SYNC_KEYS = [
61
+ "dependencies",
62
+ "devDependencies",
63
+ "peerDependencies",
64
+ "optionalDependencies",
65
+ "scripts",
66
+ "engines",
67
+ "packageManager",
68
+ "overrides",
69
+ "exports",
70
+ "pnpm"
71
+ ];
62
72
  var cellaConfigSchema = z.object({
63
73
  settings: z.object({
64
74
  upstreamUrl: z.string().min(1),
65
75
  upstreamBranch: z.string().min(1).optional(),
66
76
  upstreamTrack: z.enum(["release", "branch"]).optional(),
67
77
  releaseBase: z.string().min(1).optional(),
68
- packageJsonSync: z.array(
69
- z.enum([
70
- "dependencies",
71
- "devDependencies",
72
- "peerDependencies",
73
- "optionalDependencies",
74
- "scripts",
75
- "engines",
76
- "packageManager",
77
- "overrides",
78
- "exports",
79
- "pnpm"
80
- ])
81
- ).optional(),
78
+ packageJsonSync: z.array(z.enum(PACKAGE_JSON_SYNC_KEYS)).optional(),
82
79
  syncWithPackages: z.boolean().optional(),
83
80
  fileLinkMode: z.enum(["commit", "file", "local"]).optional()
84
81
  }).strict(),
@@ -95,6 +92,12 @@ var cellaConfigSchema = z.object({
95
92
  }).strict()
96
93
  ).optional()
97
94
  }).strict();
95
+ var SYNC_APPLIED_STATUSES = ["behind", "diverged", "renamed", "ignored", "pinned"];
96
+
97
+ // src/utils/errors.ts
98
+ function errorMessage(error) {
99
+ return error instanceof Error ? error.message : String(error);
100
+ }
98
101
 
99
102
  // src/utils/managed-files.ts
100
103
  var CONFIG_FILE = "cella/cella.config.ts";
@@ -160,6 +163,10 @@ var DEFAULT_UPSTREAM_REMOTE = "cella-upstream";
160
163
  var DEFAULT_BRANCH = "main";
161
164
  var DEFAULT_SYNC_PREFIX = "cella/sync";
162
165
  var DEFAULT_RELEASE_BASE = "main";
166
+ var DEFAULT_PACKAGE_JSON_SYNC = ["dependencies", "devDependencies"];
167
+ function isUpstreamRepo(repoPath) {
168
+ return JSON.parse(readFileSync(join2(repoPath, "package.json"), "utf8")).name === "cella";
169
+ }
163
170
  function isTemporarySyncBranch(branch) {
164
171
  return branch.startsWith(`${DEFAULT_SYNC_PREFIX}/`);
165
172
  }
@@ -192,8 +199,7 @@ async function loadConfig(forkPath) {
192
199
  try {
193
200
  configModule = await import(realConfigPath);
194
201
  } catch (error) {
195
- const message = error instanceof Error ? error.message : String(error);
196
- throw new Error(`failed to load ${configPath}: ${message}`);
202
+ throw new Error(`failed to load ${configPath}: ${errorMessage(error)}`);
197
203
  }
198
204
  const result = cellaConfigSchema.safeParse(configModule.default);
199
205
  if (!result.success) {
@@ -220,7 +226,7 @@ import process4 from "process";
220
226
  // package.json
221
227
  var package_default = {
222
228
  name: "@cellajs/cli",
223
- version: "0.3.1",
229
+ version: "0.3.3",
224
230
  private: false,
225
231
  license: "MIT",
226
232
  description: "CLI for managing your monorepo and syncing with upstream Cella.",
@@ -346,6 +352,8 @@ var package_default = {
346
352
  var NAME = "cella cli";
347
353
  var VERSION = package_default.version;
348
354
  var DIVIDER = "\u2500".repeat(60);
355
+ var MENU_DIVIDER = "\u2500".repeat(40);
356
+ var COMMIT_LIST_MAX = 50;
349
357
  var warningMark = colors_default.yellow("\u26A0");
350
358
  var checkMark = colors_default.green("\u2713");
351
359
  var TerminalSpinner = class {
@@ -454,7 +462,7 @@ function getVsCodeOpenFileLink(filePath, options) {
454
462
  const url = `vscode://file${absolutePath}`;
455
463
  return hyperlink(filePath, url);
456
464
  }
457
- function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, maxShownCommits = 50) {
465
+ function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, maxShownCommits = COMMIT_LIST_MAX) {
458
466
  const commitLabel = commitCount === 1 ? "1 new commit" : `${commitCount} new commits`;
459
467
  const lines = [`${commitLabel} since last merge`];
460
468
  const shown = commits.slice(0, maxShownCommits);
@@ -538,19 +546,7 @@ var summaryStatusConfig = {
538
546
  var statusOrder = Object.keys(statusConfig);
539
547
  function printSummary(summary, title = "summary") {
540
548
  printSectionHeader(colors_default.cyan(title));
541
- const maxCount = Math.max(
542
- summary.managed,
543
- summary.ignored,
544
- summary.identical,
545
- summary.ahead,
546
- summary.local,
547
- summary.drifted,
548
- summary.behind,
549
- summary.diverged,
550
- summary.pinned,
551
- summary.deleted,
552
- summary.renamed
553
- );
549
+ const maxCount = Math.max(...statusOrder.map((status) => summary[status]), summary.managed);
554
550
  const countWidth = String(maxCount).length + 1;
555
551
  const printLine = (status, count) => {
556
552
  const { icon, label, color, description } = summaryStatusConfig[status];
@@ -613,11 +609,23 @@ function formatUpstreamChangedLines(file) {
613
609
  const n = file.upstreamChangedLines;
614
610
  return colors_default.yellow(` \xB7 ${n} ${n === 1 ? "line" : "lines"} changed upstream`);
615
611
  }
612
+ function protectedDiffHints(source, paths) {
613
+ const range = source?.upstreamDiffRange;
614
+ if (!range) return [];
615
+ const listed = new Set(paths);
616
+ const groups = (source?.protectedUpstreamChanges ?? []).filter((group) => group.paths.some((p) => listed.has(p)));
617
+ if (groups.length === 0) return [];
618
+ return [
619
+ "what upstream changed since the last sync, per pinned/ignored entry:",
620
+ ...groups.map((group) => ` git diff ${range} -- ${shellPath(group.entry)}`)
621
+ ];
622
+ }
616
623
  function formatUpstreamLinesAbsent(file) {
617
624
  const n = file.upstreamLinesAbsent ?? 0;
618
625
  return n > 0 ? colors_default.yellow(` \xB7 ${n} upstream ${n === 1 ? "line" : "lines"} absent`) : "";
619
626
  }
620
- function printAnalysisFileGroups(files, linkOptions) {
627
+ function printAnalysisFileGroups(files, linkOptions, diffSource) {
628
+ const protectedBehind = findProtectedBehind(files);
621
629
  printFileGroup(files, "behind", linkOptions, {
622
630
  title: colors_default.cyan("\u2193 behind on upstream")
623
631
  });
@@ -629,17 +637,18 @@ function printAnalysisFileGroups(files, linkOptions) {
629
637
  "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."
630
638
  ]
631
639
  });
632
- printFileSection(
633
- findProtectedBehind(files),
634
- `${warningMark} ${colors_default.yellow("protected but behind upstream")}`,
635
- linkOptions,
636
- {
637
- icon: protectionIcon,
638
- suffix: formatUpstreamChangedLines,
639
- dateSource: "upstream",
640
- hint: PROTECTED_BEHIND_HINT
641
- }
642
- );
640
+ printFileSection(protectedBehind, `${warningMark} ${colors_default.yellow("protected but behind upstream")}`, linkOptions, {
641
+ icon: protectionIcon,
642
+ suffix: formatUpstreamChangedLines,
643
+ dateSource: "upstream",
644
+ hint: [
645
+ PROTECTED_BEHIND_HINT,
646
+ ...protectedDiffHints(
647
+ diffSource,
648
+ protectedBehind.map((file) => file.path)
649
+ )
650
+ ]
651
+ });
643
652
  printFileGroup(files, "drifted", linkOptions, {
644
653
  title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
645
654
  hint: "these files have fork changes but are not pinned or ignored."
@@ -653,7 +662,7 @@ function printAnalysisFileGroups(files, linkOptions) {
653
662
  function writeLogFile(forkPath, files) {
654
663
  const logPath = join3(forkPath, "cella-sync.log");
655
664
  const lines = [
656
- `cella sync analysis - ${(/* @__PURE__ */ new Date()).toISOString()}`,
665
+ `cella sync analysis: ${(/* @__PURE__ */ new Date()).toISOString()}`,
657
666
  DIVIDER,
658
667
  "",
659
668
  `complete file list (${files.length} files)`,
@@ -671,7 +680,7 @@ function writeLogFile(forkPath, files) {
671
680
  const icon = config.icon.replace(/\x1b\[[0-9;]*m/g, "");
672
681
  lines.push(` ${icon} ${config.label.padEnd(12)} ${file.path}`);
673
682
  }
674
- writeFileSync(logPath, lines.join("\n"), "utf-8");
683
+ writeFileSync(logPath, lines.join("\n"), "utf8");
675
684
  return logPath;
676
685
  }
677
686
  function printSyncComplete(result, options = {}) {
@@ -681,12 +690,12 @@ function printSyncComplete(result, options = {}) {
681
690
  const conflicts = result.conflicts.length;
682
691
  console.info();
683
692
  if (options.stagedBranch) {
684
- console.info(`${colors_default.green("\u2713")} Sync merge staged on '${options.stagedBranch}'`);
693
+ console.info(`${checkMark} sync merge staged on '${options.stagedBranch}'`);
685
694
  console.info(
686
- colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. Committing next...`)
695
+ colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. committing next...`)
687
696
  );
688
697
  } else {
689
- console.info(`${colors_default.green("\u2713")} sync complete`);
698
+ console.info(`${checkMark} sync complete`);
690
699
  console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
691
700
  }
692
701
  printProtectedConflicts(result);
@@ -702,18 +711,18 @@ function printProtectedConflicts(result) {
702
711
  `${paths.length} protected ${paths.length === 1 ? "file kept" : "files kept"} the fork version, dropping upstream changes:`
703
712
  )}`
704
713
  );
705
- for (const path4 of paths) {
706
- const file = byPath.get(path4);
714
+ for (const path of paths) {
715
+ const file = byPath.get(path);
707
716
  const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
708
- console.info(` ${icon} ${path4}${file ? formatUpstreamChangedLines(file) : ""}`);
717
+ console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
709
718
  }
710
- console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
719
+ for (const line of [PROTECTED_BEHIND_HINT, ...protectedDiffHints(result, paths)]) console.info(colors_default.dim(` ${line}`));
711
720
  }
712
721
  function plural(count, singular, pluralForm = `${singular}s`) {
713
722
  return `${count} ${count === 1 ? singular : pluralForm}`;
714
723
  }
715
- function shellPath(path4) {
716
- return /^[\w./@+-]+$/.test(path4) ? path4 : `'${path4.replace(/'/g, `'\\''`)}'`;
724
+ function shellPath(path) {
725
+ return /^[\w./@+-]+$/.test(path) ? path : `'${path.replace(/'/g, `'\\''`)}'`;
717
726
  }
718
727
  function printIgnoredUpstreamChanges(result) {
719
728
  const groups = result.ignoredUpstreamChanges ?? [];
@@ -741,19 +750,19 @@ function printUpstreamOverrideChanges(result) {
741
750
  if (report.kind === "unreadable") {
742
751
  const side = report.side === "base" ? "the last sync point" : "the incoming ref";
743
752
  console.info(
744
- colors_default.dim(`upstream overrides not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
753
+ colors_default.dim(`upstream sync config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
745
754
  );
746
755
  return;
747
756
  }
748
757
  const lines = [];
749
- for (const list of ["pinned", "ignored"]) {
758
+ for (const list of ["pinned", "ignored", "packageJsonSync"]) {
750
759
  for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
751
760
  for (const entry of report[list].removed) {
752
761
  lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
753
762
  }
754
763
  }
755
764
  console.info(
756
- `${warningMark} ${colors_default.yellow(`upstream changed its sync overrides \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
765
+ `${warningMark} ${colors_default.yellow(`upstream changed its sync config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
757
766
  );
758
767
  for (const line of lines) console.info(line);
759
768
  console.info(colors_default.dim(` ${CONFIG_FILE} never syncs: add or drop these by hand where they fit your app.`));
@@ -770,7 +779,7 @@ function printMaskingPinWarning(files) {
770
779
  console.info();
771
780
  console.info(
772
781
  `${warningMark} ${colors_default.yellow(
773
- `${masking.length} pinned ${many ? "files match" : "file matches"} the previous upstream but changed upstream \u2014`
782
+ `${masking.length} pinned ${many ? "files match" : "file matches"} the previous upstream but changed upstream:`
774
783
  )}`
775
784
  );
776
785
  console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
@@ -778,7 +787,7 @@ function printMaskingPinWarning(files) {
778
787
  console.info(colors_default.dim(` \u2A00 ${file.path}`));
779
788
  }
780
789
  console.info(
781
- colors_default.yellow(" If the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
790
+ colors_default.yellow(" if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
782
791
  );
783
792
  console.info();
784
793
  }
@@ -795,19 +804,26 @@ function printFlagWarnings(options) {
795
804
  )
796
805
  );
797
806
  }
798
- console.info(colors_default.yellow(" Be extra careful & cherrypick what you want only."));
807
+ console.info(colors_default.yellow(" be extra careful and cherry-pick what you want only."));
799
808
  console.info();
800
809
  }
810
+ function printEngineReports(result, title) {
811
+ printSummary(result.summary, title);
812
+ printIgnoredUpstreamChanges(result);
813
+ printUpstreamOverrideChanges(result);
814
+ printMaskingPinWarning(result.files);
815
+ }
816
+ function printLogFileReport(config, files) {
817
+ if (!config.logFile) return;
818
+ const logPath = writeLogFile(config.forkPath, files);
819
+ console.info();
820
+ console.info(colors_default.dim(`full file list written to: ${logPath}`));
821
+ }
801
822
 
802
823
  // src/utils/overrides.ts
803
824
  import { existsSync as existsSync5 } from "fs";
804
825
  import { join as join7 } from "path";
805
826
 
806
- // src/utils/migration-notes.ts
807
- import { existsSync as existsSync4 } from "fs";
808
- import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
809
- import { dirname as dirname3, join as join6 } from "path";
810
-
811
827
  // src/utils/git.ts
812
828
  import { execFile } from "child_process";
813
829
  import { existsSync as existsSync3 } from "fs";
@@ -819,9 +835,9 @@ import { promisify } from "util";
819
835
  import { mkdir, readFile, writeFile } from "fs/promises";
820
836
  import { dirname, join as join4 } from "path";
821
837
  var MANIFEST_FILE = "cella/cella.manifest.json";
822
- async function readSyncManifest(cwd) {
838
+ function parseSyncManifest(raw) {
823
839
  try {
824
- const parsed = JSON.parse(await readFile(join4(cwd, MANIFEST_FILE), "utf8"));
840
+ const parsed = JSON.parse(raw);
825
841
  const commit = parsed?.upstream?.commit;
826
842
  if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
827
843
  return parsed;
@@ -829,6 +845,13 @@ async function readSyncManifest(cwd) {
829
845
  return null;
830
846
  }
831
847
  }
848
+ async function readSyncManifest(cwd) {
849
+ try {
850
+ return parseSyncManifest(await readFile(join4(cwd, MANIFEST_FILE), "utf8"));
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
832
855
  async function readManifestBase(cwd) {
833
856
  const manifest = await readSyncManifest(cwd);
834
857
  return manifest ? manifest.upstream.commit.toLowerCase() : null;
@@ -842,7 +865,8 @@ async function writeSyncManifest(cwd, manifest) {
842
865
 
843
866
  // src/utils/git.ts
844
867
  var execFileAsync = promisify(execFile);
845
- function sanitizeCredentials(text) {
868
+ var MAX_BUFFER = 50 * 1024 * 1024;
869
+ function redactUrlSecrets(text) {
846
870
  return text.replace(/https?:\/\/[^@\s]+@/g, (match) => {
847
871
  const protocol = match.startsWith("https") ? "https" : "http";
848
872
  return `${protocol}://***@`;
@@ -854,14 +878,14 @@ async function git(args, cwd, options = {}) {
854
878
  ...options.skipEditor ? { GIT_EDITOR: "true" } : {},
855
879
  ...options.env
856
880
  };
857
- const maxBuffer = options.maxBuffer ?? 50 * 1024 * 1024;
881
+ const maxBuffer = options.maxBuffer ?? MAX_BUFFER;
858
882
  try {
859
883
  const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer });
860
884
  return stdout.trim();
861
885
  } catch (error) {
862
886
  if (options.ignoreErrors) return "";
863
887
  if (error instanceof Error) {
864
- error.message = sanitizeCredentials(error.message);
888
+ error.message = redactUrlSecrets(error.message);
865
889
  }
866
890
  throw error;
867
891
  }
@@ -932,8 +956,8 @@ async function flattenBranch(cwd, baseRef, message) {
932
956
  }
933
957
  async function commitSquash(cwd, message) {
934
958
  for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
935
- const path4 = await gitPath(cwd, file);
936
- if (existsSync3(path4)) await unlink(path4);
959
+ const path = await gitPath(cwd, file);
960
+ if (existsSync3(path)) await unlink(path);
937
961
  }
938
962
  await git(["commit", "-m", message], cwd, { skipEditor: true });
939
963
  }
@@ -965,12 +989,13 @@ async function ensureRemote(cwd, remoteName, url) {
965
989
  await git(["remote", "add", remoteName, url], cwd);
966
990
  }
967
991
  }
968
- async function fetch2(cwd, remoteName) {
992
+ async function fetchRemote(cwd, remoteName) {
969
993
  await git(["fetch", remoteName], cwd);
970
994
  }
971
995
  async function fetchUpstreamTags(cwd, remoteName) {
972
996
  await git(["fetch", remoteName, "--no-tags", `+refs/tags/*:refs/${remoteName}/tags/*`], cwd);
973
997
  }
998
+ var releaseTagPattern = /^v?\d[\w.+-]*$/;
974
999
  async function resolveLatestReleaseTag(cwd, remoteName) {
975
1000
  const prefix = `refs/${remoteName}/tags/`;
976
1001
  const output = await git(["for-each-ref", "--sort=-version:refname", "--format=%(refname)", prefix], cwd, {
@@ -981,7 +1006,7 @@ async function resolveLatestReleaseTag(cwd, remoteName) {
981
1006
  const ref = line.trim();
982
1007
  if (!ref) continue;
983
1008
  const tag = ref.slice(prefix.length);
984
- if (/^v\d/.test(tag)) return { tag, ref };
1009
+ if (releaseTagPattern.test(tag)) return { tag, ref };
985
1010
  }
986
1011
  return null;
987
1012
  }
@@ -991,7 +1016,7 @@ async function resolveUpstreamCommit(cwd, remoteName, ref) {
991
1016
  for (const candidate of [`refs/remotes/${remoteName}/${ref}`, tagRef, ref]) {
992
1017
  const sha = await git(["rev-parse", "-q", "--verify", `${candidate}^{commit}`], cwd, { ignoreErrors: true });
993
1018
  if (!sha) continue;
994
- const release = candidate === tagRef && /^v\d[\w.+-]*$/.test(ref) ? { tag: ref, ref: tagRef } : void 0;
1019
+ const release = candidate === tagRef && releaseTagPattern.test(ref) ? { tag: ref, ref: tagRef } : void 0;
995
1020
  return { sha, release };
996
1021
  }
997
1022
  return null;
@@ -1005,7 +1030,8 @@ async function isPublishedUpstream(cwd, remoteName, branchRef, sha) {
1005
1030
  "--contains",
1006
1031
  sha,
1007
1032
  `refs/remotes/${branchRef}`,
1008
- `refs/${remoteName}/tags/v*`
1033
+ `refs/${remoteName}/tags/[0-9]*`,
1034
+ `refs/${remoteName}/tags/v[0-9]*`
1009
1035
  ],
1010
1036
  cwd,
1011
1037
  { ignoreErrors: true }
@@ -1023,10 +1049,8 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
1023
1049
  return Number.parseInt(output.trim(), 10) || 0;
1024
1050
  }
1025
1051
  async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
1026
- const { oldestFirst = true, skip = 0, limit } = options;
1027
- const args = ["log", "--format=%H%x1f%s%x1f%ar"];
1028
- if (oldestFirst) args.push("--reverse");
1029
- args.push(`${fromRef}..${toRef}`);
1052
+ const { skip, limit } = options;
1053
+ const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
1030
1054
  const output = await git(args, cwd, { ignoreErrors: true });
1031
1055
  if (!output) return [];
1032
1056
  const commits = [];
@@ -1036,14 +1060,8 @@ async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
1036
1060
  if (!hash || !message || !date) continue;
1037
1061
  commits.push({ hash, message, date });
1038
1062
  }
1039
- const normalizedSkip = Math.max(0, skip);
1040
- const normalizedLimit = typeof limit === "number" && limit > 0 ? limit : void 0;
1041
- if (normalizedSkip === 0 && normalizedLimit === void 0) {
1042
- return commits;
1043
- }
1044
- const start = Math.min(normalizedSkip, commits.length);
1045
- const end = normalizedLimit === void 0 ? commits.length : Math.min(start + normalizedLimit, commits.length);
1046
- return commits.slice(start, end);
1063
+ const start = Math.max(0, skip ?? 0);
1064
+ return limit && limit > 0 ? commits.slice(start, start + limit) : commits.slice(start);
1047
1065
  }
1048
1066
  async function getFileChangeInfo(cwd, fromRef, toRef) {
1049
1067
  const info = /* @__PURE__ */ new Map();
@@ -1098,12 +1116,9 @@ async function listWorktrees(cwd) {
1098
1116
  }
1099
1117
  return paths;
1100
1118
  }
1101
- async function merge(cwd, ref, options = {}) {
1102
- const args = ["merge", "--no-ff", ref];
1103
- if (options.noCommit) args.push("--no-commit");
1104
- if (options.noEdit) args.push("--no-edit");
1119
+ async function merge(cwd, ref) {
1105
1120
  try {
1106
- await git(args, cwd, { skipEditor: true });
1121
+ await git(["merge", "--no-ff", "--no-commit", "--no-edit", ref], cwd, { skipEditor: true });
1107
1122
  return { success: true, conflicts: [] };
1108
1123
  } catch (error) {
1109
1124
  const conflicts = await getConflictedFiles(cwd);
@@ -1131,6 +1146,22 @@ async function checkoutFromRef(cwd, ref, filePath) {
1131
1146
  async function restoreWorktreeFromRef(cwd, ref, filePath) {
1132
1147
  await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
1133
1148
  }
1149
+ async function batchRestoreWorktreeFromRef(cwd, ref, filePaths) {
1150
+ const batchSize = 200;
1151
+ for (let i = 0; i < filePaths.length; i += batchSize) {
1152
+ const batch = filePaths.slice(i, i + batchSize);
1153
+ await git(["--literal-pathspecs", "restore", `--source=${ref}`, "--worktree", "--", ...batch], cwd);
1154
+ }
1155
+ }
1156
+ async function listIdenticalToRef(cwd, ref) {
1157
+ const nulSeparated = (out) => out.split("\0").filter(Boolean);
1158
+ const [tracked, changed] = await Promise.all([
1159
+ git(["ls-tree", "-r", "--name-only", "-z", ref], cwd),
1160
+ git(["diff", "--name-only", "--no-renames", "-z", ref, "--"], cwd)
1161
+ ]);
1162
+ const differs = new Set(nulSeparated(changed));
1163
+ return new Set(nulSeparated(tracked).filter((path) => !differs.has(path)));
1164
+ }
1134
1165
  async function getMergeBase(cwd, ref1, ref2) {
1135
1166
  return git(["merge-base", ref1, ref2], cwd);
1136
1167
  }
@@ -1140,6 +1171,7 @@ async function getFileChanges(cwd, baseRef, targetRef) {
1140
1171
  });
1141
1172
  const changes = /* @__PURE__ */ new Map();
1142
1173
  if (!output) return changes;
1174
+ const hashOrNull = (hash) => hash === "0".repeat(40) ? null : hash;
1143
1175
  for (const line of output.split("\n")) {
1144
1176
  if (!line) continue;
1145
1177
  const renameMatch = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) R\d*\s+(.+)\t(.+)$/);
@@ -1147,10 +1179,9 @@ async function getFileChanges(cwd, baseRef, targetRef) {
1147
1179
  const [, baseHash, targetHash, oldPath, newPath] = renameMatch;
1148
1180
  changes.set(newPath, {
1149
1181
  status: "R",
1150
- baseHash: baseHash === "0".repeat(40) ? null : baseHash,
1151
- targetHash: targetHash === "0".repeat(40) ? null : targetHash,
1152
- oldPath,
1153
- newPath
1182
+ baseHash: hashOrNull(baseHash),
1183
+ targetHash: hashOrNull(targetHash),
1184
+ oldPath
1154
1185
  });
1155
1186
  continue;
1156
1187
  }
@@ -1159,8 +1190,8 @@ async function getFileChanges(cwd, baseRef, targetRef) {
1159
1190
  const [, baseHash, targetHash, status, filePath] = match;
1160
1191
  changes.set(filePath, {
1161
1192
  status,
1162
- baseHash: baseHash === "0".repeat(40) ? null : baseHash,
1163
- targetHash: targetHash === "0".repeat(40) ? null : targetHash
1193
+ baseHash: hashOrNull(baseHash),
1194
+ targetHash: hashOrNull(targetHash)
1164
1195
  });
1165
1196
  }
1166
1197
  }
@@ -1174,9 +1205,9 @@ async function getDiffStat(cwd, fromRef, toRef, paths = []) {
1174
1205
  for (const line of out.split("\n")) {
1175
1206
  if (!line.trim()) continue;
1176
1207
  const [a, d, ...rest] = line.split(" ");
1177
- const path4 = rest.join(" ");
1178
- if (!path4) continue;
1179
- stat.set(path4, {
1208
+ const path = rest.join(" ");
1209
+ if (!path) continue;
1210
+ stat.set(path, {
1180
1211
  additions: a === "-" ? null : Number(a),
1181
1212
  deletions: d === "-" ? null : Number(d)
1182
1213
  });
@@ -1258,8 +1289,8 @@ async function storeLastSyncRef(cwd, upstreamHash) {
1258
1289
  await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
1259
1290
  }
1260
1291
  }
1261
- async function stagePath(cwd, path4) {
1262
- await git(["add", "--", path4], cwd, { ignoreErrors: true });
1292
+ async function stagePath(cwd, path) {
1293
+ await git(["add", "--", path], cwd, { ignoreErrors: true });
1263
1294
  }
1264
1295
  async function getStoredSyncRef(cwd) {
1265
1296
  const ref = await git(["rev-parse", "refs/cella/last-sync"], cwd, { ignoreErrors: true });
@@ -1272,14 +1303,7 @@ async function getStoredSyncHead(cwd) {
1272
1303
  async function readManifestAtRef(cwd, ref) {
1273
1304
  const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
1274
1305
  if (!raw) return null;
1275
- try {
1276
- const parsed = JSON.parse(raw);
1277
- const commit = parsed?.upstream?.commit;
1278
- if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
1279
- return parsed;
1280
- } catch {
1281
- return null;
1282
- }
1306
+ return parseSyncManifest(raw);
1283
1307
  }
1284
1308
  async function readManifestBaseAtRef(cwd, ref) {
1285
1309
  const manifest = await readManifestAtRef(cwd, ref);
@@ -1400,8 +1424,8 @@ async function ensureSyncBase(cwd, headRef, upstreamRef) {
1400
1424
  throw new Error(
1401
1425
  `no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
1402
1426
 
1403
- This fork has unrelated history with upstream \u2014 typical for a create-cella scaffold, or after
1404
- upstream squashed its history \u2014 and no recorded or inferable base commit exists after fetching
1427
+ This fork has unrelated history with upstream (typical for a create-cella scaffold, or after
1428
+ upstream squashed its history) and no recorded or inferable base commit exists after fetching
1405
1429
  upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
1406
1430
 
1407
1431
  git update-ref refs/cella/last-sync <upstream-sha>
@@ -1460,9 +1484,15 @@ async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
1460
1484
  }
1461
1485
 
1462
1486
  // src/utils/migration-notes.ts
1487
+ import { existsSync as existsSync4 } from "fs";
1488
+ import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
1489
+ import { dirname as dirname3, join as join6 } from "path";
1463
1490
  var NOTES_DIR = "cella/migrations";
1464
1491
  var PENDING_FILE = "cella/cella.migrations.json";
1465
1492
  var NOTE_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
1493
+ function isCodemodFile(name) {
1494
+ return name.endsWith(".ts") && !name.endsWith(".test.ts");
1495
+ }
1466
1496
  function isUpstreamOnly(filePath) {
1467
1497
  return filePath === NOTES_DIR || filePath.startsWith(`${NOTES_DIR}/`);
1468
1498
  }
@@ -1493,7 +1523,7 @@ function parseNote(id, source, fileNames) {
1493
1523
  syncBreaking: meta.get("syncBreaking") === "true",
1494
1524
  clientCacheBump: meta.get("clientCacheBump") === "true",
1495
1525
  roots: (meta.get("roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean),
1496
- codemod: fileNames.find((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")) ?? null
1526
+ codemod: fileNames.find(isCodemodFile) ?? null
1497
1527
  };
1498
1528
  }
1499
1529
  async function listNoteIds(cwd, ref) {
@@ -1512,23 +1542,23 @@ async function arrivedNoteIds(cwd, from, to) {
1512
1542
  return after.filter((id) => !known.has(id));
1513
1543
  }
1514
1544
  async function readPending(cwd, syncRef) {
1515
- const path4 = join6(cwd, PENDING_FILE);
1516
- if (!existsSync4(path4)) return [];
1517
- const parsed = JSON.parse(await readFile2(path4, "utf8"));
1545
+ const path = join6(cwd, PENDING_FILE);
1546
+ if (!existsSync4(path)) return [];
1547
+ const parsed = JSON.parse(await readFile2(path, "utf8"));
1518
1548
  if (Array.isArray(parsed.pending)) return parsed.pending.filter((id) => typeof id === "string");
1519
1549
  if (!Array.isArray(parsed.applied) || !syncRef) return [];
1520
1550
  const applied = new Set(parsed.applied);
1521
1551
  return (await listNoteIds(cwd, syncRef)).filter((id) => !applied.has(id));
1522
1552
  }
1523
1553
  async function writePending(cwd, ids) {
1524
- const path4 = join6(cwd, PENDING_FILE);
1554
+ const path = join6(cwd, PENDING_FILE);
1525
1555
  const pending = [...new Set(ids)].sort();
1526
1556
  if (pending.length === 0) {
1527
- await rm(path4, { force: true });
1557
+ await rm(path, { force: true });
1528
1558
  return;
1529
1559
  }
1530
- await mkdir3(dirname3(path4), { recursive: true });
1531
- await writeFile2(path4, `${JSON.stringify({ pending }, null, 2)}
1560
+ await mkdir3(dirname3(path), { recursive: true });
1561
+ await writeFile2(path, `${JSON.stringify({ pending }, null, 2)}
1532
1562
  `, "utf8");
1533
1563
  }
1534
1564
  function noteUrl(upstreamGitHubUrl, commit, id) {
@@ -1572,6 +1602,19 @@ function groupIgnoredUpstreamChanges(files, ignored) {
1572
1602
  for (const group of groups.values()) group.paths.sort();
1573
1603
  return entries.flatMap((entry) => groups.get(entry) ?? []);
1574
1604
  }
1605
+ function groupProtectedUpstreamChanges(paths, config) {
1606
+ const entries = [...config.overrides?.pinned ?? [], ...config.overrides?.ignored ?? []].map((entry) => entry.replace(/\/+$/, "")).filter(Boolean);
1607
+ const mostSpecificFirst = [...new Set(entries)].sort((a, b) => b.length - a.length);
1608
+ const groups = /* @__PURE__ */ new Map();
1609
+ for (const path of paths) {
1610
+ const entry = mostSpecificFirst.find((candidate) => isUnderAnyFolder(path, [candidate])) ?? path;
1611
+ const group = groups.get(entry) ?? { entry, paths: [] };
1612
+ group.paths.push(path);
1613
+ groups.set(entry, group);
1614
+ }
1615
+ for (const group of groups.values()) group.paths.sort();
1616
+ return [...groups.values()];
1617
+ }
1575
1618
  function isPinnedForSync(filePath, config, unpinned) {
1576
1619
  if (isManagedFile(filePath)) return true;
1577
1620
  if (unpinned) return false;
@@ -1580,7 +1623,7 @@ function isPinnedForSync(filePath, config, unpinned) {
1580
1623
  function hasGlobChars(entry) {
1581
1624
  return entry.includes("*") || entry.includes("?");
1582
1625
  }
1583
- function validateOverrides(config, forkPath) {
1626
+ async function validateOverrides(config, forkPath, upstreamRef) {
1584
1627
  const warnings = [];
1585
1628
  const pinned = config.overrides?.pinned || [];
1586
1629
  for (const entry of pinned) {
@@ -1607,6 +1650,7 @@ function validateOverrides(config, forkPath) {
1607
1650
  message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
1608
1651
  });
1609
1652
  } else if (!existsSync5(join7(forkPath, entry.replace(/\/+$/, "")))) {
1653
+ if (upstreamRef && await fileExistsAtRef(forkPath, upstreamRef, entry.replace(/\/+$/, ""))) continue;
1610
1654
  warnings.push({
1611
1655
  type: "ignored-not-found",
1612
1656
  pattern: entry,
@@ -1623,78 +1667,124 @@ function printWarnings(warnings) {
1623
1667
  }
1624
1668
 
1625
1669
  // src/cli.ts
1626
- function readOptions(opts) {
1627
- const scope = typeof opts.scope === "string" ? opts.scope : void 0;
1670
+ var str = (value) => typeof value === "string" ? value : void 0;
1671
+ var flag = (value) => value === true;
1672
+ function readOptions(opts, operands = []) {
1673
+ const scope = str(opts.scope);
1628
1674
  if (scope && scope !== "all" && scope !== "risk" && scope !== "protected") {
1629
1675
  throw new Error(`invalid --scope '${scope}'. expected one of: all, risk, protected`);
1630
1676
  }
1631
1677
  const normalizedScope = scope;
1678
+ const run = str(opts.run);
1679
+ if (operands.length > 0 && !run) {
1680
+ throw new Error(`unexpected argument '${operands[0]}'. arguments after the options only go with --run <id>`);
1681
+ }
1632
1682
  return {
1633
- logFile: opts.log === true,
1634
- verbose: opts.verbose === true,
1635
- list: opts.list === true,
1636
- json: opts.json === true,
1637
- diff: typeof opts.diff === "string" ? opts.diff : void 0,
1638
- openDiff: typeof opts.openDiff === "string" ? opts.openDiff : void 0,
1683
+ logFile: flag(opts.log),
1684
+ verbose: flag(opts.verbose),
1685
+ list: flag(opts.list),
1686
+ json: flag(opts.json),
1687
+ diff: str(opts.diff),
1688
+ openDiff: str(opts.openDiff),
1639
1689
  scope: normalizedScope,
1640
- fork: typeof opts.fork === "string" ? opts.fork : void 0,
1641
- hard: opts.hard === true,
1642
- unpinned: opts.unpinned === true,
1690
+ fork: str(opts.fork),
1691
+ hard: flag(opts.hard),
1692
+ unpinned: flag(opts.unpinned),
1643
1693
  track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
1644
- ref: typeof opts.ref === "string" && opts.ref ? opts.ref : void 0,
1645
- force: opts.force === true,
1646
- checkOverrides: opts.checkOverrides === true,
1647
- coverage: opts.coverage === true,
1648
- all: opts.all === true,
1649
- show: typeof opts.show === "string" ? opts.show : void 0,
1650
- extract: typeof opts.extract === "string" ? opts.extract : void 0,
1694
+ ref: str(opts.ref) || void 0,
1695
+ keepConfig: flag(opts.keepConfig),
1696
+ force: flag(opts.force),
1697
+ checkOverrides: flag(opts.checkOverrides),
1698
+ coverage: flag(opts.coverage),
1699
+ since: str(opts.since) || void 0,
1700
+ md: flag(opts.md),
1701
+ all: flag(opts.all),
1702
+ show: str(opts.show),
1703
+ extract: str(opts.extract),
1704
+ run,
1705
+ script: str(opts.script),
1706
+ runArgs: run ? operands : void 0,
1651
1707
  mark: Array.isArray(opts.mark) ? opts.mark.filter((id) => typeof id === "string") : void 0
1652
1708
  };
1653
1709
  }
1710
+ var logOption = {
1711
+ flags: "--log",
1712
+ description: "write complete file list to cella-sync.log"
1713
+ };
1714
+ var jsonOption = {
1715
+ flags: "--json",
1716
+ description: "machine-readable output for tooling/agents"
1717
+ };
1718
+ var trackOption = {
1719
+ flags: "--track <mode>",
1720
+ description: "override upstream tracking for this run: release|branch"
1721
+ };
1722
+ var refOption = {
1723
+ flags: "--ref <ref>",
1724
+ description: "pin the upstream commit for this run: sha, release tag or branch"
1725
+ };
1726
+ var hardOption = {
1727
+ flags: "--hard",
1728
+ description: "overwrite drifted files with upstream version (aggressive realignment)"
1729
+ };
1730
+ var verboseOption = {
1731
+ flags: "-V, --verbose",
1732
+ description: "show detailed output during operations"
1733
+ };
1734
+ var forkOption = {
1735
+ flags: "--fork <name>",
1736
+ description: "pre-select a fork by name (skips the fork selection prompt)"
1737
+ };
1654
1738
  var serviceDefinitions = [
1655
1739
  {
1656
1740
  name: "analyze",
1657
1741
  description: "dry run to see what would change on sync",
1658
1742
  options: [
1659
- { flags: "--log", description: "write complete file list to cella-sync.log" },
1743
+ logOption,
1660
1744
  { flags: "--list", description: "non-interactive output for tooling (one file per line)" },
1661
- { flags: "--json", description: "machine-readable output for tooling/agents" },
1745
+ jsonOption,
1662
1746
  { flags: "--scope <scope>", description: "analyze scope for --list/--json: all|risk|protected" },
1663
- { flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
1664
- { flags: "--ref <ref>", description: "pin the upstream commit for this run: sha, release tag or branch" },
1665
- { flags: "--diff <path>", description: "print unified diff for one file, then exit" },
1747
+ trackOption,
1748
+ refOption,
1749
+ { flags: "--diff <path>", description: "print the unified diff for one file, then exit" },
1666
1750
  { flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
1667
1751
  ],
1668
1752
  includeInMenu: (context) => !context.isUpstreamRepo
1669
1753
  },
1670
1754
  {
1671
1755
  name: "sync",
1672
- description: "merge upstream changes into your app",
1756
+ description: "merge upstream changes onto a fresh branch, sync package.json and open a squash-merge PR",
1673
1757
  options: [
1674
- { flags: "--log", description: "write complete file list to cella-sync.log" },
1675
- { flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" },
1758
+ logOption,
1759
+ hardOption,
1676
1760
  { flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
1677
- { flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
1678
- { flags: "--ref <ref>", description: "pin the upstream commit for this run: sha, release tag or branch" }
1761
+ trackOption,
1762
+ refOption,
1763
+ { flags: "--keep-config", description: "merge with your sync config as it stands when upstream changed its own" }
1679
1764
  ],
1680
- includeInMenu: (context) => !context.isUpstreamRepo,
1681
- menuDescription: () => "merge upstream changes + sync package.json"
1765
+ includeInMenu: (context) => !context.isUpstreamRepo
1682
1766
  },
1683
1767
  {
1684
1768
  name: "migrate",
1685
1769
  description: "list the upstream migration notes this app has not handled yet",
1686
1770
  options: [
1687
1771
  { flags: "--all", description: "list every upstream note, handled or not" },
1688
- { flags: "--json", description: "machine-readable output for tooling/agents" },
1772
+ jsonOption,
1689
1773
  { flags: "--show <id>", description: "print one note's README" },
1690
1774
  { flags: "--extract <id>", description: "write one note's folder under node_modules/.cache to run its codemod" },
1775
+ { flags: "--run <id>", description: "run one note's codemod; files identical to upstream stay as they are" },
1776
+ { flags: "--script <file>", description: "with --run: the script to run when the note folder holds several" },
1691
1777
  { flags: "--mark <ids...>", description: "record notes as handled" }
1692
1778
  ],
1779
+ operands: {
1780
+ flags: "[codemodArgs...]",
1781
+ description: "with --run, after `--`: arguments for the codemod (default: inventory and the note's roots)"
1782
+ },
1693
1783
  includeInMenu: (context) => !context.isUpstreamRepo
1694
1784
  },
1695
1785
  {
1696
1786
  name: "audit",
1697
- description: "check for outdated packages & vulnerabilities",
1787
+ description: "check for outdated packages and vulnerabilities",
1698
1788
  options: [
1699
1789
  { flags: "--list", description: "skip interactive update prompts after printing audit results" },
1700
1790
  { flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
@@ -1703,23 +1793,27 @@ var serviceDefinitions = [
1703
1793
  },
1704
1794
  {
1705
1795
  name: "forks",
1706
- description: "sync downstream to local fork repositories",
1796
+ description: "run normal sync inside local fork repositories",
1707
1797
  options: [
1708
- { flags: "--fork <name>", description: "pre-select fork by name (skips fork selection prompt)" },
1798
+ forkOption,
1709
1799
  { flags: "--log", description: "write complete file list to cella-sync.log for each synced fork" },
1710
- { flags: "-V, --verbose", description: "show detailed output during operations" },
1711
- { flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" }
1800
+ verboseOption,
1801
+ hardOption,
1802
+ {
1803
+ flags: "--keep-config",
1804
+ description: "merge with the fork's sync config as it stands when upstream changed its own"
1805
+ }
1712
1806
  ],
1713
1807
  includeInMenu: (context) => context.hasForks
1714
1808
  },
1715
1809
  {
1716
1810
  name: "contributions",
1717
- description: "pull and adopt changes from forks",
1811
+ description: "pull and adopt changes from local forks",
1718
1812
  options: [
1719
- { flags: "--fork <name>", description: "select a specific fork directly (skips fork selection)" },
1720
- { flags: "--list", description: "non-interactive output (one file per line)" },
1721
- { flags: "--json", description: "machine-readable JSON output for tooling/agents" },
1722
- { flags: "--diff <path>", description: "print the unified diff for a single contributed file, then exit" }
1813
+ forkOption,
1814
+ { flags: "--list", description: "non-interactive output for tooling (tab-separated rows)" },
1815
+ jsonOption,
1816
+ { flags: "--diff <path>", description: "print the unified diff for one contributed file, then exit" }
1723
1817
  ],
1724
1818
  includeInMenu: (context) => context.hasForks
1725
1819
  },
@@ -1727,26 +1821,31 @@ var serviceDefinitions = [
1727
1821
  name: "stats",
1728
1822
  description: "count files by category and workspace package",
1729
1823
  options: [
1730
- { flags: "-V, --verbose", description: "show detailed output during operations" },
1731
- { flags: "--coverage", description: "regenerate test coverage before showing the stats summary" }
1824
+ verboseOption,
1825
+ { flags: "--coverage", description: "regenerate test coverage before showing the stats summary" },
1826
+ {
1827
+ flags: "--since <ref>",
1828
+ description: "count what the current branch changed since it left <ref>, by kind of file"
1829
+ },
1830
+ { flags: "--md", description: "with --since: print the table as markdown for a pull request description" }
1732
1831
  ]
1733
1832
  }
1734
1833
  ];
1735
- async function getMenuContext(userConfig, forkPath) {
1834
+ function getMenuContext(userConfig, forkPath) {
1736
1835
  return {
1737
1836
  hasForks: (userConfig.forks?.length ?? 0) > 0,
1738
- isUpstreamRepo: JSON.parse(readFileSync(join8(forkPath, "package.json"), "utf8")).name === "cella"
1837
+ isUpstreamRepo: isUpstreamRepo(forkPath)
1739
1838
  };
1740
1839
  }
1741
1840
  function buildServiceChoices(context) {
1742
1841
  const label = (name) => name.padEnd(14);
1743
1842
  const baseChoices = serviceDefinitions.filter((service) => service.includeInMenu?.(context) ?? true).map((service) => ({
1744
1843
  value: service.name,
1745
- name: `${label(service.name)}${colors_default.dim(service.menuDescription?.(context) ?? service.description)}`
1844
+ name: `${label(service.name)}${colors_default.dim(service.description)}`
1746
1845
  }));
1747
1846
  return [
1748
1847
  ...baseChoices,
1749
- { type: "separator", separator: "\u2500".repeat(40) },
1848
+ { type: "separator", separator: MENU_DIVIDER },
1750
1849
  { value: "exit", name: colors_default.red(`${label("exit")}${colors_default.dim("quit without doing anything")}`) }
1751
1850
  ];
1752
1851
  }
@@ -1755,10 +1854,13 @@ function addServiceCommand(program, definition, setSelection) {
1755
1854
  for (const option of definition.options ?? []) {
1756
1855
  command.option(option.flags, option.description);
1757
1856
  }
1758
- command.action((opts) => {
1857
+ if (definition.operands) command.argument(definition.operands.flags, definition.operands.description);
1858
+ command.action((...args) => {
1859
+ const operands = definition.operands ? args[0] : [];
1860
+ const opts = definition.operands ? args[1] : args[0];
1759
1861
  setSelection({
1760
1862
  service: definition.name,
1761
- options: readOptions(opts)
1863
+ options: readOptions(opts, operands)
1762
1864
  });
1763
1865
  });
1764
1866
  }
@@ -1775,8 +1877,10 @@ function buildProgram(setSelection) {
1775
1877
  " $ cella sync --unpinned",
1776
1878
  " $ cella sync --track branch",
1777
1879
  " $ cella sync --ref 4f7d87c",
1880
+ " $ cella migrate --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
1778
1881
  " $ cella migrate --mark 20261002T0614-config-switch",
1779
1882
  " $ cella audit --check-overrides",
1883
+ " $ cella stats --since origin/main --md",
1780
1884
  " $ cella contributions --fork raak --json"
1781
1885
  ].join("\n")
1782
1886
  );
@@ -1799,7 +1903,7 @@ function parseCommandLine(argv) {
1799
1903
  async function promptForService(userConfig, forkPath) {
1800
1904
  const selected = await select({
1801
1905
  message: "choose a service:",
1802
- choices: buildServiceChoices(await getMenuContext(userConfig, forkPath)),
1906
+ choices: buildServiceChoices(getMenuContext(userConfig, forkPath)),
1803
1907
  loop: false
1804
1908
  });
1805
1909
  if (selected === "exit") {
@@ -1822,9 +1926,10 @@ function buildRuntimeConfig(userConfig, forkPath, selection) {
1822
1926
  }
1823
1927
  async function parseCli(userConfig, forkPath) {
1824
1928
  const selection = parseCommandLine(process5.argv);
1825
- if (selection.options.json || selection.options.diff) setJsonMode(true);
1929
+ const machineList = selection.options.list && selection.service !== "audit";
1930
+ if (selection.options.json || selection.options.diff || selection.options.md || machineList) setJsonMode(true);
1826
1931
  printHeader();
1827
- const warnings = validateOverrides(userConfig, forkPath);
1932
+ const warnings = await validateOverrides(userConfig, forkPath, resolveUpstream(userConfig.settings).branchRef);
1828
1933
  if (warnings.length > 0) {
1829
1934
  printWarnings(warnings);
1830
1935
  console.info();
@@ -1842,14 +1947,14 @@ import { basename as basename4 } from "path";
1842
1947
  import { execFileSync } from "child_process";
1843
1948
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
1844
1949
  import { tmpdir } from "os";
1845
- import { basename as basename2, join as join9 } from "path";
1950
+ import { basename as basename2, join as join8 } from "path";
1846
1951
  import process6 from "process";
1847
1952
  function gitDiffFile(cwd, range, filePath, options = {}) {
1848
1953
  const args = ["diff", "--no-color"];
1849
1954
  if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
1850
1955
  args.push(range, "--", filePath);
1851
1956
  try {
1852
- return execFileSync("git", args, { cwd, maxBuffer: 50 * 1024 * 1024 });
1957
+ return execFileSync("git", args, { cwd, maxBuffer: MAX_BUFFER });
1853
1958
  } catch (error) {
1854
1959
  const stderr = error.stderr?.toString().trim();
1855
1960
  throw new Error(stderr || `failed to diff ${filePath}`);
@@ -1936,9 +2041,9 @@ function openWithDefaultApp(target) {
1936
2041
  }
1937
2042
  async function openDiffInBrowser(patch, meta, repoPath) {
1938
2043
  const html = await renderDiffPage(patch, meta);
1939
- const dir = join9(tmpdir(), `cella-diff-${basename2(repoPath)}`);
2044
+ const dir = join8(tmpdir(), `cella-diff-${basename2(repoPath)}`);
1940
2045
  mkdirSync(dir, { recursive: true });
1941
- const outPath = join9(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
2046
+ const outPath = join8(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
1942
2047
  writeFileSync2(outPath, html);
1943
2048
  openWithDefaultApp(outPath);
1944
2049
  return outPath;
@@ -1947,13 +2052,13 @@ async function openDiffInBrowser(patch, meta, repoPath) {
1947
2052
  // src/services/merge-engine.ts
1948
2053
  import { existsSync as existsSync7 } from "fs";
1949
2054
  import { rm as rm2 } from "fs/promises";
1950
- import { join as join11 } from "path";
2055
+ import { join as join10 } from "path";
1951
2056
 
1952
2057
  // src/utils/cleanup.ts
1953
2058
  import { createHash } from "crypto";
1954
2059
  import { existsSync as existsSync6, rmSync } from "fs";
1955
2060
  import { tmpdir as tmpdir2 } from "os";
1956
- import { basename as basename3, join as join10, resolve as resolve4 } from "path";
2061
+ import { basename as basename3, join as join9, resolve as resolve4 } from "path";
1957
2062
  import process7 from "process";
1958
2063
  var WORKTREE_PREFIXES = {
1959
2064
  sync: "cella-sync-",
@@ -1961,10 +2066,10 @@ var WORKTREE_PREFIXES = {
1961
2066
  };
1962
2067
  function buildWorktreePath(kind, repoPath) {
1963
2068
  const key = createHash("sha256").update(resolve4(repoPath)).digest("hex").slice(0, 8);
1964
- return join10(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
2069
+ return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
1965
2070
  }
1966
2071
  function legacyWorktreePath(kind, repoPath) {
1967
- return join10(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
2072
+ return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
1968
2073
  }
1969
2074
  function getWorktreePath(repoPath) {
1970
2075
  return buildWorktreePath("sync", repoPath);
@@ -2003,7 +2108,7 @@ async function cleanupLeftoverWorktrees(repoPath) {
2003
2108
  }
2004
2109
  async function handleAbort(signal) {
2005
2110
  console.info();
2006
- console.info(`${warningMark} Interrupted (${signal}) - cleaning up...`);
2111
+ console.info(`${warningMark} interrupted (${signal}): cleaning up...`);
2007
2112
  if (currentRepoPath && currentWorktreePath) {
2008
2113
  try {
2009
2114
  await mergeAbort(currentWorktreePath);
@@ -2011,11 +2116,9 @@ async function handleAbort(signal) {
2011
2116
  }
2012
2117
  try {
2013
2118
  await cleanupWorktree(currentRepoPath, currentWorktreePath);
2014
- console.info(`${colors_default.green("\u2713")} no changes were made to your repository.`);
2119
+ console.info(`${checkMark} no changes were made to your repository.`);
2015
2120
  } catch (error) {
2016
- console.error(
2017
- `${colors_default.red("\u2717")} failed to clean up worktree: ${error instanceof Error ? error.message : "unknown error"}`
2018
- );
2121
+ console.error(`${colors_default.red("\u2717")} failed to clean up worktree: ${errorMessage(error)}`);
2019
2122
  }
2020
2123
  }
2021
2124
  process7.exit(1);
@@ -2111,25 +2214,44 @@ function readProperty(object, name, sourceFile) {
2111
2214
  }
2112
2215
  return void 0;
2113
2216
  }
2217
+ function readStringEntries(node) {
2218
+ if (!Node.isArrayLiteralExpression(node)) return null;
2219
+ const entries = [];
2220
+ for (const element of node.getElements()) {
2221
+ if (Node.isStringLiteral(element) || Node.isNoSubstitutionTemplateLiteral(element)) {
2222
+ entries.push(element.getLiteralValue());
2223
+ }
2224
+ }
2225
+ return entries;
2226
+ }
2227
+ function readPackageJsonSync(config, sourceFile) {
2228
+ const settings = readProperty(config, "settings", sourceFile);
2229
+ if (settings === void 0) return [...DEFAULT_PACKAGE_JSON_SYNC];
2230
+ if (!settings || !Node.isObjectLiteralExpression(settings)) return null;
2231
+ const keys = readProperty(settings, "packageJsonSync", sourceFile);
2232
+ if (keys === void 0) return [...DEFAULT_PACKAGE_JSON_SYNC];
2233
+ return keys ? readStringEntries(keys) : null;
2234
+ }
2114
2235
  function parseOverrideLists(source) {
2115
2236
  try {
2116
2237
  const project = new Project2({ useInMemoryFileSystem: true, skipLoadingLibFiles: true });
2117
2238
  const sourceFile = project.createSourceFile("cella.config.ts", source);
2118
2239
  const config = findConfigObject(sourceFile);
2119
2240
  if (!config) return null;
2241
+ const lists = {
2242
+ pinned: [],
2243
+ ignored: [],
2244
+ packageJsonSync: readPackageJsonSync(config, sourceFile)
2245
+ };
2120
2246
  const overrides = readProperty(config, "overrides", sourceFile);
2121
- if (overrides === void 0) return { pinned: [], ignored: [] };
2247
+ if (overrides === void 0) return lists;
2122
2248
  if (!overrides || !Node.isObjectLiteralExpression(overrides)) return null;
2123
- const lists = { pinned: [], ignored: [] };
2124
2249
  for (const key of LIST_KEYS) {
2125
2250
  const list = readProperty(overrides, key, sourceFile);
2126
2251
  if (list === void 0) continue;
2127
- if (!list || !Node.isArrayLiteralExpression(list)) return null;
2128
- for (const element of list.getElements()) {
2129
- if (Node.isStringLiteral(element) || Node.isNoSubstitutionTemplateLiteral(element)) {
2130
- lists[key].push(element.getLiteralValue());
2131
- }
2132
- }
2252
+ const entries = list ? readStringEntries(list) : null;
2253
+ if (!entries) return null;
2254
+ lists[key] = entries;
2133
2255
  }
2134
2256
  return lists;
2135
2257
  } catch {
@@ -2149,11 +2271,19 @@ function diffOverrideLists(base, incoming, fork) {
2149
2271
  removed: [...baseSet].filter((entry) => !incomingSet.has(entry) && forkEntries.includes(entry))
2150
2272
  };
2151
2273
  };
2152
- return { pinned: diff("pinned"), ignored: diff("ignored") };
2274
+ const diffKeys = () => {
2275
+ const [baseKeys, incomingKeys, forkKeys] = [base.packageJsonSync, incoming.packageJsonSync, fork.packageJsonSync];
2276
+ if (!baseKeys || !incomingKeys || !forkKeys) return { added: [], removed: [] };
2277
+ return {
2278
+ added: [...new Set(incomingKeys)].filter((key) => !baseKeys.includes(key) && !forkKeys.includes(key)),
2279
+ removed: [...new Set(baseKeys)].filter((key) => !incomingKeys.includes(key) && forkKeys.includes(key))
2280
+ };
2281
+ };
2282
+ return { pinned: diff("pinned"), ignored: diff("ignored"), packageJsonSync: diffKeys() };
2153
2283
  }
2154
2284
  async function readConfigSourceAtRef(repoPath, ref) {
2155
- for (const path4 of CONFIG_FILE_PATHS) {
2156
- const source = await git(["show", `${ref}:${path4}`], repoPath, { ignoreErrors: true });
2285
+ for (const path of CONFIG_FILE_PATHS) {
2286
+ const source = await git(["show", `${ref}:${path}`], repoPath, { ignoreErrors: true });
2157
2287
  if (source) return source;
2158
2288
  }
2159
2289
  return null;
@@ -2170,10 +2300,11 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
2170
2300
  if (!incoming) return { kind: "unreadable", side: "incoming" };
2171
2301
  const fork = {
2172
2302
  pinned: forkConfig.overrides?.pinned ?? [],
2173
- ignored: forkConfig.overrides?.ignored ?? []
2303
+ ignored: forkConfig.overrides?.ignored ?? [],
2304
+ packageJsonSync: forkConfig.settings.packageJsonSync ?? DEFAULT_PACKAGE_JSON_SYNC
2174
2305
  };
2175
2306
  const changes = diffOverrideLists(base, incoming, fork);
2176
- const count = LIST_KEYS.reduce((n, key) => n + changes[key].added.length + changes[key].removed.length, 0);
2307
+ const count = Object.values(changes).reduce((n, list) => n + list.added.length + list.removed.length, 0);
2177
2308
  return count > 0 ? { kind: "changes", ...changes } : void 0;
2178
2309
  }
2179
2310
 
@@ -2297,10 +2428,8 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
2297
2428
  status = fileIsPinned ? "pinned" : "diverged";
2298
2429
  } else if (forkChanged && !upstreamChanged2) {
2299
2430
  status = fileIsPinned ? "ahead" : predicates.hard ? "behind" : "drifted";
2300
- } else if (!forkChanged && upstreamChanged2) {
2301
- status = "behind";
2302
2431
  } else {
2303
- status = "identical";
2432
+ status = "behind";
2304
2433
  }
2305
2434
  }
2306
2435
  const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
@@ -2385,6 +2514,21 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
2385
2514
  }
2386
2515
 
2387
2516
  // src/services/merge-engine.ts
2517
+ var BehindSyncPointError = class extends Error {
2518
+ };
2519
+ var UpstreamConfigChangedError = class extends Error {
2520
+ report;
2521
+ constructor(report) {
2522
+ super(
2523
+ [
2524
+ "upstream changed its sync config and yours does not follow it: stopped before the merge, nothing changed.",
2525
+ ` add or drop the entries that fit your app in ${CONFIG_FILE}, commit, then rerun.`,
2526
+ " to merge with your config as it stands, rerun with --keep-config."
2527
+ ].join("\n")
2528
+ );
2529
+ this.report = report;
2530
+ }
2531
+ };
2388
2532
  function getGitHubBaseUrl(remoteUrl) {
2389
2533
  const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^.]+)(?:\.git)?$/);
2390
2534
  if (sshMatch) {
@@ -2396,10 +2540,13 @@ function getGitHubBaseUrl(remoteUrl) {
2396
2540
  }
2397
2541
  return null;
2398
2542
  }
2543
+ function isProtectedFile(file) {
2544
+ return file.isIgnored || file.isPinned;
2545
+ }
2399
2546
  function syncPredicates(config) {
2400
2547
  return {
2401
- isIgnored: (path4) => isIgnored(path4, config),
2402
- isPinned: (path4) => isPinnedForSync(path4, config, config.unpinned),
2548
+ isIgnored: (path) => isIgnored(path, config),
2549
+ isPinned: (path) => isPinnedForSync(path, config, config.unpinned),
2403
2550
  hard: config.hard
2404
2551
  };
2405
2552
  }
@@ -2418,6 +2565,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
2418
2565
  for (const file of analyzedFiles) {
2419
2566
  const { path: filePath, status, isPinned: pinned, isIgnored: ignored, existsInFork } = file;
2420
2567
  if (status === "identical" || status === "ahead") continue;
2568
+ if (!isProtectedFile(file)) continue;
2421
2569
  if (ignored && existsInFork) {
2422
2570
  batchRestorePaths.push(filePath);
2423
2571
  } else if (ignored && !existsInFork) {
@@ -2429,12 +2577,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
2429
2577
  }
2430
2578
  }
2431
2579
  onProgress?.("starting merge in fork...");
2432
- await withTemporarySyncBaseGraft(
2433
- forkPath,
2434
- "HEAD",
2435
- mergeBaseRef,
2436
- () => merge(forkPath, upstreamRef, { noCommit: true, noEdit: true })
2437
- );
2580
+ await withTemporarySyncBaseGraft(forkPath, "HEAD", mergeBaseRef, () => merge(forkPath, upstreamRef));
2438
2581
  if (batchRestorePaths.length > 0) {
2439
2582
  onProgress?.(`batch restoring ${batchRestorePaths.length} pinned/ignored files...`);
2440
2583
  await batchRestoreToHead(forkPath, batchRestorePaths);
@@ -2447,29 +2590,11 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
2447
2590
  }
2448
2591
  }
2449
2592
  for (const file of analyzedFiles) {
2450
- const { path: filePath, status, isPinned: pinned, isIgnored: ignored, existsInFork } = file;
2593
+ const { path: filePath, status, existsInFork } = file;
2451
2594
  if (status === "identical" || status === "ahead") {
2452
2595
  continue;
2453
2596
  }
2454
- if (ignored) continue;
2455
- if (pinned && !file.renamedFrom) continue;
2456
- if (pinned && file.renamedFrom) {
2457
- const oldPathExists = await fileExistsInWorktree(forkPath, file.renamedFrom);
2458
- if (oldPathExists) {
2459
- onProgress?.(`\u2192 ${file.renamedFrom} \u2192 ${filePath}: moving fork content (pinned rename)`);
2460
- try {
2461
- await gitMv(forkPath, file.renamedFrom, filePath);
2462
- } catch {
2463
- await removeFileFully(forkPath, file.renamedFrom);
2464
- await checkoutFromRef(forkPath, "HEAD", file.renamedFrom).catch(() => {
2465
- });
2466
- }
2467
- } else {
2468
- onProgress?.(`\u2192 ${filePath}: keeping fork content (pinned rename, old path removed)`);
2469
- await restoreToHead(forkPath, filePath);
2470
- }
2471
- continue;
2472
- }
2597
+ if (isProtectedFile(file)) continue;
2473
2598
  if (status === "diverged") {
2474
2599
  onProgress?.(`\u2192 ${filePath}: using git merge result (diverged)`);
2475
2600
  continue;
@@ -2514,6 +2639,7 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
2514
2639
  }
2515
2640
  }
2516
2641
  const protectedConflicts = analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path);
2642
+ const protectedConflictSet = new Set(protectedConflicts);
2517
2643
  const gitConflicts = await getConflictedFiles(forkPath);
2518
2644
  for (const filePath of gitConflicts) {
2519
2645
  const isProtected = isIgnored(filePath, config) || isPinnedForSync(filePath, config, config.unpinned);
@@ -2521,7 +2647,10 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
2521
2647
  if (await fileExistsAtRef(forkPath, "HEAD", filePath)) {
2522
2648
  onProgress?.(`\u2192 ${filePath}: keeping fork (protected conflict)`);
2523
2649
  await restoreToHead(forkPath, filePath);
2524
- if (!protectedConflicts.includes(filePath)) protectedConflicts.push(filePath);
2650
+ if (!protectedConflictSet.has(filePath)) {
2651
+ protectedConflictSet.add(filePath);
2652
+ protectedConflicts.push(filePath);
2653
+ }
2525
2654
  } else {
2526
2655
  onProgress?.(`\u2192 ${filePath}: removing (protected conflict, not in fork)`);
2527
2656
  await removeFileFully(forkPath, filePath);
@@ -2553,8 +2682,7 @@ function calculateSummary(files) {
2553
2682
  pinned: 0,
2554
2683
  ignored: 0,
2555
2684
  deleted: 0,
2556
- renamed: 0,
2557
- total: files.length
2685
+ renamed: 0
2558
2686
  };
2559
2687
  for (const file of files) {
2560
2688
  if (file.status !== "identical" && isManagedFile(file.path)) {
@@ -2574,12 +2702,12 @@ async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
2574
2702
  const resolved = await resolveUpstreamCommit(forkPath, remoteName, ref);
2575
2703
  if (!resolved) {
2576
2704
  throw new Error(
2577
- `--ref '${ref}' does not resolve to a commit. Pass a commit sha, a release tag (v*) or a branch on ${config.settings.upstreamUrl}.`
2705
+ `--ref '${ref}' does not resolve to a commit. Pass a commit sha, a release tag or a branch on ${config.settings.upstreamUrl}.`
2578
2706
  );
2579
2707
  }
2580
2708
  if (!await isPublishedUpstream(forkPath, remoteName, branchRef, resolved.sha)) {
2581
2709
  throw new Error(
2582
- `--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release (v*). Sync only to commits upstream has published there.`
2710
+ `--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release. Sync only to commits upstream has published there.`
2583
2711
  );
2584
2712
  }
2585
2713
  return resolved.release ? { ref: resolved.release.ref, releaseTag: resolved.release.tag } : { ref: resolved.sha };
@@ -2600,7 +2728,7 @@ async function prepareUpstream(config, onProgress, onStep) {
2600
2728
  const track = config.track ?? configTrack;
2601
2729
  await ensureRemote(forkPath, remoteName, config.settings.upstreamUrl);
2602
2730
  onProgress?.(`fetching upstream (${remoteName})...`);
2603
- await fetch2(forkPath, remoteName);
2731
+ await fetchRemote(forkPath, remoteName);
2604
2732
  let upstreamRef = branchRef;
2605
2733
  let releaseTag;
2606
2734
  if (config.ref) {
@@ -2613,7 +2741,7 @@ async function prepareUpstream(config, onProgress, onStep) {
2613
2741
  const latest = await resolveLatestReleaseTag(forkPath, remoteName);
2614
2742
  if (!latest) {
2615
2743
  throw new Error(
2616
- `no upstream releases (v*) found on '${remoteName}'. Set upstreamTrack: 'branch' to follow ${branchRef}, or check that ${config.settings.upstreamUrl} publishes release tags.`
2744
+ `no upstream release tags found on '${remoteName}'. Set upstreamTrack: 'branch' to follow ${branchRef}, or check that ${config.settings.upstreamUrl} publishes release tags.`
2617
2745
  );
2618
2746
  }
2619
2747
  upstreamRef = latest.ref;
@@ -2638,21 +2766,23 @@ async function prepareUpstream(config, onProgress, onStep) {
2638
2766
  getShortSha(forkPath, syncPoint)
2639
2767
  ]);
2640
2768
  const hint = config.ref ? "Pick a newer --ref." : releaseTag ? `Nothing to sync until a release past ${syncShort}; to follow the tip instead, run with --track branch.` : `Nothing to sync until '${branchRef}' moves past ${syncShort}.`;
2641
- throw new Error(
2769
+ throw new BehindSyncPointError(
2642
2770
  `upstream ${upstreamLabel} (${upstreamShort}) is behind the last sync point ${syncShort}: the fork already has it, and syncing it would revert the upstream changes after it.
2643
2771
  ${hint}`
2644
2772
  );
2645
2773
  }
2646
2774
  const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
2647
- const commitListMax = 50;
2648
- const commitSkip = commitCount > commitListMax ? commitCount - commitListMax : 0;
2649
- const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, {
2650
- oldestFirst: true,
2651
- skip: commitSkip,
2652
- limit: commitListMax
2653
- }) : [];
2775
+ const commitSkip = commitCount > COMMIT_LIST_MAX ? commitCount - COMMIT_LIST_MAX : 0;
2776
+ const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, { skip: commitSkip, limit: COMMIT_LIST_MAX }) : [];
2654
2777
  onStep?.("fetched upstream", formatFetchedUpstreamDetail(commitCount, upstreamCommits, upstreamGitHubUrl));
2655
- return { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit, upstreamCommits };
2778
+ return { upstreamRef, releaseTag, mergeBase, syncPoint, upstreamGitHubUrl, upstreamCommit, upstreamCommits };
2779
+ }
2780
+ async function assertSyncConfigFollowed(config, ctx) {
2781
+ if (config.keepConfig) return;
2782
+ const report = await compareUpstreamOverrides(config.forkPath, ctx.syncPoint, ctx.upstreamRef, config).catch(
2783
+ () => void 0
2784
+ );
2785
+ if (report?.kind === "changes") throw new UpstreamConfigChangedError(report);
2656
2786
  }
2657
2787
  function resultMeta(config, ctx) {
2658
2788
  return {
@@ -2664,7 +2794,7 @@ function resultMeta(config, ctx) {
2664
2794
  upstreamCommits: ctx.upstreamCommits
2665
2795
  };
2666
2796
  }
2667
- async function upstreamOnlyReports(config, ctx, files) {
2797
+ async function upstreamOnlyReports(config, ctx, files, protectedConflicts) {
2668
2798
  const { forkPath } = config;
2669
2799
  try {
2670
2800
  const [baseSha, upstreamSha, upstreamOverrides] = await Promise.all([
@@ -2676,7 +2806,8 @@ async function upstreamOnlyReports(config, ctx, files) {
2676
2806
  return {
2677
2807
  upstreamDiffRange: `${baseSha}..${upstreamSha}`,
2678
2808
  upstreamOverrides,
2679
- ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? [])
2809
+ ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? []),
2810
+ protectedUpstreamChanges: groupProtectedUpstreamChanges(protectedConflicts, config)
2680
2811
  };
2681
2812
  } catch {
2682
2813
  return {};
@@ -2685,12 +2816,10 @@ async function upstreamOnlyReports(config, ctx, files) {
2685
2816
  async function readNotesInRange(config, ctx) {
2686
2817
  const { forkPath } = config;
2687
2818
  try {
2688
- const aggressive = config.hard === true || config.unpinned === true;
2689
- const syncBase = aggressive ? (await getEffectiveMergeBase(forkPath, "HEAD", ctx.upstreamRef)).base : ctx.mergeBase;
2690
2819
  const [total, arrived, pending] = await Promise.all([
2691
2820
  listNoteIds(forkPath, ctx.upstreamRef),
2692
- arrivedNoteIds(forkPath, syncBase, ctx.upstreamRef),
2693
- readPending(forkPath, syncBase)
2821
+ arrivedNoteIds(forkPath, ctx.syncPoint, ctx.upstreamRef),
2822
+ readPending(forkPath, ctx.syncPoint)
2694
2823
  ]);
2695
2824
  return { total: total.length, arrived, pending: [.../* @__PURE__ */ new Set([...pending, ...arrived])] };
2696
2825
  } catch {
@@ -2701,14 +2830,15 @@ function notesResult(notes) {
2701
2830
  return notes ? { migrationNotes: { total: notes.total, arrived: notes.arrived, open: notes.pending.length } } : {};
2702
2831
  }
2703
2832
  async function removeUpstreamOnlyCopy(forkPath, onStep) {
2704
- if (!existsSync7(join11(forkPath, NOTES_DIR))) return;
2833
+ if (!existsSync7(join10(forkPath, NOTES_DIR))) return;
2705
2834
  await git(["rm", "-r", "-q", "--ignore-unmatch", "--", NOTES_DIR], forkPath);
2706
- await rm2(join11(forkPath, NOTES_DIR), { recursive: true, force: true });
2835
+ await rm2(join10(forkPath, NOTES_DIR), { recursive: true, force: true });
2707
2836
  onStep?.("migration notes", `removed ${NOTES_DIR}/: notes stay upstream, list them with pnpm cella migrate`);
2708
2837
  }
2709
2838
  async function runSyncMerge(config, ctx, onProgress, onStep) {
2710
2839
  const { forkPath } = config;
2711
2840
  const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
2841
+ await assertSyncConfigFollowed(config, ctx);
2712
2842
  const notes = await readNotesInRange(config, ctx);
2713
2843
  const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
2714
2844
  forkPath,
@@ -2719,9 +2849,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
2719
2849
  );
2720
2850
  const summary = calculateSummary(analyzedFiles);
2721
2851
  const synced = analyzedFiles.filter((file) => ["behind", "diverged", "renamed"].includes(file.status)).length;
2722
- const totalResolved = analyzedFiles.filter(
2723
- (file) => ["behind", "diverged", "renamed", "ignored", "pinned"].includes(file.status)
2724
- ).length;
2852
+ const totalResolved = analyzedFiles.filter((file) => SYNC_APPLIED_STATUSES.includes(file.status)).length;
2725
2853
  const syncManifest = {
2726
2854
  upstream: {
2727
2855
  repo: upstreamGitHubUrl ? upstreamGitHubUrl.replace("https://github.com/", "") : void 0,
@@ -2764,7 +2892,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
2764
2892
  autoMergedFiles,
2765
2893
  protectedConflicts,
2766
2894
  ...resultMeta(config, ctx),
2767
- ...await upstreamOnlyReports(config, ctx, analyzedFiles),
2895
+ ...await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts),
2768
2896
  ...notesResult(notes)
2769
2897
  };
2770
2898
  }
@@ -2776,12 +2904,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
2776
2904
  await createWorktree(forkPath, worktreePath, "HEAD");
2777
2905
  onStep?.("worktree created", worktreePath);
2778
2906
  onProgress?.("performing merge in worktree...");
2779
- await withTemporarySyncBaseGraft(
2780
- worktreePath,
2781
- "HEAD",
2782
- ctx.mergeBase,
2783
- () => merge(worktreePath, ctx.upstreamRef, { noCommit: true, noEdit: true })
2784
- );
2907
+ await withTemporarySyncBaseGraft(worktreePath, "HEAD", ctx.mergeBase, () => merge(worktreePath, ctx.upstreamRef));
2785
2908
  onStep?.("merge complete", "upstream merged into worktree");
2786
2909
  onProgress?.("analyzing files...");
2787
2910
  const analyzedFiles = await analyzeRefs(
@@ -2793,8 +2916,9 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
2793
2916
  onProgress
2794
2917
  );
2795
2918
  await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
2796
- const reports = await upstreamOnlyReports(config, ctx, analyzedFiles);
2797
- onStep?.("analysis complete", `${analyzedFiles.length} files analyzed, dry run \u2014 no changes applied`);
2919
+ const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
2920
+ const reports = await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts);
2921
+ onStep?.("analysis complete", `${analyzedFiles.length} files analyzed, dry run, no changes applied`);
2798
2922
  onProgress?.("cleaning up worktree...");
2799
2923
  await cleanupWorktree(forkPath, worktreePath);
2800
2924
  return {
@@ -2803,7 +2927,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
2803
2927
  summary: calculateSummary(analyzedFiles),
2804
2928
  // For analyze mode, count diverged files as potential conflicts
2805
2929
  conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
2806
- protectedConflicts: analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path),
2930
+ protectedConflicts,
2807
2931
  ...resultMeta(config, ctx),
2808
2932
  ...reports,
2809
2933
  ...notesResult(notes)
@@ -2823,23 +2947,39 @@ async function runMergeEngine(config, options) {
2823
2947
  throw error;
2824
2948
  }
2825
2949
  }
2950
+ async function runEngineWithSpinner(config, apply) {
2951
+ return runMergeEngine(config, {
2952
+ apply,
2953
+ onProgress: (message) => {
2954
+ spinnerText(message);
2955
+ },
2956
+ onStep: (label, detail) => {
2957
+ spinnerSuccess(label, detail);
2958
+ createSpinner("...");
2959
+ }
2960
+ });
2961
+ }
2826
2962
 
2827
2963
  // src/services/migrate.ts
2828
- import { execFileSync as execFileSync2 } from "child_process";
2964
+ import { execFileSync as execFileSync2, spawnSync } from "child_process";
2829
2965
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
2830
- import { dirname as dirname4, join as join12 } from "path";
2966
+ import { dirname as dirname4, join as join11 } from "path";
2831
2967
  var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
2968
+ var RESTORED_LIST_MAX = 20;
2969
+ async function ensureUpstreamCommit(config, sha) {
2970
+ const { forkPath } = config;
2971
+ const present = async () => !!await git(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`], forkPath, { ignoreErrors: true });
2972
+ if (await present()) return;
2973
+ await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
2974
+ await fetchRemote(forkPath, DEFAULT_UPSTREAM_REMOTE);
2975
+ if (!await present())
2976
+ throw new Error(`upstream commit ${sha.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
2977
+ }
2832
2978
  async function resolveNotesRef(config) {
2833
2979
  const { forkPath } = config;
2834
2980
  const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
2835
2981
  if (!ref) return null;
2836
- const present = async () => !!await git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], forkPath, { ignoreErrors: true });
2837
- if (!await present()) {
2838
- await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
2839
- await fetch2(forkPath, DEFAULT_UPSTREAM_REMOTE);
2840
- if (!await present())
2841
- throw new Error(`upstream commit ${ref.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
2842
- }
2982
+ await ensureUpstreamCommit(config, ref);
2843
2983
  return ref;
2844
2984
  }
2845
2985
  async function readNotesStatus(config) {
@@ -2881,27 +3021,88 @@ function printNote(index, note, url) {
2881
3021
  if (note.summary) console.info(` ${note.summary}`);
2882
3022
  console.info(colors_default.dim(` read: pnpm cella migrate --show ${note.id}`));
2883
3023
  if (url) console.info(colors_default.dim(` ${url}`));
2884
- if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --extract ${note.id}`));
3024
+ if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --run ${note.id}`));
2885
3025
  console.info();
2886
3026
  }
2887
- async function extractNote(config, ref, id) {
3027
+ async function writeNoteFolder(config, ref, id) {
2888
3028
  const { forkPath } = config;
2889
3029
  const note = await readNote(forkPath, ref, id);
2890
3030
  if (!note) throw new Error(`no migration note '${id}' at upstream ${ref.slice(0, 7)}`);
2891
3031
  const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${NOTES_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
2892
- const target = join12(EXTRACT_DIR, id);
3032
+ const target = join11(EXTRACT_DIR, id);
2893
3033
  for (const file of files) {
2894
3034
  const content = execFileSync2("git", ["show", `${ref}:${NOTES_DIR}/${id}/${file}`], {
2895
3035
  cwd: forkPath,
2896
- maxBuffer: 50 * 1024 * 1024
3036
+ maxBuffer: MAX_BUFFER
2897
3037
  });
2898
- const path4 = join12(forkPath, target, file);
2899
- await mkdir4(dirname4(path4), { recursive: true });
2900
- await writeFile3(path4, content);
3038
+ const path = join11(forkPath, target, file);
3039
+ await mkdir4(dirname4(path), { recursive: true });
3040
+ await writeFile3(path, content);
2901
3041
  }
2902
- console.info(`${colors_default.green("\u2713")} extracted ${files.length} file(s) to ${target}/`);
3042
+ return { note, target, files };
3043
+ }
3044
+ async function extractNote(config, ref, id) {
3045
+ const { note, target, files } = await writeNoteFolder(config, ref, id);
3046
+ console.info(`${checkMark} extracted ${files.length} file(s) to ${target}/`);
2903
3047
  console.info(colors_default.dim(` the README's ${NOTES_DIR}/${id}/ paths are this folder.`));
2904
- if (note.codemod) console.info(colors_default.dim(` pnpm exec tsx ${target}/${note.codemod} inventory ${note.roots.join(" ")}`));
3048
+ if (note.codemod) {
3049
+ console.info(colors_default.dim(` pnpm exec tsx ${target}/${note.codemod} inventory ${note.roots.join(" ")}`));
3050
+ console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella migrate --run ${id}`));
3051
+ }
3052
+ }
3053
+ function resolveCodemod(id, files, script) {
3054
+ if (script) {
3055
+ const name = script.replace(/^\.\//, "");
3056
+ if (!files.includes(name)) {
3057
+ throw new Error(`migration note '${id}' has no file '${script}'. its files: ${files.join(", ")}`);
3058
+ }
3059
+ return name;
3060
+ }
3061
+ const scripts = files.filter((file) => !file.includes("/") && isCodemodFile(file));
3062
+ if (scripts.length === 0) {
3063
+ throw new Error(
3064
+ `migration note '${id}' ships no codemod script: its steps are manual, read them with \`pnpm cella migrate --show ${id}\`.`
3065
+ );
3066
+ }
3067
+ if (scripts.length > 1) {
3068
+ throw new Error(
3069
+ `migration note '${id}' ships ${scripts.length} scripts (${scripts.join(", ")}): name the one to run with --script <file>.`
3070
+ );
3071
+ }
3072
+ return scripts[0];
3073
+ }
3074
+ async function runNote(config, ref, id) {
3075
+ const { forkPath } = config;
3076
+ const { note, target, files } = await writeNoteFolder(config, ref, id);
3077
+ const script = join11(target, resolveCodemod(id, files, config.script));
3078
+ const args = config.runArgs?.length ? config.runArgs : ["inventory", ...note.roots];
3079
+ const upstreamCommit = await readManifestBase(forkPath) ?? ref;
3080
+ await ensureUpstreamCommit(config, upstreamCommit);
3081
+ const identical = await listIdenticalToRef(forkPath, upstreamCommit);
3082
+ console.info(colors_default.dim(`running pnpm exec tsx ${script} ${args.join(" ")}`));
3083
+ console.info();
3084
+ const child = spawnSync("pnpm", ["exec", "tsx", script, ...args], { cwd: forkPath, stdio: "inherit" });
3085
+ if (child.error) throw new Error(`could not run the codemod: ${child.error.message}`);
3086
+ const stillIdentical = await listIdenticalToRef(forkPath, upstreamCommit);
3087
+ const restored = [...identical].filter((path) => !stillIdentical.has(path)).sort();
3088
+ await batchRestoreWorktreeFromRef(forkPath, upstreamCommit, restored);
3089
+ const upstreamShort = upstreamCommit.slice(0, 9);
3090
+ console.info();
3091
+ if (restored.length > 0) {
3092
+ console.info(`${checkMark} restored ${restored.length} file(s) to upstream ${upstreamShort}`);
3093
+ console.info(colors_default.dim(" the codemod changed them, and they were identical to upstream before the run:"));
3094
+ for (const path of restored.slice(0, RESTORED_LIST_MAX)) console.info(colors_default.dim(` ${path}`));
3095
+ if (restored.length > RESTORED_LIST_MAX) {
3096
+ console.info(colors_default.dim(` ... + ${restored.length - RESTORED_LIST_MAX} more`));
3097
+ }
3098
+ } else {
3099
+ console.info(colors_default.dim(`no file identical to upstream ${upstreamShort} was changed by the codemod.`));
3100
+ }
3101
+ if (child.status !== 0) throw new Error(`the codemod exited with code ${child.status ?? "unknown"}`);
3102
+ if (!config.runArgs?.length) {
3103
+ const apply = ["rewrite", ...note.roots].join(" ");
3104
+ console.info(colors_default.dim(`report only. to apply: pnpm cella migrate --run ${id} -- ${apply}`));
3105
+ }
2905
3106
  }
2906
3107
  async function markNotes(config, ref, pending, ids) {
2907
3108
  const open = new Set(pending);
@@ -2909,7 +3110,7 @@ async function markNotes(config, ref, pending, ids) {
2909
3110
  for (const id of ids) open.delete(id);
2910
3111
  await writePending(config.forkPath, [...open]);
2911
3112
  const marked = ids.length - unknown.length;
2912
- if (marked > 0) console.info(`${colors_default.green("\u2713")} recorded ${marked} note(s) as handled`);
3113
+ if (marked > 0) console.info(`${checkMark} recorded ${marked} note(s) as handled`);
2913
3114
  if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
2914
3115
  console.info(colors_default.dim(formatNotesLine((await listNoteIds(config.forkPath, ref)).length, open.size)));
2915
3116
  }
@@ -2924,6 +3125,7 @@ async function runMigrate(config) {
2924
3125
  const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
2925
3126
  const urlOf = (id) => noteUrl(githubUrl, ref, id);
2926
3127
  if (config.mark?.length) return markNotes(config, ref, pending, config.mark);
3128
+ if (config.run) return runNote(config, ref, config.run);
2927
3129
  if (config.extract) return extractNote(config, ref, config.extract);
2928
3130
  if (config.show) {
2929
3131
  const source = await git(["show", `${ref}:${NOTES_DIR}/${config.show}/README.md`], forkPath, {
@@ -2997,16 +3199,7 @@ function printUnifiedDiff(config, filePath) {
2997
3199
  }
2998
3200
  async function runAnalyze(config) {
2999
3201
  createSpinner("starting analysis...");
3000
- const result = await runMergeEngine(config, {
3001
- apply: false,
3002
- onProgress: (message) => {
3003
- spinnerText(message);
3004
- },
3005
- onStep: (label, detail) => {
3006
- spinnerSuccess(label, detail);
3007
- createSpinner("...");
3008
- }
3009
- });
3202
+ const result = await runEngineWithSpinner(config, false);
3010
3203
  spinnerSuccess();
3011
3204
  const scopedFiles = filterByScope(result, config.scope ?? "all");
3012
3205
  if (config.diff) {
@@ -3028,15 +3221,15 @@ async function runAnalyze(config) {
3028
3221
  upstreamChangedLines: f.upstreamChangedLines ?? null,
3029
3222
  upstreamLinesAbsent: f.upstreamLinesAbsent ?? null,
3030
3223
  upstreamOnly: f.upstreamOnly ?? false,
3031
- // Only on the sync config: upstream override entries the fork config does not follow
3032
- upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored } : null
3224
+ // Only on the sync config: upstream override entries and packageJsonSync keys the fork config does not follow
3225
+ upstreamOverrides: overrides && f.path === CONFIG_FILE ? { pinned: overrides.pinned, ignored: overrides.ignored, packageJsonSync: overrides.packageJsonSync } : null
3033
3226
  }));
3034
3227
  writeStdout(JSON.stringify(out, null, 2));
3035
3228
  return result;
3036
3229
  }
3037
3230
  if (config.list) {
3038
3231
  for (const file of scopedFiles) {
3039
- console.info(file.path);
3232
+ writeStdout(file.path);
3040
3233
  }
3041
3234
  return result;
3042
3235
  }
@@ -3045,7 +3238,7 @@ async function runAnalyze(config) {
3045
3238
  if (!file) throw new Error(`file not found in analysis results: ${config.openDiff}`);
3046
3239
  const patch = gitDiffFile(config.forkPath, config.upstreamRef, file.path);
3047
3240
  if (patch.length === 0) {
3048
- console.info(`${colors_default.green("\u2713")} ${file.path} is identical to upstream`);
3241
+ console.info(`${checkMark} ${file.path} is identical to upstream`);
3049
3242
  return result;
3050
3243
  }
3051
3244
  const pagePath = await openDiffInBrowser(
@@ -3058,7 +3251,7 @@ async function runAnalyze(config) {
3058
3251
  },
3059
3252
  config.forkPath
3060
3253
  );
3061
- console.info(`${colors_default.green("\u2713")} opened browser diff for ${file.path}`);
3254
+ console.info(`${checkMark} opened browser diff for ${file.path}`);
3062
3255
  console.info(colors_default.dim(` ${pagePath}`));
3063
3256
  return result;
3064
3257
  }
@@ -3068,41 +3261,34 @@ async function runAnalyze(config) {
3068
3261
  fileLinkMode: config.settings.fileLinkMode,
3069
3262
  forkPath: config.forkPath
3070
3263
  };
3071
- printAnalysisFileGroups(result.files, linkOptions);
3072
- printSummary(result.summary, "analysis summary");
3073
- printIgnoredUpstreamChanges(result);
3074
- printUpstreamOverrideChanges(result);
3075
- printMaskingPinWarning(result.files);
3264
+ printAnalysisFileGroups(result.files, linkOptions, result);
3265
+ printEngineReports(result, "analysis summary");
3076
3266
  await printMigrationNotesLine(config, result);
3077
- if (config.logFile) {
3078
- const logPath = writeLogFile(config.forkPath, result.files);
3079
- console.info();
3080
- console.info(colors_default.dim(`full file list written to: ${logPath}`));
3081
- }
3267
+ printLogFileReport(config, result.files);
3082
3268
  console.info();
3083
3269
  return result;
3084
3270
  }
3085
3271
 
3086
3272
  // src/services/audit.ts
3087
- import { spawnSync } from "child_process";
3088
- import * as fs2 from "fs";
3089
- import * as path2 from "path";
3273
+ import { spawnSync as spawnSync2 } from "child_process";
3274
+ import { readFileSync as readFileSync3 } from "fs";
3275
+ import { basename as basename5, join as join13, relative as relative2 } from "path";
3090
3276
  import { checkbox, confirm, Separator } from "@inquirer/prompts";
3091
3277
 
3092
3278
  // src/utils/audit-utils.ts
3093
3279
  import { execFile as execFile2 } from "child_process";
3094
- import * as fs from "fs";
3095
- import * as path from "path";
3280
+ import { existsSync as existsSync8, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
3281
+ import { join as join12 } from "path";
3096
3282
  import { promisify as promisify2 } from "util";
3097
3283
  var execFileAsync2 = promisify2(execFile2);
3098
- var CACHE_FILE = path.join(import.meta.dirname, "..", "..", ".audit.cache.json");
3284
+ var CACHE_FILE = join12(import.meta.dirname, "..", "..", ".audit.cache.json");
3099
3285
  var CACHE_TTL = 7 * 24 * 60 * 60 * 1e3;
3100
3286
  var CHANGELOG_PATHS = ["CHANGELOG.md", "CHANGELOG", "changelog.md", "HISTORY.md", "CHANGES.md", "NEWS.md"];
3101
3287
  var DEFAULT_BRANCHES = ["main", "master"];
3102
3288
  function loadCache() {
3103
3289
  try {
3104
- if (fs.existsSync(CACHE_FILE)) {
3105
- return JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
3290
+ if (existsSync8(CACHE_FILE)) {
3291
+ return JSON.parse(readFileSync2(CACHE_FILE, "utf8"));
3106
3292
  }
3107
3293
  } catch {
3108
3294
  }
@@ -3110,15 +3296,15 @@ function loadCache() {
3110
3296
  }
3111
3297
  function saveCache(cache) {
3112
3298
  try {
3113
- fs.writeFileSync(CACHE_FILE, `${JSON.stringify(cache, null, 2)}
3299
+ writeFileSync3(CACHE_FILE, `${JSON.stringify(cache, null, 2)}
3114
3300
  `);
3115
3301
  } catch {
3116
3302
  }
3117
3303
  }
3118
3304
  function clearCache() {
3119
3305
  try {
3120
- if (fs.existsSync(CACHE_FILE)) {
3121
- fs.unlinkSync(CACHE_FILE);
3306
+ if (existsSync8(CACHE_FILE)) {
3307
+ unlinkSync(CACHE_FILE);
3122
3308
  console.info(colors_default.green("\u2713 cache cleared successfully"));
3123
3309
  } else {
3124
3310
  console.info(colors_default.yellow("no cache file found"));
@@ -3206,56 +3392,32 @@ function isMajorVersionChange(current, latest) {
3206
3392
  if (!currentMajor || !latestMajor) return false;
3207
3393
  return Number.parseInt(latestMajor, 10) > Number.parseInt(currentMajor, 10);
3208
3394
  }
3209
- async function getOutdatedPackages(cwd) {
3395
+ async function runPnpmJson(args, cwd) {
3396
+ const parse = (stdout) => {
3397
+ if (!stdout || stdout.trim() === "") return null;
3398
+ try {
3399
+ return JSON.parse(stdout);
3400
+ } catch {
3401
+ return null;
3402
+ }
3403
+ };
3210
3404
  try {
3211
- const { stdout } = await execFileAsync2("pnpm", ["-r", "outdated", "--json"], {
3212
- encoding: "utf-8",
3405
+ const { stdout } = await execFileAsync2("pnpm", args, {
3406
+ encoding: "utf8",
3213
3407
  maxBuffer: 10 * 1024 * 1024,
3214
3408
  // 10MB buffer for large outputs
3215
3409
  cwd
3216
3410
  });
3217
- if (!stdout || stdout.trim() === "") {
3218
- return {};
3219
- }
3220
- return JSON.parse(stdout);
3411
+ return parse(stdout);
3221
3412
  } catch (error) {
3222
- if (error instanceof Error && "stdout" in error) {
3223
- const stdout = error.stdout;
3224
- if (stdout?.trim()) {
3225
- try {
3226
- return JSON.parse(stdout);
3227
- } catch {
3228
- return {};
3229
- }
3230
- }
3231
- }
3232
- return {};
3413
+ return parse(error instanceof Error && "stdout" in error ? error.stdout : void 0);
3233
3414
  }
3234
3415
  }
3416
+ async function getOutdatedPackages(cwd) {
3417
+ return await runPnpmJson(["-r", "outdated", "--json"], cwd) ?? {};
3418
+ }
3235
3419
  async function runPnpmAudit(cwd) {
3236
- try {
3237
- const { stdout } = await execFileAsync2("pnpm", ["audit", "--json"], {
3238
- encoding: "utf-8",
3239
- maxBuffer: 10 * 1024 * 1024,
3240
- cwd
3241
- });
3242
- if (!stdout || stdout.trim() === "") {
3243
- return null;
3244
- }
3245
- return JSON.parse(stdout);
3246
- } catch (error) {
3247
- if (error instanceof Error && "stdout" in error) {
3248
- const stdout = error.stdout;
3249
- if (stdout?.trim()) {
3250
- try {
3251
- return JSON.parse(stdout);
3252
- } catch {
3253
- return null;
3254
- }
3255
- }
3256
- }
3257
- return null;
3258
- }
3420
+ return await runPnpmJson(["audit", "--json"], cwd);
3259
3421
  }
3260
3422
  function parseDependencyPath(paths) {
3261
3423
  for (const pathStr of paths) {
@@ -3277,12 +3439,9 @@ function buildVulnerabilityMap(auditResult) {
3277
3439
  const { workspace, directDependency } = parseDependencyPath(allPaths);
3278
3440
  const existing = map.get(advisory.module_name) || [];
3279
3441
  existing.push({
3280
- id: advisory.id,
3281
3442
  title: advisory.title,
3282
3443
  severity: advisory.severity,
3283
- url: advisory.url,
3284
3444
  vulnerableVersions: advisory.vulnerable_versions,
3285
- patchedVersions: advisory.patched_versions,
3286
3445
  cves: advisory.cves || [],
3287
3446
  workspace,
3288
3447
  directDependency
@@ -3291,19 +3450,15 @@ function buildVulnerabilityMap(auditResult) {
3291
3450
  }
3292
3451
  return map;
3293
3452
  }
3453
+ var vulnIcons = {
3454
+ critical: colors_default.red("\u25CF"),
3455
+ high: colors_default.red("\u25CF"),
3456
+ moderate: colors_default.yellow("\u25CF"),
3457
+ low: colors_default.blue("\u25CF"),
3458
+ info: colors_default.gray("\u25CF")
3459
+ };
3294
3460
  function getVulnIcon(severity) {
3295
- switch (severity) {
3296
- case "critical":
3297
- return colors_default.red("\u25CF");
3298
- case "high":
3299
- return colors_default.red("\u25CF");
3300
- case "moderate":
3301
- return colors_default.yellow("\u25CF");
3302
- case "low":
3303
- return colors_default.blue("\u25CF");
3304
- default:
3305
- return colors_default.gray("\u25CF");
3306
- }
3461
+ return vulnIcons[severity] ?? colors_default.gray("\u25CF");
3307
3462
  }
3308
3463
  function getHighestSeverity(vulns) {
3309
3464
  if (vulns.length === 0) return null;
@@ -3313,13 +3468,13 @@ function getHighestSeverity(vulns) {
3313
3468
  }
3314
3469
  return null;
3315
3470
  }
3316
- function middleTruncate(str, maxLen) {
3317
- if (str.length <= maxLen) return str;
3471
+ function middleTruncate(str2, maxLen) {
3472
+ if (str2.length <= maxLen) return str2;
3318
3473
  const ellipsis = "\u2026";
3319
3474
  const charsToShow = maxLen - ellipsis.length;
3320
3475
  const frontChars = Math.ceil(charsToShow / 2);
3321
3476
  const backChars = Math.floor(charsToShow / 2);
3322
- return str.slice(0, frontChars) + ellipsis + str.slice(-backChars);
3477
+ return str2.slice(0, frontChars) + ellipsis + str2.slice(-backChars);
3323
3478
  }
3324
3479
  function formatDependents(dependents) {
3325
3480
  if (dependents.length === 0) return "";
@@ -3364,18 +3519,42 @@ function parseYamlBlockList(content, key) {
3364
3519
  }
3365
3520
 
3366
3521
  // src/services/audit.ts
3367
- async function runAudit(config, options = {}) {
3522
+ var AUDIT_FIX_VALUE = "__audit_fix__";
3523
+ function createDependentPkgReader() {
3524
+ const cache = /* @__PURE__ */ new Map();
3525
+ return (location) => {
3526
+ let cached = cache.get(location);
3527
+ if (cached === void 0) {
3528
+ try {
3529
+ cached = JSON.parse(readFileSync3(join13(location, "package.json"), "utf8"));
3530
+ } catch {
3531
+ cached = null;
3532
+ }
3533
+ cache.set(location, cached ?? null);
3534
+ }
3535
+ return cached ?? null;
3536
+ };
3537
+ }
3538
+ function formatSeverityCounts(counts, suffix = "") {
3539
+ const parts = [];
3540
+ if (counts.critical) parts.push(colors_default.red(`${counts.critical} critical${suffix}`));
3541
+ if (counts.high) parts.push(colors_default.red(`${counts.high} high${suffix}`));
3542
+ if (counts.moderate) parts.push(colors_default.yellow(`${counts.moderate} moderate${suffix}`));
3543
+ if (counts.low) parts.push(colors_default.blue(`${counts.low} low`));
3544
+ return parts;
3545
+ }
3546
+ async function runAudit(config) {
3368
3547
  const { forkPath } = config;
3369
- if (options.checkOverrides) {
3548
+ if (config.checkOverrides) {
3370
3549
  await checkOverrides(forkPath);
3371
3550
  return;
3372
3551
  }
3373
3552
  const spinner = createSpinner("checking for outdated packages & vulnerabilities...");
3374
3553
  try {
3375
- if (options.force) {
3554
+ if (config.force) {
3376
3555
  spinnerText("clearing pnpm metadata cache...");
3377
3556
  clearCache();
3378
- spawnSync("pnpm", ["cache", "delete", "*"], {
3557
+ spawnSync2("pnpm", ["cache", "delete", "*"], {
3379
3558
  cwd: forkPath,
3380
3559
  stdio: "ignore"
3381
3560
  });
@@ -3389,25 +3568,13 @@ async function runAudit(config, options = {}) {
3389
3568
  return;
3390
3569
  }
3391
3570
  const cache = loadCache();
3392
- spinnerText(`found ${packageNames.length} outdated package(s) - fetching metadata...`);
3393
- const pkgJsonCache = /* @__PURE__ */ new Map();
3394
- const readDependentPkg = (location) => {
3395
- let cached = pkgJsonCache.get(location);
3396
- if (cached === void 0) {
3397
- try {
3398
- cached = JSON.parse(fs2.readFileSync(path2.join(location, "package.json"), "utf-8"));
3399
- } catch {
3400
- cached = null;
3401
- }
3402
- pkgJsonCache.set(location, cached ?? null);
3403
- }
3404
- return cached ?? null;
3405
- };
3571
+ spinnerText(`found ${packageNames.length} outdated package(s), fetching metadata...`);
3572
+ const readDependentPkg = createDependentPkgReader();
3406
3573
  const dependentNameCache = /* @__PURE__ */ new Map();
3407
3574
  for (const name of packageNames) {
3408
3575
  for (const dep of outdatedPackages[name].dependentPackages) {
3409
3576
  if (!dependentNameCache.has(dep.location)) {
3410
- dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name || path2.basename(dep.location));
3577
+ dependentNameCache.set(dep.location, readDependentPkg(dep.location)?.name || basename5(dep.location));
3411
3578
  }
3412
3579
  }
3413
3580
  }
@@ -3518,7 +3685,7 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
3518
3685
  const dependentMap = /* @__PURE__ */ new Map();
3519
3686
  for (const pkg of enhancedPackages) {
3520
3687
  for (const loc of pkg.dependentLocations) {
3521
- const dependentName = dependentNameCache.get(loc) || path2.basename(loc);
3688
+ const dependentName = dependentNameCache.get(loc) || basename5(loc);
3522
3689
  const existing = dependentMap.get(dependentName);
3523
3690
  if (existing) {
3524
3691
  existing.count++;
@@ -3530,34 +3697,15 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
3530
3697
  const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
3531
3698
  const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
3532
3699
  for (const [dependentName, { location, count }] of sortedDependents) {
3533
- const packageJsonPath2 = path2.join(location, "package.json");
3534
- const relativePath = path2.relative(forkPath, packageJsonPath2);
3700
+ const packageJsonPath2 = join13(location, "package.json");
3701
+ const relativePath = relative2(forkPath, packageJsonPath2);
3535
3702
  const paddedName = dependentName.padEnd(maxDependentNameLen);
3536
3703
  console.info(
3537
3704
  ` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count}`)}`
3538
3705
  );
3539
3706
  }
3540
3707
  }
3541
- async function promptForUpdates(packages, forkPath, auditResult) {
3542
- const hasVulnerabilities = auditResult !== null && Object.values(auditResult.metadata?.vulnerabilities || {}).some((c) => c > 0);
3543
- const hasPackages = packages.length > 0;
3544
- let confirmMessage = "update packages?";
3545
- if (hasVulnerabilities && hasPackages) {
3546
- confirmMessage = "update packages and fix vulnerabilities?";
3547
- } else if (hasVulnerabilities) {
3548
- confirmMessage = "fix vulnerabilities?";
3549
- }
3550
- let wantsUpdate;
3551
- try {
3552
- wantsUpdate = await confirm({
3553
- message: confirmMessage,
3554
- default: true
3555
- });
3556
- } catch {
3557
- return;
3558
- }
3559
- if (!wantsUpdate) return;
3560
- console.info();
3708
+ function buildUpdateChoices(packages, auditResult, hasVulnerabilities) {
3561
3709
  const makeChoice = (pkg) => {
3562
3710
  const devTag = pkg.isDev ? colors_default.dim(" (dev)") : "";
3563
3711
  const version = `${colors_default.red(pkg.current)} \u2192 ${colors_default.green(pkg.latest)}`;
@@ -3568,17 +3716,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3568
3716
  checked: false
3569
3717
  };
3570
3718
  };
3571
- const AUDIT_FIX_VALUE = "__audit_fix__";
3572
3719
  const pinned = packages.filter((p) => p.pinnedIn.length > 0);
3573
3720
  const regular = packages.filter((p) => p.pinnedIn.length === 0);
3574
3721
  const choices = [];
3575
3722
  if (hasVulnerabilities) {
3576
- const vulnMeta = auditResult?.metadata?.vulnerabilities || {};
3577
- const parts = [];
3578
- if (vulnMeta.critical) parts.push(colors_default.red(`${vulnMeta.critical} critical`));
3579
- if (vulnMeta.high) parts.push(colors_default.red(`${vulnMeta.high} high`));
3580
- if (vulnMeta.moderate) parts.push(colors_default.yellow(`${vulnMeta.moderate} moderate`));
3581
- if (vulnMeta.low) parts.push(colors_default.blue(`${vulnMeta.low} low`));
3723
+ const parts = formatSeverityCounts(auditResult?.metadata?.vulnerabilities || {});
3582
3724
  const vulnSummary = parts.length > 0 ? ` ${colors_default.dim("\xB7")} ${parts.join(" ")}` : "";
3583
3725
  choices.push(new Separator(colors_default.red(`\u2500\u2500 vulnerabilities${vulnSummary} \u2500\u2500`)));
3584
3726
  choices.push({
@@ -3597,6 +3739,29 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3597
3739
  }
3598
3740
  choices.push(...regular.map(makeChoice));
3599
3741
  }
3742
+ return choices;
3743
+ }
3744
+ async function promptForUpdates(packages, forkPath, auditResult) {
3745
+ const hasVulnerabilities = auditResult !== null && Object.values(auditResult.metadata?.vulnerabilities || {}).some((c) => c > 0);
3746
+ const hasPackages = packages.length > 0;
3747
+ let confirmMessage = "update packages?";
3748
+ if (hasVulnerabilities && hasPackages) {
3749
+ confirmMessage = "update packages and fix vulnerabilities?";
3750
+ } else if (hasVulnerabilities) {
3751
+ confirmMessage = "fix vulnerabilities?";
3752
+ }
3753
+ let wantsUpdate;
3754
+ try {
3755
+ wantsUpdate = await confirm({
3756
+ message: confirmMessage,
3757
+ default: true
3758
+ });
3759
+ } catch {
3760
+ return;
3761
+ }
3762
+ if (!wantsUpdate) return;
3763
+ console.info();
3764
+ const choices = buildUpdateChoices(packages, auditResult, hasVulnerabilities);
3600
3765
  let selected;
3601
3766
  try {
3602
3767
  selected = await checkbox({
@@ -3624,6 +3789,9 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3624
3789
  }
3625
3790
  const runAuditFix = selected.includes(AUDIT_FIX_VALUE);
3626
3791
  const selectedPackages = selected.filter((s) => s !== AUDIT_FIX_VALUE);
3792
+ await executeUpdates(packages, forkPath, selectedPackages, runAuditFix);
3793
+ }
3794
+ async function executeUpdates(packages, forkPath, selectedPackages, runAuditFix) {
3627
3795
  const summaryParts = [];
3628
3796
  if (selectedPackages.length > 0) {
3629
3797
  summaryParts.push(`updating ${selectedPackages.length} package${selectedPackages.length !== 1 ? "s" : ""}`);
@@ -3655,17 +3823,15 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3655
3823
  for (const pkgName of selectedPackages) {
3656
3824
  const pkg = packages.find((p) => p.name === pkgName);
3657
3825
  if (!pkg) continue;
3658
- for (let i = 0; i < pkg.dependents.length; i++) {
3659
- const dependent = pkg.dependents[i];
3660
- if (!workspacePackages.has(dependent)) {
3661
- workspacePackages.set(dependent, { packages: [] });
3662
- }
3663
- workspacePackages.get(dependent)?.packages.push(pkgName);
3826
+ for (const dependent of pkg.dependents) {
3827
+ const group = workspacePackages.get(dependent);
3828
+ if (group) group.push(pkgName);
3829
+ else workspacePackages.set(dependent, [pkgName]);
3664
3830
  }
3665
3831
  }
3666
3832
  if (runAuditFix) {
3667
3833
  console.info(colors_default.dim("running pnpm audit --fix override..."));
3668
- const auditFixResult = spawnSync("pnpm", ["audit", "--fix", "override"], {
3834
+ const auditFixResult = spawnSync2("pnpm", ["audit", "--fix", "override"], {
3669
3835
  cwd: forkPath,
3670
3836
  stdio: "inherit"
3671
3837
  });
@@ -3676,7 +3842,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3676
3842
  console.error(colors_default.red(`\u2717 pnpm audit --fix override failed (exit code ${auditFixResult.status})`));
3677
3843
  }
3678
3844
  }
3679
- for (const [workspace, { packages: pkgs }] of workspacePackages) {
3845
+ for (const [workspace, pkgs] of workspacePackages) {
3680
3846
  const pkgList = pkgs.join(", ");
3681
3847
  console.info(`${colors_default.cyan("\u2191")} ${colors_default.bold(workspace)}: ${colors_default.dim(pkgList)}`);
3682
3848
  }
@@ -3685,7 +3851,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3685
3851
  const selectedCatalogPackages = selectedPackages.filter((pkgName) => catalogPackages.has(pkgName));
3686
3852
  if (selectedCatalogPackages.length > 0) {
3687
3853
  console.info(colors_default.dim(`normalizing catalog consumers: ${selectedCatalogPackages.join(", ")}`));
3688
- const normalizeResult = spawnSync("pnpm", ["-r", "up", ...selectedCatalogPackages], {
3854
+ const normalizeResult = spawnSync2("pnpm", ["-r", "up", ...selectedCatalogPackages], {
3689
3855
  cwd: forkPath,
3690
3856
  stdio: "inherit"
3691
3857
  });
@@ -3695,7 +3861,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3695
3861
  return;
3696
3862
  }
3697
3863
  }
3698
- const result = spawnSync("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
3864
+ const result = spawnSync2("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
3699
3865
  cwd: forkPath,
3700
3866
  stdio: "inherit"
3701
3867
  });
@@ -3710,15 +3876,11 @@ async function promptForUpdates(packages, forkPath, auditResult) {
3710
3876
  function printVulnerabilityResults(auditResult, vulnMap) {
3711
3877
  if (auditResult && vulnMap.size > 0) {
3712
3878
  const vulnMeta = auditResult.metadata?.vulnerabilities || {};
3713
- const criticalCount = vulnMeta.critical || 0;
3714
- const highCount = vulnMeta.high || 0;
3715
- const moderateCount = vulnMeta.moderate || 0;
3716
- const lowCount = vulnMeta.low || 0;
3717
3879
  console.info();
3718
3880
  console.info(
3719
- `${colors_default.red("\u26A0")} ${colors_default.bold("vulnerabilities")} ${colors_default.dim("\xB7")} ` + (criticalCount > 0 ? colors_default.red(`${criticalCount} critical `) : "") + (highCount > 0 ? colors_default.red(`${highCount} high `) : "") + (moderateCount > 0 ? colors_default.yellow(`${moderateCount} moderate `) : "") + (lowCount > 0 ? colors_default.blue(`${lowCount} low`) : "")
3881
+ `${colors_default.red("\u26A0")} ${colors_default.bold("vulnerabilities")} ${colors_default.dim("\xB7")} ${formatSeverityCounts(vulnMeta, " ").join("")}`
3720
3882
  );
3721
- console.info("\u2500".repeat(60));
3883
+ console.info(DIVIDER);
3722
3884
  for (const [pkgName, vulns] of vulnMap.entries()) {
3723
3885
  for (const vuln of vulns) {
3724
3886
  const severityIcon = getVulnIcon(vuln.severity);
@@ -3738,14 +3900,14 @@ function printVulnerabilityResults(auditResult, vulnMap) {
3738
3900
  }
3739
3901
  } else {
3740
3902
  console.info();
3741
- console.info(`${colors_default.green("\u2713")} no vulnerabilities found`);
3903
+ console.info(`${checkMark} no vulnerabilities found`);
3742
3904
  }
3743
3905
  console.info();
3744
3906
  }
3745
3907
  function readCatalogPackageNames(forkPath) {
3746
- const workspacePath = path2.join(forkPath, "pnpm-workspace.yaml");
3908
+ const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
3747
3909
  try {
3748
- const content = fs2.readFileSync(workspacePath, "utf-8");
3910
+ const content = readFileSync3(workspacePath, "utf8");
3749
3911
  return new Set(Object.keys(parseYamlBlockMap(content, "catalog")));
3750
3912
  } catch {
3751
3913
  return /* @__PURE__ */ new Set();
@@ -3753,18 +3915,18 @@ function readCatalogPackageNames(forkPath) {
3753
3915
  }
3754
3916
  function readOverrides(forkPath) {
3755
3917
  const overrides = /* @__PURE__ */ new Map();
3756
- const pkgJsonPath = path2.join(forkPath, "package.json");
3918
+ const pkgJsonPath = join13(forkPath, "package.json");
3757
3919
  try {
3758
- const pkgJson = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf-8"));
3920
+ const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
3759
3921
  const pnpmOverrides = pkgJson.pnpm?.overrides || {};
3760
3922
  for (const [key, value] of Object.entries(pnpmOverrides)) {
3761
3923
  overrides.set(key, { target: value, source: "package.json" });
3762
3924
  }
3763
3925
  } catch {
3764
3926
  }
3765
- const workspacePath = path2.join(forkPath, "pnpm-workspace.yaml");
3927
+ const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
3766
3928
  try {
3767
- const content = fs2.readFileSync(workspacePath, "utf-8");
3929
+ const content = readFileSync3(workspacePath, "utf8");
3768
3930
  const wsOverrides = parseYamlBlockMap(content, "overrides");
3769
3931
  for (const [key, value] of Object.entries(wsOverrides)) {
3770
3932
  overrides.set(key, { target: value, source: "pnpm-workspace.yaml" });
@@ -3807,7 +3969,7 @@ async function checkOverrides(forkPath) {
3807
3969
  const maxTargetLen = Math.max(6, ...allValues.map((v) => v.target.length));
3808
3970
  if (securityOverrides.length > 0) {
3809
3971
  console.info(colors_default.bold(`security overrides (${securityOverrides.length})`));
3810
- console.info("\u2500".repeat(60));
3972
+ console.info(DIVIDER);
3811
3973
  for (const o of securityOverrides) {
3812
3974
  const icon = o.status === "active" ? colors_default.red("\u25CF") : colors_default.green("\u25CB");
3813
3975
  const statusText = o.status === "active" ? colors_default.red("active") : colors_default.green("likely stale");
@@ -3820,7 +3982,7 @@ async function checkOverrides(forkPath) {
3820
3982
  }
3821
3983
  if (pinOverrides.length > 0) {
3822
3984
  console.info(colors_default.bold(`version pins (${pinOverrides.length})`));
3823
- console.info("\u2500".repeat(60));
3985
+ console.info(DIVIDER);
3824
3986
  for (const o of pinOverrides) {
3825
3987
  const keyText = colors_default.white(o.key.padEnd(maxKeyLen));
3826
3988
  const targetText = colors_default.dim(o.target.padEnd(maxTargetLen));
@@ -3833,26 +3995,24 @@ async function checkOverrides(forkPath) {
3833
3995
  const activeCount = securityOverrides.filter((o) => o.status === "active").length;
3834
3996
  if (staleCount > 0) {
3835
3997
  console.info(
3836
- `${colors_default.green("\u25CB")} ${staleCount} override${staleCount !== 1 ? "s" : ""} likely stale \u2014 no matching advisory found in current audit`
3998
+ `${colors_default.green("\u25CB")} ${staleCount} override${staleCount !== 1 ? "s" : ""} likely stale: no matching advisory found in current audit`
3837
3999
  );
3838
- console.info(colors_default.dim(" overrides mask vulnerabilities \u2014 verify by temporarily removing and running pnpm audit"));
4000
+ console.info(colors_default.dim(" overrides mask vulnerabilities: verify by temporarily removing and running pnpm audit"));
3839
4001
  }
3840
4002
  if (activeCount > 0) {
3841
4003
  console.info(
3842
- `${colors_default.red("\u25CF")} ${activeCount} override${activeCount !== 1 ? "s" : ""} still active \u2014 advisory present without override`
4004
+ `${colors_default.red("\u25CF")} ${activeCount} override${activeCount !== 1 ? "s" : ""} still active: advisory present without override`
3843
4005
  );
3844
4006
  }
3845
4007
  if (pinOverrides.length > 0) {
3846
4008
  console.info(
3847
- `${colors_default.blue("\u25C6")} ${pinOverrides.length} version pin${pinOverrides.length !== 1 ? "s" : ""} \u2014 not security-related, review manually`
4009
+ `${colors_default.blue("\u25C6")} ${pinOverrides.length} version pin${pinOverrides.length !== 1 ? "s" : ""}: not security-related, review manually`
3848
4010
  );
3849
4011
  }
3850
4012
  console.info();
3851
4013
  }
3852
4014
 
3853
4015
  // src/services/contributions.ts
3854
- import { readFileSync as readFileSync4 } from "fs";
3855
- import { join as join16 } from "path";
3856
4016
  import {
3857
4017
  createPrompt,
3858
4018
  isDownKey,
@@ -3860,14 +4020,13 @@ import {
3860
4020
  isSpaceKey,
3861
4021
  isUpKey,
3862
4022
  useKeypress,
3863
- useMemo,
3864
4023
  usePagination,
3865
4024
  useState
3866
4025
  } from "@inquirer/core";
3867
4026
  import { select as select2 } from "@inquirer/prompts";
3868
4027
 
3869
4028
  // src/services/contrib-core.ts
3870
- import { dirname as dirname5, join as join15, resolve as resolve5 } from "path";
4029
+ import { dirname as dirname5, join as join14, resolve as resolve5 } from "path";
3871
4030
  async function listFilesAtRef(repoPath, ref) {
3872
4031
  const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
3873
4032
  return new Set(raw.split("\n").filter(Boolean));
@@ -3875,8 +4034,8 @@ async function listFilesAtRef(repoPath, ref) {
3875
4034
  async function detectContributableFiles(repoPath, baseRef, forkRef, config, forkTerritory = []) {
3876
4035
  const mergeBase = (await git(["merge-base", baseRef, forkRef], repoPath, { ignoreErrors: true })).trim() || baseRef;
3877
4036
  const predicates = {
3878
- isIgnored: (path4) => isIgnored(path4, config) || isUnderAnyFolder(path4, forkTerritory),
3879
- isPinned: (path4) => isPinned(path4, config)
4037
+ isIgnored: (path) => isIgnored(path, config) || isUnderAnyFolder(path, forkTerritory),
4038
+ isPinned: (path) => isPinned(path, config)
3880
4039
  };
3881
4040
  const analyzed = await analyzeRefs(repoPath, baseRef, forkRef, mergeBase, predicates);
3882
4041
  await enrichChangeInfo(repoPath, analyzed, mergeBase, baseRef, forkRef);
@@ -3931,7 +4090,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
3931
4090
  async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
3932
4091
  const branch = `contrib/${forkName}`;
3933
4092
  const gitDir = resolve5(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
3934
- const tmpIndex = join15(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
4093
+ const tmpIndex = join14(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
3935
4094
  const indexEnv = { GIT_INDEX_FILE: tmpIndex };
3936
4095
  try {
3937
4096
  await git(["read-tree", baseRef], repoPath, { env: indexEnv });
@@ -4004,20 +4163,14 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
4004
4163
  if (patch.length === 0) return null;
4005
4164
  return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
4006
4165
  }
4007
- function shouldApplyContributionsUnstaged(repoPath) {
4008
- return JSON.parse(readFileSync4(join16(repoPath, "package.json"), "utf8")).name === "cella";
4009
- }
4010
4166
  var contribPrompt = createPrompt((config, done) => {
4011
4167
  const { pageSize = 20, baseRef, cwd, forkName } = config;
4012
4168
  const [items, setItems] = useState(config.items);
4013
4169
  const [active, setActive] = useState(0);
4014
4170
  const [statusMsg, setStatusMsg] = useState("");
4015
4171
  const [promptStatus, setPromptStatus] = useState("idle");
4016
- const bounds = useMemo(() => {
4017
- if (items.length === 0) return { first: 0, last: 0 };
4018
- return { first: 0, last: items.length - 1 };
4019
- }, [items]);
4020
4172
  useKeypress(async (key) => {
4173
+ const last = items.length - 1;
4021
4174
  if (items.length === 0) {
4022
4175
  if (isEnterKey(key) || key.name === "q") {
4023
4176
  setPromptStatus("done");
@@ -4041,22 +4194,22 @@ var contribPrompt = createPrompt((config, done) => {
4041
4194
  return;
4042
4195
  }
4043
4196
  if (key.ctrl && isUpKey(key)) {
4044
- setActive(bounds.first);
4197
+ setActive(0);
4045
4198
  setStatusMsg("");
4046
4199
  return;
4047
4200
  }
4048
4201
  if (key.ctrl && isDownKey(key)) {
4049
- setActive(bounds.last);
4202
+ setActive(last);
4050
4203
  setStatusMsg("");
4051
4204
  return;
4052
4205
  }
4053
4206
  if (isUpKey(key)) {
4054
- setActive(active <= bounds.first ? bounds.first : active - 1);
4207
+ setActive(active <= 0 ? 0 : active - 1);
4055
4208
  setStatusMsg("");
4056
4209
  return;
4057
4210
  }
4058
4211
  if (isDownKey(key)) {
4059
- setActive(active >= bounds.last ? bounds.last : active + 1);
4212
+ setActive(active >= last ? last : active + 1);
4060
4213
  setStatusMsg("");
4061
4214
  return;
4062
4215
  }
@@ -4069,7 +4222,7 @@ var contribPrompt = createPrompt((config, done) => {
4069
4222
  const pagePath = await showContribDiff(items[active], baseRef, cwd, forkName);
4070
4223
  setStatusMsg(pagePath ? `opened ${items[active].path} in browser` : `no changes for ${items[active].path}`);
4071
4224
  } catch (error) {
4072
- setStatusMsg(`diff failed: ${error instanceof Error ? error.message : "unknown error"}`);
4225
+ setStatusMsg(`diff failed: ${errorMessage(error)}`);
4073
4226
  }
4074
4227
  return;
4075
4228
  }
@@ -4083,12 +4236,12 @@ var contribPrompt = createPrompt((config, done) => {
4083
4236
  if (promptStatus === "done") {
4084
4237
  const checked = items.filter((i) => i.checked);
4085
4238
  if (checked.length > 0) {
4086
- return `${colors_default.green("\u2713")} ${checked.length} files accepted`;
4239
+ return `${checkMark} ${checked.length} files accepted`;
4087
4240
  }
4088
- return `${colors_default.green("\u2713")} done`;
4241
+ return `${checkMark} done`;
4089
4242
  }
4090
4243
  if (items.length === 0) {
4091
- return `${colors_default.green("\u2713")} no contributions \u2014 press enter`;
4244
+ return `${checkMark} no contributions: press enter`;
4092
4245
  }
4093
4246
  const checkedCount = items.filter((i) => i.checked).length;
4094
4247
  const page = usePagination({
@@ -4122,6 +4275,38 @@ var contribPrompt = createPrompt((config, done) => {
4122
4275
  const lines = [header, page, statusLine, helpLine].filter(Boolean).join("\n").trimEnd();
4123
4276
  return `${lines}\x1B[?25l`;
4124
4277
  });
4278
+ async function selectFork(config, validated) {
4279
+ if (config.fork) {
4280
+ const match = validated.find((v) => v.fork.name === config.fork);
4281
+ if (!match?.valid) {
4282
+ throw new Error(`fork '${config.fork}' not found or invalid in config`);
4283
+ }
4284
+ return match;
4285
+ }
4286
+ const validForks = validated.filter((v) => v.valid);
4287
+ if (config.list || config.json || config.diff) {
4288
+ if (validForks.length === 0) {
4289
+ throw new Error("no valid forks configured");
4290
+ }
4291
+ if (validForks.length > 1) {
4292
+ throw new Error("multiple forks configured; pass --fork <name> to choose one");
4293
+ }
4294
+ return validForks[0];
4295
+ }
4296
+ const choices = validated.map((v) => ({
4297
+ value: v.fork.name,
4298
+ name: v.valid ? `${v.fork.name} ${colors_default.dim(`[${v.fork.pullBranch}] ${v.fork.localPath}`)}` : `${v.fork.name} ${colors_default.dim(v.fork.localPath)}`,
4299
+ disabled: v.valid ? false : v.error ?? "invalid"
4300
+ }));
4301
+ const picked = await select2({
4302
+ message: "select fork to pull contributions from:",
4303
+ choices,
4304
+ loop: false
4305
+ });
4306
+ const selected = validated.find((v) => v.fork.name === picked);
4307
+ if (!selected) console.info(colors_default.dim("no fork selected."));
4308
+ return selected;
4309
+ }
4125
4310
  async function fetchForkBranch(cellaPath, fetchSource, pullBranch) {
4126
4311
  await git(["fetch", fetchSource, pullBranch], cellaPath);
4127
4312
  return git(["rev-parse", "FETCH_HEAD"], cellaPath);
@@ -4149,42 +4334,8 @@ async function runContributions(config) {
4149
4334
  }
4150
4335
  const forkBasePath = await resolveForkBasePath(config.forkPath);
4151
4336
  const validated = forks.map((fork2) => validateForkPath(fork2, forkBasePath));
4152
- const validForks = validated.filter((v) => v.valid);
4153
- let selectedFork;
4154
- if (config.fork) {
4155
- const match = validated.find((v) => v.fork.name === config.fork);
4156
- if (!match?.valid) {
4157
- console.error(colors_default.red(`fork '${config.fork}' not found or invalid in config`));
4158
- return;
4159
- }
4160
- selectedFork = match;
4161
- } else if (config.list || config.json || config.diff) {
4162
- if (validForks.length === 0) {
4163
- console.error(colors_default.red("no valid forks configured"));
4164
- return;
4165
- }
4166
- if (validForks.length > 1) {
4167
- console.error(colors_default.red("multiple forks configured; pass --fork <name> to choose one"));
4168
- return;
4169
- }
4170
- selectedFork = validForks[0];
4171
- } else {
4172
- const choices = validated.map((v) => ({
4173
- value: v.fork.name,
4174
- name: v.valid ? `${v.fork.name} ${colors_default.dim(`[${v.fork.pullBranch}] ${v.fork.localPath}`)}` : `${v.fork.name} ${colors_default.dim(v.fork.localPath)}`,
4175
- disabled: v.valid ? false : v.error ?? "invalid"
4176
- }));
4177
- const picked = await select2({
4178
- message: "select fork to pull contributions from:",
4179
- choices,
4180
- loop: false
4181
- });
4182
- selectedFork = validated.find((v) => v.fork.name === picked);
4183
- }
4184
- if (!selectedFork) {
4185
- console.info(colors_default.dim("no fork selected."));
4186
- return;
4187
- }
4337
+ const selectedFork = await selectFork(config, validated);
4338
+ if (!selectedFork) return;
4188
4339
  const { fork, resolvedPath } = selectedFork;
4189
4340
  const forkName = fork.name;
4190
4341
  createSpinner("pulling fork contributions...");
@@ -4207,40 +4358,28 @@ async function runContributions(config) {
4207
4358
  if (appliedFiles.length > 0) {
4208
4359
  const stat = await getDiffStat(config.forkPath, baseRef, branch);
4209
4360
  const metaByPath = new Map(detection.files.map((f) => [f.path, f]));
4210
- for (const path4 of [...detection.modified, ...detection.created]) {
4211
- const fileMeta = metaByPath.get(path4);
4212
- allItems.push({
4213
- path: path4,
4214
- ref: branch,
4215
- deleted: false,
4216
- created: fileMeta?.kind === "created",
4217
- status: fileMeta?.status,
4218
- changedAt: fileMeta?.changedAt,
4219
- changedTs: fileMeta?.changedTs,
4220
- additions: stat.get(path4)?.additions ?? null,
4221
- deletions: stat.get(path4)?.deletions ?? null,
4222
- checked: false
4223
- });
4224
- }
4225
- for (const path4 of detection.deleted) {
4226
- const fileMeta = metaByPath.get(path4);
4227
- allItems.push({
4228
- path: path4,
4361
+ const toItem = (path, deleted) => {
4362
+ const fileMeta = metaByPath.get(path);
4363
+ return {
4364
+ path,
4229
4365
  ref: branch,
4230
- deleted: true,
4366
+ deleted,
4367
+ ...deleted ? {} : { created: fileMeta?.kind === "created" },
4231
4368
  status: fileMeta?.status,
4232
4369
  changedAt: fileMeta?.changedAt,
4233
4370
  changedTs: fileMeta?.changedTs,
4234
- additions: stat.get(path4)?.additions ?? null,
4235
- deletions: stat.get(path4)?.deletions ?? null,
4371
+ additions: stat.get(path)?.additions ?? null,
4372
+ deletions: stat.get(path)?.deletions ?? null,
4236
4373
  checked: false
4237
- });
4238
- }
4374
+ };
4375
+ };
4376
+ for (const path of [...detection.modified, ...detection.created]) allItems.push(toItem(path, false));
4377
+ for (const path of detection.deleted) allItems.push(toItem(path, true));
4239
4378
  }
4240
4379
  }
4241
4380
  } catch (error) {
4242
4381
  spinnerFail(`failed to pull ${forkName}`);
4243
- console.info(colors_default.red(` \u2717 ${error instanceof Error ? error.message : "unknown error"}`));
4382
+ console.info(colors_default.red(` \u2717 ${errorMessage(error)}`));
4244
4383
  return;
4245
4384
  }
4246
4385
  if (allItems.length === 0) {
@@ -4251,15 +4390,13 @@ async function runContributions(config) {
4251
4390
  spinnerSuccess(`${allItems.length} files from ${forkName}`);
4252
4391
  if (!config.list && !config.diff && forkBanner) {
4253
4392
  console.info(
4254
- ` ${colors_default.dim(`${forkName}: comparing committed '${forkBanner.pullBranch}' @ ${forkBanner.sha} (${forkBanner.date}) \u2014 uncommitted fork changes are not included`)}`
4393
+ ` ${colors_default.dim(`${forkName}: comparing committed '${forkBanner.pullBranch}' @ ${forkBanner.sha} (${forkBanner.date}); uncommitted fork changes are not included`)}`
4255
4394
  );
4256
4395
  }
4257
4396
  if (config.diff) {
4258
4397
  const matches = allItems.filter((i) => i.path === config.diff);
4259
4398
  if (matches.length === 0) {
4260
- console.error(colors_default.red(`no contribution found for path: ${config.diff}`));
4261
- process.exitCode = 1;
4262
- return;
4399
+ throw new Error(`no contribution found for path: ${config.diff}`);
4263
4400
  }
4264
4401
  for (const item of matches) {
4265
4402
  const diff = gitDiffFile(config.forkPath, `${baseRef}..${item.ref}`, item.path, { dstPrefix: forkName });
@@ -4286,7 +4423,7 @@ async function runContributions(config) {
4286
4423
  const status = item.status ?? "behind";
4287
4424
  const kind = item.deleted ? "deleted" : "modified";
4288
4425
  const changedAt = item.changedAt ?? "-";
4289
- console.info(`${forkName} ${status} ${kind} ${changedAt} ${item.path}`);
4426
+ writeStdout(`${forkName} ${status} ${kind} ${changedAt} ${item.path}`);
4290
4427
  }
4291
4428
  return;
4292
4429
  }
@@ -4305,7 +4442,7 @@ async function runContributions(config) {
4305
4442
  return;
4306
4443
  }
4307
4444
  createSpinner(`applying ${selected.length} files...`);
4308
- const applyUnstaged = shouldApplyContributionsUnstaged(config.forkPath);
4445
+ const applyUnstaged = isUpstreamRepo(config.forkPath);
4309
4446
  let applied = 0;
4310
4447
  const errors = [];
4311
4448
  for (const item of selected) {
@@ -4329,7 +4466,7 @@ async function runContributions(config) {
4329
4466
  }
4330
4467
  applied++;
4331
4468
  } catch (error) {
4332
- errors.push(`${item.path}: ${error instanceof Error ? error.message : "unknown error"}`);
4469
+ errors.push(`${item.path}: ${errorMessage(error)}`);
4333
4470
  }
4334
4471
  }
4335
4472
  if (errors.length > 0) {
@@ -4343,7 +4480,7 @@ async function runContributions(config) {
4343
4480
  console.info();
4344
4481
  console.info(
4345
4482
  colors_default.dim(
4346
- applyUnstaged ? " files are unstaged \u2014 review, stage what you want, and commit when ready" : " files are staged \u2014 review and commit when ready"
4483
+ applyUnstaged ? " files are unstaged: review, stage what you want, and commit when ready" : " files are staged: review and commit when ready"
4347
4484
  )
4348
4485
  );
4349
4486
  console.info();
@@ -4355,16 +4492,16 @@ import process8 from "process";
4355
4492
  import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
4356
4493
 
4357
4494
  // src/services/sync.ts
4358
- import { spawnSync as spawnSync3 } from "child_process";
4495
+ import { spawnSync as spawnSync4 } from "child_process";
4359
4496
  import { select as select3 } from "@inquirer/prompts";
4360
4497
 
4361
4498
  // src/utils/gh.ts
4362
- import { spawnSync as spawnSync2 } from "child_process";
4499
+ import { spawnSync as spawnSync3 } from "child_process";
4363
4500
  function ghAvailable() {
4364
- return spawnSync2("gh", ["--version"], { stdio: "ignore" }).status === 0;
4501
+ return spawnSync3("gh", ["--version"], { stdio: "ignore" }).status === 0;
4365
4502
  }
4366
4503
  function runGh(cwd, args) {
4367
- const result = spawnSync2("gh", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
4504
+ const result = spawnSync3("gh", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4368
4505
  const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
4369
4506
  return { ok: result.status === 0, output };
4370
4507
  }
@@ -4404,12 +4541,12 @@ function closePr(cwd, ref) {
4404
4541
  }
4405
4542
 
4406
4543
  // src/services/packages.ts
4407
- import { existsSync as existsSync11, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
4408
- import { join as join18 } from "path";
4544
+ import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
4545
+ import { join as join16 } from "path";
4409
4546
 
4410
4547
  // src/services/package-usage.ts
4411
- import { existsSync as existsSync10, readFileSync as readFileSync5, statSync } from "fs";
4412
- import { join as join17 } from "path";
4548
+ import { existsSync as existsSync10, readFileSync as readFileSync4, statSync } from "fs";
4549
+ import { join as join15 } from "path";
4413
4550
  var MAX_FILE_BYTES = 1024 * 1024;
4414
4551
  function escapeRegExp(value) {
4415
4552
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4417,41 +4554,41 @@ function escapeRegExp(value) {
4417
4554
  function readSearchable(filePath) {
4418
4555
  try {
4419
4556
  if (statSync(filePath).size > MAX_FILE_BYTES) return "";
4420
- const text = readFileSync5(filePath, "utf-8");
4557
+ const text = readFileSync4(filePath, "utf8");
4421
4558
  return text.includes("\0") ? "" : text;
4422
4559
  } catch {
4423
4560
  return "";
4424
4561
  }
4425
4562
  }
4426
- function fileSource(forkPath, path4) {
4563
+ function fileSource(forkPath, path) {
4427
4564
  let text;
4428
- return { path: path4, label: path4, text: () => text ??= readSearchable(join17(forkPath, path4)) };
4565
+ return { path, label: path, text: () => text ??= readSearchable(join15(forkPath, path)) };
4429
4566
  }
4430
- async function readScriptsAtRef(forkPath, ref, path4) {
4567
+ async function readScriptsAtRef(forkPath, ref, path) {
4431
4568
  try {
4432
- return JSON.parse(await git(["show", `${ref}:${path4}`], forkPath)).scripts ?? {};
4569
+ return JSON.parse(await git(["show", `${ref}:${path}`], forkPath)).scripts ?? {};
4433
4570
  } catch {
4434
4571
  return {};
4435
4572
  }
4436
4573
  }
4437
- async function scriptSources(forkPath, upstreamRef, baseRef, path4) {
4574
+ async function scriptSources(forkPath, upstreamRef, baseRef, path) {
4438
4575
  let forkScripts;
4439
4576
  try {
4440
- forkScripts = JSON.parse(readFileSync5(join17(forkPath, path4), "utf-8")).scripts ?? {};
4577
+ forkScripts = JSON.parse(readFileSync4(join15(forkPath, path), "utf8")).scripts ?? {};
4441
4578
  } catch {
4442
4579
  return [];
4443
4580
  }
4444
- const baseScripts = await readScriptsAtRef(forkPath, baseRef, path4);
4445
- const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef, path4);
4446
- return Object.entries(forkScripts).filter(([name, value]) => value !== baseScripts[name] && value !== upstreamScripts[name]).map(([name, value]) => ({ path: path4, label: `${path4} (scripts.${name})`, text: () => value }));
4581
+ const baseScripts = await readScriptsAtRef(forkPath, baseRef, path);
4582
+ const upstreamScripts = await readScriptsAtRef(forkPath, upstreamRef, path);
4583
+ return Object.entries(forkScripts).filter(([name, value]) => value !== baseScripts[name] && value !== upstreamScripts[name]).map(([name, value]) => ({ path, label: `${path} (scripts.${name})`, text: () => value }));
4447
4584
  }
4448
4585
  function binNames(forkPath, location, name) {
4449
4586
  const bareName = name.split("/").pop();
4450
- for (const dir of [join17(forkPath, location), forkPath]) {
4451
- const manifest = join17(dir, "node_modules", name, "package.json");
4587
+ for (const dir of [join15(forkPath, location), forkPath]) {
4588
+ const manifest = join15(dir, "node_modules", name, "package.json");
4452
4589
  if (!existsSync10(manifest)) continue;
4453
4590
  try {
4454
- const { bin } = JSON.parse(readFileSync5(manifest, "utf-8"));
4591
+ const { bin } = JSON.parse(readFileSync4(manifest, "utf8"));
4455
4592
  if (typeof bin === "string") return [bareName];
4456
4593
  return bin && typeof bin === "object" ? Object.keys(bin) : [];
4457
4594
  } catch {
@@ -4476,9 +4613,9 @@ function scriptPattern(name) {
4476
4613
  async function loadForkUsage(forkPath, upstreamRef, baseRef) {
4477
4614
  const diff = await git(["diff", "--name-only", "-z", "--diff-filter=d", upstreamRef], forkPath);
4478
4615
  const sources = [];
4479
- for (const path4 of diff.split("\0").filter(Boolean)) {
4480
- if (isPackageJson(path4)) sources.push(...await scriptSources(forkPath, upstreamRef, baseRef, path4));
4481
- else if (!isManagedFile(path4)) sources.push(fileSource(forkPath, path4));
4616
+ for (const path of diff.split("\0").filter(Boolean)) {
4617
+ if (isPackageJson(path)) sources.push(...await scriptSources(forkPath, upstreamRef, baseRef, path));
4618
+ else if (!isManagedFile(path)) sources.push(fileSource(forkPath, path));
4482
4619
  }
4483
4620
  const findIn = (scoped, patterns) => scoped.find((source) => patterns.some((pattern) => pattern.test(source.text())))?.label;
4484
4621
  return {
@@ -4631,14 +4768,14 @@ function setTopLevelKey(pkg, key, value) {
4631
4768
  function readPackageJson(filePath) {
4632
4769
  if (!existsSync11(filePath)) return null;
4633
4770
  try {
4634
- return JSON.parse(readFileSync6(filePath, "utf-8"));
4771
+ return JSON.parse(readFileSync5(filePath, "utf8"));
4635
4772
  } catch {
4636
4773
  return null;
4637
4774
  }
4638
4775
  }
4639
4776
  function writePackageJson(filePath, data) {
4640
4777
  writeFileSync4(filePath, `${JSON.stringify(data, null, 2)}
4641
- `, "utf-8");
4778
+ `, "utf8");
4642
4779
  }
4643
4780
  function packageJsonPath(relativePath) {
4644
4781
  return relativePath ? `${relativePath}/package.json` : "package.json";
@@ -4667,16 +4804,16 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
4667
4804
  const changes = [];
4668
4805
  const kept = [];
4669
4806
  const pkgRelPath = packageJsonPath(relativePath);
4670
- const pkgPath = join18(forkPath, pkgRelPath);
4807
+ const pkgPath = join16(forkPath, pkgRelPath);
4671
4808
  const forkPkg = readPackageJson(pkgPath);
4672
4809
  const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
4673
4810
  const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
4674
4811
  if (!upstream) return { updated: false, changes, kept };
4675
4812
  if (!forkPkg) {
4676
- const workspaceArrived = relativePath !== "" && existsSync11(join18(forkPath, relativePath));
4813
+ const workspaceArrived = relativePath !== "" && existsSync11(join16(forkPath, relativePath));
4677
4814
  if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
4678
4815
  writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
4679
- `, "utf-8");
4816
+ `, "utf8");
4680
4817
  return { updated: true, changes: ["copied from upstream (new workspace)"], kept };
4681
4818
  }
4682
4819
  const upstreamPkg = upstream.data;
@@ -4748,7 +4885,7 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
4748
4885
  }
4749
4886
  async function runPackages(config, options = {}) {
4750
4887
  createSpinner("syncing package.json files...");
4751
- const keysToSync = config.settings.packageJsonSync || ["dependencies", "devDependencies"];
4888
+ const keysToSync = config.settings.packageJsonSync || DEFAULT_PACKAGE_JSON_SYNC;
4752
4889
  const conflictedSet = new Set(options.conflictedFiles ?? []);
4753
4890
  const baseRef = await resolveMergeBase(config.forkPath, config.upstreamRef);
4754
4891
  const usage = baseRef ? await loadForkUsage(config.forkPath, config.upstreamRef, baseRef) : null;
@@ -4770,26 +4907,26 @@ async function runPackages(config, options = {}) {
4770
4907
  "package sync complete",
4771
4908
  `${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
4772
4909
  );
4773
- for (const { path: path4, changes } of reports) {
4910
+ for (const { path, changes } of reports) {
4774
4911
  if (changes.length === 0) continue;
4775
- console.info(` ${path4}`);
4912
+ console.info(` ${path}`);
4776
4913
  for (const change of changes) console.info(` ${colors_default.dim(change)}`);
4777
4914
  }
4778
- const kept = reports.flatMap(({ path: path4, kept: kept2 }) => kept2.map((entry) => `${path4} ${entry}`));
4915
+ const kept = reports.flatMap(({ path, kept: kept2 }) => kept2.map((entry) => `${path} ${entry}`));
4779
4916
  if (kept.length > 0) {
4780
4917
  console.info();
4781
- console.warn(
4782
- `${warningMark} kept ${kept.length} ${kept.length === 1 ? "entry" : "entries"} upstream dropped, the fork still uses ${kept.length === 1 ? "it" : "them"}:`
4918
+ console.info(
4919
+ `${warningMark} ${colors_default.yellow(`kept ${kept.length} ${kept.length === 1 ? "entry" : "entries"} upstream dropped, the fork still uses ${kept.length === 1 ? "it" : "them"}:`)}`
4783
4920
  );
4784
- for (const entry of kept) console.warn(` ${colors_default.dim("\u2192")} ${entry}`);
4921
+ for (const entry of kept) console.info(` ${colors_default.dim("\u2192")} ${entry}`);
4785
4922
  }
4786
4923
  if (skipped.length > 0) {
4787
4924
  console.info();
4788
- console.warn(
4789
- `${warningMark} skipped ${skipped.length} conflicted package.json file(s) \u2014 resolve the conflicts, commit the merge, then rerun \`pnpm cella sync\`:`
4925
+ console.info(
4926
+ `${warningMark} ${colors_default.yellow(`skipped ${skipped.length} conflicted package.json file(s): resolve the conflicts, commit the merge, then rerun \`pnpm cella sync\`:`)}`
4790
4927
  );
4791
- for (const path4 of skipped) {
4792
- console.warn(` ${colors_default.dim("\u2192")} ${path4}`);
4928
+ for (const path of skipped) {
4929
+ console.info(` ${colors_default.dim("\u2192")} ${path}`);
4793
4930
  }
4794
4931
  }
4795
4932
  }
@@ -4797,14 +4934,14 @@ async function runPackages(config, options = {}) {
4797
4934
  // src/services/sync.ts
4798
4935
  async function resolveCycleStart(forkPath, base, currentBranch) {
4799
4936
  if (!await branchExists(forkPath, base)) {
4800
- await fetch2(forkPath, "origin").catch(() => {
4937
+ await fetchRemote(forkPath, "origin").catch(() => {
4801
4938
  });
4802
- console.info(colors_default.dim(`'${base}' has no local branch \u2014 cutting from 'origin/${base}'.`));
4939
+ console.info(colors_default.dim(`'${base}' has no local branch: cutting from 'origin/${base}'.`));
4803
4940
  return `origin/${base}`;
4804
4941
  }
4805
4942
  const { upstream, ahead, behind } = await getUpstreamStatus(forkPath, base);
4806
4943
  if (!upstream) {
4807
- console.info(colors_default.dim(`'${base}' has no upstream \u2014 skipping the up-to-date check.`));
4944
+ console.info(colors_default.dim(`'${base}' has no upstream: skipping the up-to-date check.`));
4808
4945
  return base;
4809
4946
  }
4810
4947
  if (ahead > 0 && behind > 0) {
@@ -4817,7 +4954,7 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
4817
4954
  const worktree = currentBranch === base ? null : await getBranchWorktree(forkPath, base);
4818
4955
  if (worktree) {
4819
4956
  console.info(
4820
- colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree} \u2014 leaving it as it is.`)
4957
+ colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree}; leaving it as it is.`)
4821
4958
  );
4822
4959
  return upstream;
4823
4960
  }
@@ -4827,7 +4964,7 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
4827
4964
  return base;
4828
4965
  }
4829
4966
  if (ahead > 0) {
4830
- console.info(colors_default.dim(`'${base}' is ${ahead} commit(s) ahead of '${upstream}' (unpushed) \u2014 continuing.`));
4967
+ console.info(colors_default.dim(`'${base}' is ${ahead} commit(s) ahead of '${upstream}' (unpushed); continuing.`));
4831
4968
  }
4832
4969
  return base;
4833
4970
  }
@@ -4852,7 +4989,7 @@ async function resolveCutBase(forkPath, base) {
4852
4989
  async function returnToBase(forkPath, base) {
4853
4990
  const worktree = await getBranchWorktree(forkPath, base);
4854
4991
  if (worktree) {
4855
- console.info(colors_default.dim(`'${base}' is checked out at ${worktree} \u2014 detaching at '${base}' here instead...`));
4992
+ console.info(colors_default.dim(`'${base}' is checked out at ${worktree}; detaching at '${base}' here instead...`));
4856
4993
  await switchDetached(forkPath, base);
4857
4994
  return `detached at '${base}'`;
4858
4995
  }
@@ -4864,30 +5001,24 @@ async function returnToBase(forkPath, base) {
4864
5001
  }
4865
5002
  async function runSync(config, options) {
4866
5003
  createSpinner("starting sync...");
4867
- const result = await runMergeEngine(config, {
4868
- apply: true,
4869
- onProgress: (message) => {
4870
- spinnerText(message);
4871
- },
4872
- onStep: (label, detail) => {
4873
- spinnerSuccess(label, detail);
4874
- createSpinner("...");
5004
+ let result;
5005
+ try {
5006
+ result = await runEngineWithSpinner(config, true);
5007
+ } catch (error) {
5008
+ spinnerSuccess();
5009
+ if (error instanceof UpstreamConfigChangedError) {
5010
+ printUpstreamOverrideChanges({ upstreamOverrides: error.report });
5011
+ console.info();
4875
5012
  }
4876
- });
5013
+ throw error;
5014
+ }
4877
5015
  if (result.success) {
4878
5016
  spinnerSuccess();
4879
5017
  } else {
4880
5018
  spinnerFail("sync completed with conflicts");
4881
5019
  }
4882
- printSummary(result.summary, "merge summary");
4883
- printIgnoredUpstreamChanges(result);
4884
- printUpstreamOverrideChanges(result);
4885
- printMaskingPinWarning(result.files);
4886
- if (config.logFile) {
4887
- const logPath = writeLogFile(config.forkPath, result.files);
4888
- console.info();
4889
- console.info(colors_default.dim(`full file list written to: ${logPath}`));
4890
- }
5020
+ printEngineReports(result, "merge summary");
5021
+ printLogFileReport(config, result.files);
4891
5022
  const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
4892
5023
  printSyncComplete(result, { stagedBranch });
4893
5024
  printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
@@ -4895,7 +5026,6 @@ async function runSync(config, options) {
4895
5026
  return result;
4896
5027
  }
4897
5028
  var SYNC_PR_TITLE = "chore: sync upstream cella";
4898
- var PR_BODY_COMMIT_MAX = 50;
4899
5029
  async function buildSyncCommitMessage(forkPath, upstreamRef) {
4900
5030
  const shortSha = await getShortSha(forkPath, upstreamRef).catch(() => "");
4901
5031
  if (!shortSha) return SYNC_PR_TITLE;
@@ -4947,9 +5077,8 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
4947
5077
  const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
4948
5078
  const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
4949
5079
  const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, {
4950
- oldestFirst: true,
4951
- skip: totalCount > PR_BODY_COMMIT_MAX ? totalCount - PR_BODY_COMMIT_MAX : 0,
4952
- limit: PR_BODY_COMMIT_MAX
5080
+ skip: totalCount > COMMIT_LIST_MAX ? totalCount - COMMIT_LIST_MAX : 0,
5081
+ limit: COMMIT_LIST_MAX
4953
5082
  }) : [];
4954
5083
  const notes = await readPrBodyNotes(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
4955
5084
  return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount, notes });
@@ -4973,11 +5102,8 @@ function printShipSteps(temporaryBranch, base, title) {
4973
5102
  console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
4974
5103
  printPrCreateStep(temporaryBranch, base, title);
4975
5104
  }
4976
- function printFinishSteps() {
4977
- console.info(colors_default.dim(" pnpm cella sync"));
4978
- }
4979
5105
  function hasStagedSyncChanges(result) {
4980
- return result.files.some((file) => ["behind", "diverged", "renamed", "ignored", "pinned"].includes(file.status));
5106
+ return result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
4981
5107
  }
4982
5108
  function extractFirstUrl(output) {
4983
5109
  return output.match(/https?:\/\/\S+/)?.[0];
@@ -4988,7 +5114,7 @@ async function flattenSyncBranch(forkPath, branch, base) {
4988
5114
  const message = await buildSyncCommitMessage(forkPath, `${mergeCommits[0]}^2`);
4989
5115
  console.info(
4990
5116
  colors_default.yellow(
4991
- `'${branch}' contains ${mergeCommits.length} merge commit(s) \u2014 the PR would list the entire upstream history.`
5117
+ `'${branch}' contains ${mergeCommits.length} merge commit(s): the PR would list the entire upstream history.`
4992
5118
  )
4993
5119
  );
4994
5120
  console.info(colors_default.dim(`flattening '${branch}' to a single commit (same content)...`));
@@ -5011,8 +5137,7 @@ async function shipSyncBranch(config, branch) {
5011
5137
  try {
5012
5138
  await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
5013
5139
  } catch (error) {
5014
- const detail = error instanceof Error ? error.message : String(error);
5015
- console.info(colors_default.yellow(`push failed (${detail.split("\n")[0]}). finish manually:`));
5140
+ console.info(colors_default.yellow(`push failed (${errorMessage(error).split("\n")[0]}). finish manually:`));
5016
5141
  printShipSteps(branch, base, prTitle);
5017
5142
  return;
5018
5143
  }
@@ -5020,9 +5145,9 @@ async function shipSyncBranch(config, branch) {
5020
5145
  console.info(colors_default.dim("opening a pull request..."));
5021
5146
  const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
5022
5147
  const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
5023
- const pr = spawnSync3("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
5148
+ const pr = spawnSync4("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
5024
5149
  cwd: forkPath,
5025
- encoding: "utf-8",
5150
+ encoding: "utf8",
5026
5151
  stdio: ["ignore", "pipe", "pipe"]
5027
5152
  });
5028
5153
  prUrl = extractFirstUrl(`${pr.stdout ?? ""}
@@ -5034,16 +5159,16 @@ ${pr.stderr ?? ""}`);
5034
5159
  printPrCreateStep(branch, base, prTitle);
5035
5160
  }
5036
5161
  } else {
5037
- console.info(colors_default.yellow("`gh` not found \u2014 open the PR manually:"));
5162
+ console.info(colors_default.yellow("`gh` not found, open the PR manually:"));
5038
5163
  printPrCreateStep(branch, base, prTitle);
5039
5164
  }
5040
5165
  const position = await returnToBase(forkPath, base);
5041
5166
  console.info();
5042
5167
  if (prUrl) {
5043
- console.info(`${colors_default.green("\u2713")} Sync pull request ${prOpened ? "opened" : "ready"}`);
5168
+ console.info(`${checkMark} sync pull request ${prOpened ? "opened" : "ready"}`);
5044
5169
  console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, ${position}`));
5045
5170
  } else {
5046
- console.info(`${colors_default.green("\u2713")} Sync branch pushed`);
5171
+ console.info(`${checkMark} sync branch pushed`);
5047
5172
  console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
5048
5173
  }
5049
5174
  await printMigrationNotesLine(config);
@@ -5051,23 +5176,43 @@ ${pr.stderr ?? ""}`);
5051
5176
  function finalizeWorkspace(forkPath) {
5052
5177
  for (const args of [["install"], ["check"]]) {
5053
5178
  console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
5054
- const result = spawnSync3("pnpm", args, { cwd: forkPath, stdio: "inherit" });
5179
+ const result = spawnSync4("pnpm", args, { cwd: forkPath, stdio: "inherit" });
5055
5180
  if (result.status !== 0) return false;
5056
5181
  }
5057
5182
  return true;
5058
5183
  }
5184
+ async function discardTemporarySyncBranch(forkPath, branch) {
5185
+ if (branch.startBranch === "HEAD") await switchDetached(forkPath, branch.startCommit);
5186
+ else await switchBranch(forkPath, branch.startBranch);
5187
+ await deleteBranch(forkPath, branch.temporaryBranch);
5188
+ }
5189
+ async function discardUnusedSyncBranch(forkPath, branch) {
5190
+ const unused = await getCurrentBranch(forkPath) === branch.temporaryBranch && !await mergeInProgress(forkPath) && await isClean(forkPath);
5191
+ if (!unused) return false;
5192
+ await discardTemporarySyncBranch(forkPath, branch);
5193
+ const start = branch.startBranch === "HEAD" ? `at ${branch.startCommit.slice(0, 7)}` : `on '${branch.startBranch}'`;
5194
+ console.info(colors_default.dim(`removed the unused '${branch.temporaryBranch}', back ${start}.`));
5195
+ return true;
5196
+ }
5059
5197
  async function runSyncCycle(config) {
5060
5198
  const { forkPath } = config;
5061
5199
  const branch = await setupTemporarySyncBranch(config);
5062
- const result = await runSync(config, { stagedBranch: branch.temporaryBranch });
5200
+ let result;
5201
+ try {
5202
+ result = await runSync(config, { stagedBranch: branch.temporaryBranch });
5203
+ } catch (error) {
5204
+ const discarded = await discardUnusedSyncBranch(forkPath, branch).catch(() => false);
5205
+ if (discarded && error instanceof BehindSyncPointError && !config.ref) {
5206
+ return { status: "noop", behind: error.message };
5207
+ }
5208
+ throw error;
5209
+ }
5063
5210
  if (config.settings.syncWithPackages !== false) {
5064
5211
  await runPackages(config, { conflictedFiles: result.conflicts });
5065
5212
  }
5066
5213
  if (result.conflicts.length > 0) return { status: "conflicts", branch };
5067
5214
  if (!await mergeInProgress(forkPath)) {
5068
- if (branch.startBranch === "HEAD") await switchDetached(forkPath, branch.startCommit);
5069
- else await switchBranch(forkPath, branch.startBranch);
5070
- await deleteBranch(forkPath, branch.temporaryBranch);
5215
+ await discardTemporarySyncBranch(forkPath, branch);
5071
5216
  return { status: "noop" };
5072
5217
  }
5073
5218
  return { status: "staged", branch };
@@ -5112,15 +5257,15 @@ async function mergeOpenSyncPrs(config, open) {
5112
5257
  const merged = mergePrSquash(forkPath, newest.number, { deleteBranch: true });
5113
5258
  if (!merged.ok) {
5114
5259
  console.info();
5115
- console.info(colors_default.yellow(`could not merge #${newest.number} \u2014 resolve it first, then re-run \`pnpm cella sync\`:`));
5260
+ console.info(colors_default.yellow(`could not merge #${newest.number}: resolve it first, then re-run \`pnpm cella sync\`:`));
5116
5261
  if (merged.output) console.info(colors_default.dim(indentLines(merged.output)));
5117
5262
  console.info(colors_default.dim(` ${newest.url}`));
5118
5263
  return "cancel";
5119
5264
  }
5120
- console.info(`${colors_default.green("\u2713")} merged #${newest.number}`);
5265
+ console.info(`${checkMark} merged #${newest.number}`);
5121
5266
  await deleteBranch(forkPath, newest.headRefName);
5122
5267
  for (const pr of superseded) {
5123
- console.info(colors_default.dim(`closing #${pr.number} \u2014 its changes are included in #${newest.number}...`));
5268
+ console.info(colors_default.dim(`closing #${pr.number}: its changes are included in #${newest.number}...`));
5124
5269
  closePr(forkPath, pr.number);
5125
5270
  await deleteBranch(forkPath, pr.headRefName);
5126
5271
  }
@@ -5185,15 +5330,20 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
5185
5330
  const outcome = await runSyncCycle(config);
5186
5331
  console.info();
5187
5332
  if (outcome.status === "noop") {
5188
- console.info(colors_default.green("already up to date with upstream \u2014 nothing to sync."));
5333
+ if (outcome.behind) {
5334
+ console.info(colors_default.green("nothing to sync: the last sync already went past this upstream point."));
5335
+ for (const line of outcome.behind.split("\n")) console.info(colors_default.dim(` ${line}`));
5336
+ } else {
5337
+ console.info(colors_default.green("already up to date with upstream, nothing to sync."));
5338
+ }
5189
5339
  return;
5190
5340
  }
5191
5341
  const { temporaryBranch } = outcome.branch;
5192
5342
  if (outcome.status === "conflicts") {
5193
5343
  console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve and stage them, then:`)}`);
5194
- printFinishSteps();
5344
+ console.info(colors_default.dim(" pnpm cella sync"));
5195
5345
  console.info(colors_default.dim(" rerun commits the sync and stops for drift triage; a further rerun ships (push + PR)."));
5196
- console.info(colors_default.dim(" let the rerun commit \u2014 a manual `git commit` records a merge commit that bloats the PR."));
5346
+ console.info(colors_default.dim(" let the rerun commit: a manual `git commit` records a merge commit that bloats the PR."));
5197
5347
  return;
5198
5348
  }
5199
5349
  await commitSyncMerge(config, temporaryBranch);
@@ -5201,12 +5351,11 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
5201
5351
 
5202
5352
  // src/services/forks.ts
5203
5353
  async function gatherForkStatus(forkPath) {
5204
- const [branch, porcelain, syncRef] = await Promise.all([
5354
+ const [branch, dirty, syncRef] = await Promise.all([
5205
5355
  getCurrentBranch(forkPath).catch(() => "unknown"),
5206
- git(["status", "--porcelain"], forkPath, { ignoreErrors: true }),
5356
+ getWorkingTreeChangeCount(forkPath).catch(() => 0),
5207
5357
  getStoredSyncRef(forkPath)
5208
5358
  ]);
5209
- const dirty = porcelain ? porcelain.split("\n").filter(Boolean).length : 0;
5210
5359
  let lastSync = null;
5211
5360
  if (syncRef) {
5212
5361
  const commitInfo = await getCommitInfo(forkPath, syncRef).catch(() => null);
@@ -5269,7 +5418,8 @@ async function syncFork(config, fork, forkPath) {
5269
5418
  list: false,
5270
5419
  json: false,
5271
5420
  verbose: config.verbose,
5272
- hard: config.hard
5421
+ hard: config.hard,
5422
+ keepConfig: config.keepConfig
5273
5423
  };
5274
5424
  await runSyncCommand(forkRuntimeConfig);
5275
5425
  }
@@ -5282,8 +5432,7 @@ async function runForks(config) {
5282
5432
  if (config.fork) {
5283
5433
  const match = forks.find((f) => f.name === config.fork);
5284
5434
  if (!match) {
5285
- console.error(colors_default.red(`fork '${config.fork}' not found in config`));
5286
- return;
5435
+ throw new Error(`fork '${config.fork}' not found in config`);
5287
5436
  }
5288
5437
  const resolvedPath = resolve7(await resolveForkBasePath(config.forkPath), match.localPath);
5289
5438
  await syncFork(config, match, resolvedPath);
@@ -5292,7 +5441,7 @@ async function runForks(config) {
5292
5441
  const forkBasePath = await resolveForkBasePath(config.forkPath);
5293
5442
  while (true) {
5294
5443
  const choices = await buildForkChoices(forks, forkBasePath);
5295
- const forkChoices = [...choices, new Separator2("\u2500".repeat(40)), { value: "_exit", name: colors_default.dim("exit") }];
5444
+ const forkChoices = [...choices, new Separator2(MENU_DIVIDER), { value: "_exit", name: colors_default.dim("exit") }];
5296
5445
  const selectedPath = await select4({
5297
5446
  message: "select fork to sync:",
5298
5447
  choices: forkChoices,
@@ -5314,15 +5463,15 @@ async function runForks(config) {
5314
5463
 
5315
5464
  // src/services/stats.ts
5316
5465
  import { readFile as readFile3 } from "fs/promises";
5317
- import { join as join20 } from "path";
5466
+ import { join as join18 } from "path";
5318
5467
 
5319
5468
  // src/utils/coverage-utils.ts
5320
- import { spawnSync as spawnSync4 } from "child_process";
5321
- import * as fs3 from "fs";
5322
- import * as path3 from "path";
5469
+ import { spawnSync as spawnSync5 } from "child_process";
5470
+ import { existsSync as existsSync12, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
5471
+ import { join as join17, relative as relative3, sep as sep2 } from "path";
5323
5472
  var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
5324
5473
  function summaryPath(forkPath) {
5325
- return path3.join(forkPath, ".coverage", "coverage-summary.json");
5474
+ return join17(forkPath, ".coverage", "coverage-summary.json");
5326
5475
  }
5327
5476
  function colorPct(pct) {
5328
5477
  if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
@@ -5341,7 +5490,7 @@ function formatAge(ms) {
5341
5490
  }
5342
5491
  function refreshCoverage(forkPath) {
5343
5492
  console.info(colors_default.dim("running `pnpm test` (this spins up Docker and runs the full suite)..."));
5344
- const result = spawnSync4("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
5493
+ const result = spawnSync5("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
5345
5494
  return result.status === 0;
5346
5495
  }
5347
5496
  function printCoverageSummary(forkPath, options = {}) {
@@ -5349,24 +5498,24 @@ function printCoverageSummary(forkPath, options = {}) {
5349
5498
  if (options.refresh) {
5350
5499
  const ok = refreshCoverage(forkPath);
5351
5500
  if (!ok) {
5352
- console.info(colors_default.red("coverage run failed \u2014 showing last cached results (if any)"));
5501
+ console.info(colors_default.red("coverage run failed: showing last cached results (if any)"));
5353
5502
  }
5354
5503
  console.info();
5355
5504
  }
5356
- if (!fs3.existsSync(file)) {
5505
+ if (!existsSync12(file)) {
5357
5506
  console.info(colors_default.bold("coverage"));
5358
- console.info(colors_default.yellow(" no coverage data found \u2014 run `pnpm test` to generate it"));
5507
+ console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to generate it"));
5359
5508
  console.info();
5360
5509
  return;
5361
5510
  }
5362
5511
  let summary;
5363
5512
  let mtimeMs;
5364
5513
  try {
5365
- summary = JSON.parse(fs3.readFileSync(file, "utf-8"));
5366
- mtimeMs = fs3.statSync(file).mtimeMs;
5514
+ summary = JSON.parse(readFileSync6(file, "utf8"));
5515
+ mtimeMs = statSync2(file).mtimeMs;
5367
5516
  } catch {
5368
5517
  console.info(colors_default.bold("coverage"));
5369
- console.info(colors_default.yellow(" could not read coverage-summary.json \u2014 run `pnpm test` to regenerate"));
5518
+ console.info(colors_default.yellow(" could not read coverage-summary.json: run `pnpm test` to regenerate"));
5370
5519
  console.info();
5371
5520
  return;
5372
5521
  }
@@ -5375,9 +5524,9 @@ function printCoverageSummary(forkPath, options = {}) {
5375
5524
  const perPackage = /* @__PURE__ */ new Map();
5376
5525
  for (const [key, entry] of Object.entries(summary)) {
5377
5526
  if (key === "total") continue;
5378
- const rel = path3.relative(forkPath, key);
5527
+ const rel = relative3(forkPath, key);
5379
5528
  if (rel.startsWith("..")) continue;
5380
- const pkg = rel.split(path3.sep)[0];
5529
+ const pkg = rel.split(sep2)[0];
5381
5530
  const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
5382
5531
  acc.covered += entry.lines.covered;
5383
5532
  acc.total += entry.lines.total;
@@ -5386,10 +5535,10 @@ function printCoverageSummary(forkPath, options = {}) {
5386
5535
  const ageLabel = formatAge(ageMs);
5387
5536
  console.info(colors_default.bold("coverage"), colors_default.dim(`(updated ${ageLabel})`));
5388
5537
  if (stale) {
5389
- console.info(colors_default.yellow(" \u26A0 data is older than 7 days \u2014 run `pnpm test` to refresh"));
5538
+ console.info(colors_default.yellow(" \u26A0 data is older than 7 days: run `pnpm test` to refresh"));
5390
5539
  }
5391
5540
  if (!Object.keys(summary).some((key) => key !== "total")) {
5392
- console.info(colors_default.yellow(" no coverage data found \u2014 run `pnpm test` to regenerate"));
5541
+ console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to regenerate"));
5393
5542
  console.info();
5394
5543
  return;
5395
5544
  }
@@ -5408,7 +5557,7 @@ function printCoverageSummary(forkPath, options = {}) {
5408
5557
  // src/services/stats.ts
5409
5558
  var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
5410
5559
  async function getWorkspacePackages(forkPath) {
5411
- const content = await readFile3(join20(forkPath, "pnpm-workspace.yaml"), "utf-8");
5560
+ const content = await readFile3(join18(forkPath, "pnpm-workspace.yaml"), "utf8");
5412
5561
  return parseYamlBlockList(content, "packages").map((pattern) => {
5413
5562
  if (pattern.endsWith("/*")) {
5414
5563
  const base = pattern.slice(0, -2);
@@ -5472,12 +5621,12 @@ async function collectStats(forkPath) {
5472
5621
  const categories = emptyCat();
5473
5622
  const pkgStats = /* @__PURE__ */ new Map();
5474
5623
  let totalLoc = 0;
5475
- for (const { path: path4, loc } of entries) {
5476
- const category = classifyFile(path4);
5624
+ for (const { path, loc } of entries) {
5625
+ const category = classifyFile(path);
5477
5626
  categories[category].files++;
5478
5627
  categories[category].loc += loc;
5479
5628
  totalLoc += loc;
5480
- const pkg = matchPackage(path4, packages);
5629
+ const pkg = matchPackage(path, packages);
5481
5630
  let entry = pkgStats.get(pkg);
5482
5631
  if (!entry) {
5483
5632
  entry = { total: 0, loc: 0, categories: emptyCat() };
@@ -5554,17 +5703,237 @@ function printStats(stats, verbose) {
5554
5703
  }
5555
5704
  console.info();
5556
5705
  }
5557
- async function runStats(forkPath, options = {}) {
5706
+ async function runStats(config) {
5707
+ const { forkPath } = config;
5558
5708
  createSpinner("counting files...");
5559
5709
  const stats = await collectStats(forkPath);
5560
- spinnerSuccess("Finished counting by raw line of code");
5561
- if (options.json) {
5562
- console.info(JSON.stringify(stats, null, 2));
5563
- } else {
5564
- printStats(stats, options.verbose ?? false);
5710
+ spinnerSuccess("counted raw lines of code");
5711
+ printStats(stats, config.verbose);
5712
+ console.info();
5713
+ printCoverageSummary(forkPath, { refresh: config.coverage });
5714
+ }
5715
+
5716
+ // src/services/stats-since.ts
5717
+ var kindLabels = {
5718
+ source: "source",
5719
+ test: "tests",
5720
+ stories: "stories",
5721
+ generated: "generated",
5722
+ json: "json",
5723
+ docs: "docs",
5724
+ other: "other"
5725
+ };
5726
+ var docExtensions = /* @__PURE__ */ new Set(["md", "mdx"]);
5727
+ var lockFiles = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"]);
5728
+ var codeExtensions = [...sourceExtensions].filter((ext) => !ext.startsWith("json"));
5729
+ function classifyChange(filePath) {
5730
+ const name = filePath.slice(filePath.lastIndexOf("/") + 1);
5731
+ if (lockFiles.has(name)) return "generated";
5732
+ const category = classifyFile(filePath);
5733
+ if (category !== "other") return category;
5734
+ const ext = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : "";
5735
+ if (docExtensions.has(ext)) return "docs";
5736
+ return sourceExtensions.has(ext) ? "source" : "other";
5737
+ }
5738
+ function classifyLine(text) {
5739
+ const line = text.trim();
5740
+ if (!line) return "blank";
5741
+ if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*") || line.startsWith("{/*")) {
5742
+ return "comments";
5743
+ }
5744
+ return "code";
5745
+ }
5746
+ function parseNumstat(output) {
5747
+ const files = [];
5748
+ for (const record of output.split("\0")) {
5749
+ if (!record) continue;
5750
+ const [added, removed, ...rest] = record.split(" ");
5751
+ const path = rest.join(" ");
5752
+ if (!path) continue;
5753
+ files.push({
5754
+ path,
5755
+ kind: classifyChange(path),
5756
+ added: Number.parseInt(added, 10) || 0,
5757
+ removed: Number.parseInt(removed, 10) || 0
5758
+ });
5759
+ }
5760
+ return files;
5761
+ }
5762
+ function countLineContent(patch, isCounted) {
5763
+ const content = {
5764
+ code: { added: 0, removed: 0 },
5765
+ comments: { added: 0, removed: 0 },
5766
+ blank: { added: 0, removed: 0 }
5767
+ };
5768
+ let counted = false;
5769
+ let remaining = 0;
5770
+ for (const line of patch.split("\n")) {
5771
+ if (remaining > 0) {
5772
+ if (line.startsWith("\\")) continue;
5773
+ remaining--;
5774
+ if (!counted) continue;
5775
+ const change = line.startsWith("+") ? "added" : "removed";
5776
+ content[classifyLine(line.slice(1))][change]++;
5777
+ continue;
5778
+ }
5779
+ if (line.startsWith("diff --git ")) {
5780
+ counted = false;
5781
+ continue;
5782
+ }
5783
+ if (line.startsWith("--- a/") || line.startsWith("+++ b/")) {
5784
+ counted = isCounted(line.slice(6));
5785
+ continue;
5786
+ }
5787
+ const hunk = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/.exec(line);
5788
+ if (hunk) remaining = Number(hunk[1] ?? 1) + Number(hunk[2] ?? 1);
5789
+ }
5790
+ return content;
5791
+ }
5792
+ async function collectBranchStats(forkPath, since) {
5793
+ const sinceSha = await git(["rev-parse", "--verify", "--quiet", `${since}^{commit}`], forkPath, {
5794
+ ignoreErrors: true
5795
+ });
5796
+ if (!sinceSha) throw new Error(`unknown ref '${since}'. pass a branch, tag or commit, such as origin/main`);
5797
+ const mergeBase = await git(["merge-base", sinceSha, "HEAD"], forkPath, { ignoreErrors: true });
5798
+ if (!mergeBase) throw new Error(`the current branch shares no history with '${since}'`);
5799
+ const range = [mergeBase, "HEAD"];
5800
+ const files = parseNumstat(await git(["diff", "--numstat", "-z", "--no-renames", ...range], forkPath));
5801
+ const kindOf = new Map(files.map((file) => [file.path, file.kind]));
5802
+ const emptyKind = () => ({ files: 0, added: 0, removed: 0 });
5803
+ const kinds = {
5804
+ source: emptyKind(),
5805
+ test: emptyKind(),
5806
+ stories: emptyKind(),
5807
+ generated: emptyKind(),
5808
+ json: emptyKind(),
5809
+ docs: emptyKind(),
5810
+ other: emptyKind()
5811
+ };
5812
+ const total = emptyKind();
5813
+ const packages = await getWorkspacePackages(forkPath).catch(() => []);
5814
+ const byPackage = /* @__PURE__ */ new Map();
5815
+ const add = (target, file) => {
5816
+ target.files++;
5817
+ target.added += file.added;
5818
+ target.removed += file.removed;
5819
+ };
5820
+ for (const file of files) {
5821
+ add(total, file);
5822
+ add(kinds[file.kind], file);
5823
+ if (file.kind !== "source") continue;
5824
+ const pkg = matchPackage(file.path, packages);
5825
+ const entry = byPackage.get(pkg) ?? emptyKind();
5826
+ add(entry, file);
5827
+ byPackage.set(pkg, entry);
5828
+ }
5829
+ const patch = await git(
5830
+ ["diff", "-U0", "--no-renames", "--no-color", ...range, "--", ...codeExtensions.map((ext) => `*.${ext}`)],
5831
+ forkPath,
5832
+ { ignoreErrors: true }
5833
+ );
5834
+ const sourceContent = kinds.source.files > 0 && !patch ? null : countLineContent(patch, (path) => kindOf.get(path) === "source");
5835
+ const sourceByPackage = Object.fromEntries(
5836
+ [...byPackage.entries()].sort((a, b) => net(b[1]) - net(a[1]) || a[0].localeCompare(b[0]))
5837
+ );
5838
+ const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], forkPath);
5839
+ const status = await git(["status", "--porcelain"], forkPath, { ignoreErrors: true });
5840
+ return {
5841
+ since,
5842
+ mergeBase: mergeBase.slice(0, 7),
5843
+ head: branch === "HEAD" ? await git(["rev-parse", "--short", "HEAD"], forkPath) : branch,
5844
+ total,
5845
+ kinds,
5846
+ sourceContent,
5847
+ sourceByPackage,
5848
+ files: files.sort((a, b) => net(b) - net(a) || a.path.localeCompare(b.path)),
5849
+ uncommitted: status.split("\n").filter(Boolean).length
5850
+ };
5851
+ }
5852
+ function net(change) {
5853
+ return change.added - change.removed;
5854
+ }
5855
+ function signed(n) {
5856
+ return n > 0 ? `+${n}` : String(n);
5857
+ }
5858
+ function changedKinds(stats) {
5859
+ return Object.entries(stats.kinds).filter(([, change]) => change.files > 0).sort((a, b) => net(b[1]) - net(a[1]));
5860
+ }
5861
+ function formatNetList(parts) {
5862
+ return parts.filter(([, change]) => change.added > 0 || change.removed > 0).map(([label, change]) => `${label} ${signed(net(change))}`).join(", ");
5863
+ }
5864
+ function formatMarkdown(stats) {
5865
+ const row = (label, change) => `| ${label} | ${change.files} | ${signed(change.added)} | ${signed(-change.removed)} | ${signed(net(change))} |`;
5866
+ const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
5867
+ const lines = [
5868
+ "| Kind | Files | Added | Removed | Net |",
5869
+ "| --- | ---: | ---: | ---: | ---: |",
5870
+ ...changedKinds(stats).map(
5871
+ ([kind, change]) => row(kind === "json" ? "JSON" : capitalize(kindLabels[kind]), change)
5872
+ ),
5873
+ `| **Total** | **${stats.total.files}** | **${signed(stats.total.added)}** | **${signed(-stats.total.removed)}** | **${signed(net(stats.total))}** |`
5874
+ ];
5875
+ const notes = [];
5876
+ if (stats.sourceContent) {
5877
+ const content = formatNetList(Object.entries(stats.sourceContent));
5878
+ if (content) notes.push(`Source lines by content: ${content}.`);
5879
+ }
5880
+ const byPackage = formatNetList(Object.entries(stats.sourceByPackage));
5881
+ if (byPackage) notes.push(`Source lines by package: ${byPackage}.`);
5882
+ return notes.length > 0 ? [...lines, "", ...notes].join("\n") : lines.join("\n");
5883
+ }
5884
+ function printBranchStats(stats, verbose) {
5885
+ const columns = (files, change, label) => ` ${files.padStart(5)} ${signed(change.added).padStart(8)} ${signed(-change.removed).padStart(8)} ${colors_default.cyan(signed(net(change)).padStart(8))} ${label}`;
5886
+ console.info();
5887
+ console.info(colors_default.dim(` ${"files".padStart(5)} ${"added".padStart(8)} ${"removed".padStart(8)} ${"net".padStart(8)}`));
5888
+ for (const [kind, change] of changedKinds(stats)) {
5889
+ console.info(columns(String(change.files), change, kindLabels[kind]));
5890
+ }
5891
+ console.info(columns(String(stats.total.files), stats.total, colors_default.bold("total")));
5892
+ if (stats.sourceContent && stats.kinds.source.files > 0) {
5893
+ console.info();
5894
+ console.info(colors_default.dim(" source lines by content"));
5895
+ for (const [content, change] of Object.entries(stats.sourceContent)) {
5896
+ console.info(columns("", change, content));
5897
+ }
5898
+ }
5899
+ const packages = Object.entries(stats.sourceByPackage);
5900
+ if (packages.length > 0) {
5565
5901
  console.info();
5566
- printCoverageSummary(forkPath, { refresh: options.refreshCoverage });
5902
+ console.info(colors_default.dim(" source lines by package"));
5903
+ for (const [name, change] of packages) {
5904
+ console.info(columns(String(change.files), change, name));
5905
+ }
5567
5906
  }
5907
+ if (verbose) {
5908
+ console.info();
5909
+ console.info(DIVIDER);
5910
+ console.info();
5911
+ for (const file of stats.files) {
5912
+ console.info(columns("", file, `${file.path} ${colors_default.dim(kindLabels[file.kind])}`));
5913
+ }
5914
+ }
5915
+ if (stats.uncommitted > 0) {
5916
+ console.info();
5917
+ console.info(
5918
+ colors_default.dim(` ${stats.uncommitted} uncommitted file${stats.uncommitted === 1 ? " is" : "s are"} not counted`)
5919
+ );
5920
+ }
5921
+ console.info();
5922
+ }
5923
+ async function runBranchStats(forkPath, options) {
5924
+ createSpinner("counting changed lines...");
5925
+ const stats = await collectBranchStats(forkPath, options.since);
5926
+ spinnerSuccess(`Counted what ${stats.head} changed since ${stats.since} (${stats.mergeBase})`);
5927
+ if (stats.total.files === 0) {
5928
+ console.info(colors_default.dim(` nothing committed since ${stats.since}`));
5929
+ return;
5930
+ }
5931
+ if (options.markdown) {
5932
+ writeStdout(formatMarkdown(stats));
5933
+ if (stats.uncommitted > 0) console.info(colors_default.dim(`${stats.uncommitted} uncommitted file(s) are not counted`));
5934
+ return;
5935
+ }
5936
+ printBranchStats(stats, options.verbose ?? false);
5568
5937
  }
5569
5938
 
5570
5939
  // src/cella-cli.ts
@@ -5583,8 +5952,8 @@ function getForkPath() {
5583
5952
  }
5584
5953
  return cwd;
5585
5954
  }
5586
- async function preflight(forkPath) {
5587
- if (!existsSync13(join21(forkPath, ".git"))) {
5955
+ function preflight(forkPath) {
5956
+ if (!existsSync13(join19(forkPath, ".git"))) {
5588
5957
  throw new Error(`not a git repository: ${forkPath}`);
5589
5958
  }
5590
5959
  }
@@ -5602,7 +5971,7 @@ async function main() {
5602
5971
  const userConfig = await loadConfig(forkPath);
5603
5972
  const config = await parseCli(userConfig, forkPath);
5604
5973
  if (!["audit", "forks", "contributions", "stats"].includes(config.service)) {
5605
- await preflight(forkPath);
5974
+ preflight(forkPath);
5606
5975
  }
5607
5976
  switch (config.service) {
5608
5977
  case "analyze": {
@@ -5613,26 +5982,36 @@ async function main() {
5613
5982
  await runSyncCommand(config);
5614
5983
  break;
5615
5984
  }
5616
- case "migrate":
5985
+ case "migrate": {
5617
5986
  await runMigrate(config);
5618
5987
  break;
5619
- case "audit":
5620
- await runAudit(config, { force: config.force, checkOverrides: config.checkOverrides });
5988
+ }
5989
+ case "audit": {
5990
+ await runAudit(config);
5621
5991
  break;
5622
- case "forks":
5992
+ }
5993
+ case "forks": {
5623
5994
  await runForks(config);
5624
5995
  break;
5625
- case "contributions":
5996
+ }
5997
+ case "contributions": {
5626
5998
  await runContributions(config);
5627
5999
  break;
5628
- case "stats":
5629
- await runStats(config.forkPath, { verbose: config.verbose, refreshCoverage: config.coverage });
6000
+ }
6001
+ case "stats": {
6002
+ if (config.md && !config.since) throw new Error("--md goes with --since <ref>");
6003
+ if (config.since) {
6004
+ await runBranchStats(config.forkPath, { since: config.since, markdown: config.md, verbose: config.verbose });
6005
+ } else {
6006
+ await runStats(config);
6007
+ }
5630
6008
  break;
6009
+ }
5631
6010
  }
5632
6011
  console.info();
5633
6012
  } catch (error) {
5634
6013
  console.error();
5635
- console.error(`${colors_default.red("\u2717")} ${error instanceof Error ? error.message : "unknown error"}`);
6014
+ console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
5636
6015
  process9.exit(1);
5637
6016
  }
5638
6017
  }