@cellajs/cli 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,24 +1,38 @@
1
1
  #!/usr/bin/env tsx
2
2
 
3
3
  // src/cella-cli.ts
4
- import { existsSync as existsSync11, statSync as statSync2 } from "fs";
5
- import { join as join16, resolve as resolve6 } from "path";
6
- import process8 from "process";
4
+ import { existsSync as existsSync9, statSync as statSync2 } from "fs";
5
+ import { join as join16, resolve as resolve5 } from "path";
6
+ import process9 from "process";
7
7
 
8
8
  // src/cli.ts
9
9
  import { readFileSync } from "fs";
10
10
  import { join as join4 } from "path";
11
- import process4 from "process";
11
+ import process5 from "process";
12
12
  import { select } from "@inquirer/prompts";
13
13
  import { Command } from "commander";
14
14
 
15
15
  // src/utils/colors.ts
16
- import process2 from "process";
16
+ import process3 from "process";
17
17
  import { styleText } from "util";
18
+
19
+ // src/utils/env.ts
20
+ import process2 from "process";
21
+ function getEnv(name) {
22
+ return process2.env[name];
23
+ }
24
+ function hasEnv(name) {
25
+ return name in process2.env;
26
+ }
27
+ function getEnvSnapshot() {
28
+ return { ...process2.env };
29
+ }
30
+
31
+ // src/utils/colors.ts
18
32
  function colorsEnabled() {
19
- if ("NO_COLOR" in process2.env) return false;
20
- if ("FORCE_COLOR" in process2.env) return true;
21
- return !!process2.stdout.isTTY;
33
+ if (hasEnv("NO_COLOR")) return false;
34
+ if (hasEnv("FORCE_COLOR")) return true;
35
+ return !!process3.stdout.isTTY;
22
36
  }
23
37
  function applyStyle(style, text) {
24
38
  if (!colorsEnabled()) return text;
@@ -165,18 +179,17 @@ ${z2.prettifyError(result.error)}`);
165
179
  }
166
180
 
167
181
  // src/utils/display.ts
168
- import { spawnSync } from "child_process";
169
- import { existsSync as existsSync2, writeFileSync } from "fs";
182
+ import { writeFileSync } from "fs";
170
183
  import { join as join2, resolve as resolve2 } from "path";
171
- import process3 from "process";
184
+ import process4 from "process";
172
185
 
173
186
  // package.json
174
187
  var package_default = {
175
188
  name: "@cellajs/cli",
176
- version: "0.0.1",
189
+ version: "0.0.3",
177
190
  private: false,
178
191
  license: "MIT",
179
- description: "CLI for syncing and managing apps with upstream Cella.",
192
+ description: "CLI for managing your monorepo and syncing with upstream Cella.",
180
193
  repository: {
181
194
  type: "git",
182
195
  url: "https://github.com/cellajs/cella-cli"
@@ -225,6 +238,7 @@ var package_default = {
225
238
  dependencies: {
226
239
  "@inquirer/core": "^11.2.1",
227
240
  "@inquirer/prompts": "^8.5.2",
241
+ "@pierre/diffs": "^1.2.12",
228
242
  commander: "^15.0.0",
229
243
  "ts-morph": "^28.0.0",
230
244
  zod: "^4.4.3"
@@ -311,7 +325,7 @@ var checkMark = colors_default.green("\u2713");
311
325
  var TerminalSpinner = class {
312
326
  constructor(text, isSilent) {
313
327
  this.text = text;
314
- this.enabled = !isSilent && !!process3.stdout.isTTY;
328
+ this.enabled = !isSilent && !!process4.stdout.isTTY;
315
329
  }
316
330
  text;
317
331
  frames = ["-", "\\", "|", "/"];
@@ -333,10 +347,10 @@ var TerminalSpinner = class {
333
347
  this.timer = null;
334
348
  }
335
349
  if (!this.enabled) return;
336
- process3.stdout.write("\r\x1B[2K");
350
+ process4.stdout.write("\r\x1B[2K");
337
351
  }
338
352
  render() {
339
- process3.stdout.write(`\r${colors_default.cyan(this.frames[this.frameIndex])} ${this.text}`);
353
+ process4.stdout.write(`\r${colors_default.cyan(this.frames[this.frameIndex])} ${this.text}`);
340
354
  }
341
355
  };
342
356
  var activeSpinner = null;
@@ -345,7 +359,7 @@ function setJsonMode(enabled) {
345
359
  jsonMode = enabled;
346
360
  if (!enabled) return;
347
361
  const toStderr = (...args) => {
348
- process3.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
362
+ process4.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
349
363
  `);
350
364
  };
351
365
  console.info = toStderr;
@@ -353,7 +367,7 @@ function setJsonMode(enabled) {
353
367
  console.warn = toStderr;
354
368
  }
355
369
  function writeStdout(text) {
356
- process3.stdout.write(text.endsWith("\n") ? text : `${text}
370
+ process4.stdout.write(text.endsWith("\n") ? text : `${text}
357
371
  `);
358
372
  }
359
373
  function getHeader() {
@@ -376,7 +390,7 @@ function printStep(label, detail) {
376
390
  }
377
391
  }
378
392
  function createSpinner(text) {
379
- const isTestEnv = !!process3.env.VITEST || process3.env.NODE_ENV === "test";
393
+ const isTestEnv = !!getEnv("VITEST") || getEnv("NODE_ENV") === "test";
380
394
  activeSpinner = new TerminalSpinner(text, isTestEnv || jsonMode);
381
395
  activeSpinner.start();
382
396
  return activeSpinner;
@@ -402,23 +416,6 @@ function stopSpinner() {
402
416
  activeSpinner = null;
403
417
  }
404
418
  }
405
- function showDiffInPager(diffOutput) {
406
- if (diffOutput.length === 0) return;
407
- const hasBat = spawnSync("which", ["bat"], { stdio: "pipe" }).status === 0;
408
- process3.stdout.write("\x1B[?25h");
409
- if (hasBat) {
410
- spawnSync("bat", ["--language", "diff", "--paging", "always", "--style", "plain"], {
411
- input: diffOutput,
412
- stdio: ["pipe", "inherit", "inherit"]
413
- });
414
- } else {
415
- spawnSync("less", ["-R"], {
416
- input: diffOutput,
417
- stdio: ["pipe", "inherit", "inherit"]
418
- });
419
- }
420
- process3.stdout.write("\x1B[2J\x1B[0;0H");
421
- }
422
419
  function hyperlink(label, url) {
423
420
  if (!url) return label;
424
421
  const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
@@ -447,18 +444,12 @@ function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, ma
447
444
  return lines.join("\n");
448
445
  }
449
446
  function getFileLink(filePath, commitHash, options) {
450
- const { upstreamGitHubUrl, upstreamBranch, fileLinkMode = "commit", upstreamViewPath, forkPath } = options;
451
- if (fileLinkMode === "local" && upstreamViewPath && forkPath) {
452
- const absolutePath = resolve2(upstreamViewPath, filePath);
453
- if (existsSync2(absolutePath)) {
454
- const url = `vscode://file${absolutePath}`;
455
- return { label: filePath.split("/").pop() || filePath, url };
456
- }
457
- }
447
+ const { upstreamGitHubUrl, upstreamBranch, fileLinkMode = "commit" } = options;
448
+ const mode = fileLinkMode === "local" ? "file" : fileLinkMode;
458
449
  if (!upstreamGitHubUrl) {
459
450
  return { label: commitHash?.slice(0, 9) || "" };
460
451
  }
461
- if (fileLinkMode === "file" && upstreamBranch) {
452
+ if (mode === "file" && upstreamBranch) {
462
453
  const url = `${upstreamGitHubUrl}/blob/${upstreamBranch}/${filePath}`;
463
454
  return { label: filePath.split("/").pop() || filePath, url };
464
455
  }
@@ -625,14 +616,23 @@ function writeLogFile(forkPath, files) {
625
616
  writeFileSync(logPath, lines.join("\n"), "utf-8");
626
617
  return logPath;
627
618
  }
628
- function printSyncComplete(result) {
619
+ function printSyncComplete(result, options = {}) {
629
620
  const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
630
621
  const diverged = result.files.filter((file) => file.status === "diverged").length;
631
622
  const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);
632
623
  const conflicts = result.conflicts.length;
633
624
  console.info();
634
- console.info(`${colors_default.green("\u2713")} sync complete`);
635
- console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
625
+ if (options.stagedBranch) {
626
+ console.info(`${colors_default.green("\u2713")} Sync merge staged on '${options.stagedBranch}'`);
627
+ console.info(
628
+ colors_default.dim(
629
+ ` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. Review, then rerun \`pnpm cella sync\` to finish.`
630
+ )
631
+ );
632
+ } else {
633
+ console.info(`${colors_default.green("\u2713")} sync complete`);
634
+ console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
635
+ }
636
636
  console.info();
637
637
  }
638
638
  function printFlagWarnings(options) {
@@ -651,7 +651,7 @@ function printFlagWarnings(options) {
651
651
  }
652
652
 
653
653
  // src/utils/overrides.ts
654
- import { existsSync as existsSync3 } from "fs";
654
+ import { existsSync as existsSync2 } from "fs";
655
655
  import { join as join3 } from "path";
656
656
  function isUnderAnyFolder(filePath, folders) {
657
657
  return folders.some((folder) => {
@@ -684,7 +684,7 @@ function validateOverrides(config, forkPath) {
684
684
  pattern: entry,
685
685
  message: `pin uses glob (not supported, use a path or folder): ${entry}`
686
686
  });
687
- } else if (!existsSync3(join3(forkPath, entry))) {
687
+ } else if (!existsSync2(join3(forkPath, entry))) {
688
688
  warnings.push({
689
689
  type: "pinned-not-found",
690
690
  pattern: entry,
@@ -700,7 +700,7 @@ function validateOverrides(config, forkPath) {
700
700
  pattern: entry,
701
701
  message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
702
702
  });
703
- } else if (!existsSync3(join3(forkPath, entry.replace(/\/+$/, "")))) {
703
+ } else if (!existsSync2(join3(forkPath, entry.replace(/\/+$/, "")))) {
704
704
  warnings.push({
705
705
  type: "ignored-not-found",
706
706
  pattern: entry,
@@ -751,7 +751,7 @@ var serviceDefinitions = [
751
751
  { flags: "--scope <scope>", description: "analyze scope for --list/--json: all|risk|protected" },
752
752
  { flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
753
753
  { flags: "--diff <path>", description: "print unified diff for one file, then exit" },
754
- { flags: "--open-diff <path>", description: "open VS Code side-by-side diff for one file, then exit" }
754
+ { flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
755
755
  ],
756
756
  includeInMenu: (context) => !context.isUpstreamRepo
757
757
  },
@@ -878,7 +878,7 @@ async function promptForService(userConfig, forkPath) {
878
878
  if (selected === "exit") {
879
879
  console.info(colors_default.dim("exiting..."));
880
880
  console.info();
881
- process4.exit(0);
881
+ process5.exit(0);
882
882
  }
883
883
  console.info();
884
884
  return selected;
@@ -894,7 +894,7 @@ function buildRuntimeConfig(userConfig, forkPath, selection) {
894
894
  };
895
895
  }
896
896
  async function parseCli(userConfig, forkPath) {
897
- const selection = parseCommandLine(process4.argv);
897
+ const selection = parseCommandLine(process5.argv);
898
898
  if (selection.options.json || selection.options.diff) setJsonMode(true);
899
899
  printHeader();
900
900
  const warnings = validateOverrides(userConfig, forkPath);
@@ -909,29 +909,134 @@ async function parseCli(userConfig, forkPath) {
909
909
  }
910
910
 
911
911
  // src/services/analyze.ts
912
- import { basename as basename2 } from "path";
912
+ import { basename as basename3 } from "path";
913
913
 
914
- // src/utils/cleanup.ts
915
- import { existsSync as existsSync5, rmSync } from "fs";
914
+ // src/utils/diff.ts
915
+ import { execFileSync } from "child_process";
916
+ import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
916
917
  import { tmpdir } from "os";
917
- import { basename, join as join7 } from "path";
918
+ import { basename, join as join5 } from "path";
918
919
  import process6 from "process";
920
+ function gitDiffFile(cwd, range, filePath, options = {}) {
921
+ const args = ["diff", "--no-color"];
922
+ if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
923
+ args.push(range, "--", filePath);
924
+ try {
925
+ return execFileSync("git", args, { cwd, maxBuffer: 50 * 1024 * 1024 });
926
+ } catch (error) {
927
+ const stderr = error.stderr?.toString().trim();
928
+ throw new Error(stderr || `failed to diff ${filePath}`);
929
+ }
930
+ }
931
+ function escapeHtml(text) {
932
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
933
+ }
934
+ async function renderDiffPage(patch, meta) {
935
+ const { preloadPatchDiff } = await import("@pierre/diffs/ssr");
936
+ const { prerenderedHTML } = await preloadPatchDiff({
937
+ patch,
938
+ options: {
939
+ diffStyle: "split",
940
+ themeType: "system",
941
+ lineDiffType: "word-alt",
942
+ hunkSeparators: "line-info",
943
+ overflow: "wrap",
944
+ disableFileHeader: true
945
+ }
946
+ });
947
+ const title = `${escapeHtml(meta.filePath)} \xB7 ${escapeHtml(meta.srcLabel)} vs ${escapeHtml(meta.dstLabel)}`;
948
+ const note = meta.note ? `<span class="note">\xB7 ${escapeHtml(meta.note)}</span>` : "";
949
+ return `<!doctype html>
950
+ <html lang="en">
951
+ <head>
952
+ <meta charset="utf-8">
953
+ <meta name="viewport" content="width=device-width, initial-scale=1">
954
+ <title>${title}</title>
955
+ <style>
956
+ :root { color-scheme: light dark; }
957
+ * { box-sizing: border-box; }
958
+ body {
959
+ margin: 0;
960
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
961
+ background: light-dark(#fafafa, #111);
962
+ color: light-dark(#111, #eee);
963
+ }
964
+ header {
965
+ display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap;
966
+ padding: 0.9rem 1.25rem;
967
+ border-bottom: 1px solid light-dark(#e2e2e2, #2a2a2a);
968
+ position: sticky; top: 0;
969
+ background: inherit;
970
+ }
971
+ header .brand { font-weight: 600; color: light-dark(#0891b2, #22d3ee); }
972
+ header code { font-family: ui-monospace, monospace; font-size: 0.9rem; }
973
+ header .sides { margin-left: auto; font-size: 0.8rem; opacity: 0.7; }
974
+ main { max-width: 1400px; margin: 1rem auto; padding: 0 1rem 2rem; }
975
+ .diff-shell {
976
+ border: 1px solid light-dark(#e2e2e2, #2a2a2a);
977
+ border-radius: 8px; overflow: hidden;
978
+ background: light-dark(#fff, #161616);
979
+ }
980
+ </style>
981
+ </head>
982
+ <body>
983
+ <header>
984
+ <span class="brand">\u29C8 cella diff</span>
985
+ <code>${escapeHtml(meta.filePath)}</code>
986
+ <span class="sides">${escapeHtml(meta.srcLabel)} \u2190 \u2192 ${escapeHtml(meta.dstLabel)} ${note}</span>
987
+ </header>
988
+ <main>
989
+ <!-- Declarative shadow DOM: the library's CSS targets :host, which only
990
+ matches inside a shadow root (its browser component renders into one). -->
991
+ <div class="diff-shell">
992
+ <template shadowrootmode="open">
993
+ ${prerenderedHTML}
994
+ </template>
995
+ </div>
996
+ </main>
997
+ </body>
998
+ </html>`;
999
+ }
1000
+ function openWithDefaultApp(target) {
1001
+ try {
1002
+ if (process6.platform === "win32") {
1003
+ execFileSync("rundll32", ["url.dll,FileProtocolHandler", target], { stdio: "ignore" });
1004
+ } else {
1005
+ execFileSync(process6.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
1006
+ }
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ async function openDiffInBrowser(patch, meta, repoPath) {
1011
+ const html = await renderDiffPage(patch, meta);
1012
+ const dir = join5(tmpdir(), `cella-diff-${basename(repoPath)}`);
1013
+ mkdirSync(dir, { recursive: true });
1014
+ const outPath = join5(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
1015
+ writeFileSync2(outPath, html);
1016
+ openWithDefaultApp(outPath);
1017
+ return outPath;
1018
+ }
1019
+
1020
+ // src/utils/cleanup.ts
1021
+ import { existsSync as existsSync4, rmSync } from "fs";
1022
+ import { tmpdir as tmpdir2 } from "os";
1023
+ import { basename as basename2, join as join8 } from "path";
1024
+ import process7 from "process";
919
1025
 
920
1026
  // src/utils/git.ts
921
1027
  import { execFile } from "child_process";
922
- import { existsSync as existsSync4 } from "fs";
1028
+ import { existsSync as existsSync3 } from "fs";
923
1029
  import { mkdir, readdir, rmdir, unlink } from "fs/promises";
924
- import { dirname, join as join6 } from "path";
925
- import process5 from "process";
1030
+ import { dirname, join as join7 } from "path";
926
1031
  import { promisify } from "util";
927
1032
 
928
1033
  // src/utils/manifest.ts
929
1034
  import { readFile, writeFile } from "fs/promises";
930
- import { join as join5 } from "path";
1035
+ import { join as join6 } from "path";
931
1036
  var MANIFEST_FILE = "cella.manifest.json";
932
1037
  async function readSyncManifest(cwd) {
933
1038
  try {
934
- const parsed = JSON.parse(await readFile(join5(cwd, MANIFEST_FILE), "utf8"));
1039
+ const parsed = JSON.parse(await readFile(join6(cwd, MANIFEST_FILE), "utf8"));
935
1040
  const commit = parsed?.upstream?.commit;
936
1041
  if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
937
1042
  return parsed;
@@ -944,7 +1049,7 @@ async function readManifestBase(cwd) {
944
1049
  return manifest ? manifest.upstream.commit.toLowerCase() : null;
945
1050
  }
946
1051
  async function writeSyncManifest(cwd, manifest) {
947
- await writeFile(join5(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
1052
+ await writeFile(join6(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
948
1053
  `, "utf8");
949
1054
  }
950
1055
 
@@ -958,7 +1063,7 @@ function sanitizeCredentials(text) {
958
1063
  }
959
1064
  async function git(args, cwd, options = {}) {
960
1065
  const env = {
961
- ...process5.env,
1066
+ ...getEnvSnapshot(),
962
1067
  ...options.skipEditor ? { GIT_EDITOR: "true" } : {},
963
1068
  ...options.env
964
1069
  };
@@ -1000,13 +1105,23 @@ async function getUpstreamStatus(cwd) {
1000
1105
  const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
1001
1106
  return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
1002
1107
  }
1003
- async function pushBranch(cwd, remote, branch) {
1004
- await git(["push", "-u", remote, branch], cwd);
1108
+ async function pushBranch(cwd, remote, branch, options = {}) {
1109
+ await git(["push", "-u", ...options.forceWithLease ? ["--force-with-lease"] : [], remote, branch], cwd);
1110
+ }
1111
+ async function listBranchMergeCommits(cwd, baseRef) {
1112
+ const out = await git(["rev-list", "--merges", "--first-parent", `${baseRef}..HEAD`], cwd, { ignoreErrors: true });
1113
+ return out ? out.split("\n").filter(Boolean) : [];
1114
+ }
1115
+ async function flattenBranch(cwd, baseRef, message) {
1116
+ const parent = await getMergeBase(cwd, baseRef, "HEAD");
1117
+ const tree = await git(["rev-parse", "HEAD^{tree}"], cwd);
1118
+ const commit = await git(["commit-tree", tree, "-p", parent, "-m", message], cwd);
1119
+ await git(["reset", "--hard", commit], cwd);
1005
1120
  }
1006
1121
  async function commitSquash(cwd, message) {
1007
1122
  for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
1008
- const path4 = join6(cwd, ".git", file);
1009
- if (existsSync4(path4)) await unlink(path4);
1123
+ const path4 = join7(cwd, ".git", file);
1124
+ if (existsSync3(path4)) await unlink(path4);
1010
1125
  }
1011
1126
  await git(["commit", "-m", message], cwd, { skipEditor: true });
1012
1127
  }
@@ -1014,7 +1129,7 @@ async function stageAll(cwd) {
1014
1129
  await git(["add", "-A"], cwd);
1015
1130
  }
1016
1131
  function mergeInProgress(cwd) {
1017
- return existsSync4(join6(cwd, ".git", "MERGE_HEAD"));
1132
+ return existsSync3(join7(cwd, ".git", "MERGE_HEAD"));
1018
1133
  }
1019
1134
  async function getShortSha(cwd, ref) {
1020
1135
  return git(["rev-parse", "--short", ref], cwd);
@@ -1244,15 +1359,15 @@ async function batchUnstageFiles(cwd, filePaths) {
1244
1359
  await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
1245
1360
  }
1246
1361
  async function gitMv(cwd, oldPath, newPath) {
1247
- const newDir = dirname(join6(cwd, newPath));
1362
+ const newDir = dirname(join7(cwd, newPath));
1248
1363
  await mkdir(newDir, { recursive: true });
1249
1364
  await git(["mv", "-f", oldPath, newPath], cwd);
1250
1365
  }
1251
1366
  async function fileExistsInWorktree(cwd, filePath) {
1252
- return existsSync4(join6(cwd, filePath));
1367
+ return existsSync3(join7(cwd, filePath));
1253
1368
  }
1254
1369
  async function removeFileFromWorktree(cwd, filePath) {
1255
- const fullPath = join6(cwd, filePath);
1370
+ const fullPath = join7(cwd, filePath);
1256
1371
  try {
1257
1372
  await unlink(fullPath);
1258
1373
  await cleanupEmptyParentDirs(cwd, dirname(filePath));
@@ -1265,7 +1380,7 @@ async function removeFileFully(cwd, filePath) {
1265
1380
  }
1266
1381
  async function cleanupEmptyParentDirs(cwd, relativePath) {
1267
1382
  if (!relativePath || relativePath === ".") return;
1268
- const fullPath = join6(cwd, relativePath);
1383
+ const fullPath = join7(cwd, relativePath);
1269
1384
  try {
1270
1385
  const entries = await readdir(fullPath);
1271
1386
  if (entries.length === 0) {
@@ -1382,23 +1497,11 @@ var WORKTREE_PREFIXES = {
1382
1497
  view: "cella-view-"
1383
1498
  };
1384
1499
  function buildWorktreePath(kind, repoPath) {
1385
- return join7(tmpdir(), `${WORKTREE_PREFIXES[kind]}${basename(repoPath)}`);
1500
+ return join8(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename2(repoPath)}`);
1386
1501
  }
1387
1502
  function getWorktreePath(repoPath) {
1388
1503
  return buildWorktreePath("sync", repoPath);
1389
1504
  }
1390
- function getViewWorktreePath(repoPath) {
1391
- return buildWorktreePath("view", repoPath);
1392
- }
1393
- async function refreshViewWorktree(repoPath, upstreamRef) {
1394
- const viewPath = getViewWorktreePath(repoPath);
1395
- await removeWorktree(repoPath, viewPath);
1396
- if (existsSync5(viewPath)) {
1397
- rmSync(viewPath, { recursive: true, force: true });
1398
- }
1399
- await createWorktree(repoPath, viewPath, upstreamRef);
1400
- return viewPath;
1401
- }
1402
1505
  var cleanupRegistered = false;
1403
1506
  var currentWorktreePath = null;
1404
1507
  var currentRepoPath = null;
@@ -1412,20 +1515,22 @@ function unregisterWorktree() {
1412
1515
  }
1413
1516
  async function cleanupWorktree(repoPath, worktreePath) {
1414
1517
  await removeWorktree(repoPath, worktreePath);
1415
- if (existsSync5(worktreePath)) {
1518
+ if (existsSync4(worktreePath)) {
1416
1519
  rmSync(worktreePath, { recursive: true, force: true });
1417
1520
  }
1418
1521
  unregisterWorktree();
1419
1522
  }
1420
1523
  async function cleanupLeftoverWorktrees(repoPath) {
1421
- const worktreePath = getWorktreePath(repoPath);
1422
- if (existsSync5(worktreePath)) {
1423
- await cleanupWorktree(repoPath, worktreePath);
1524
+ for (const kind of Object.keys(WORKTREE_PREFIXES)) {
1525
+ const worktreePath = buildWorktreePath(kind, repoPath);
1526
+ if (existsSync4(worktreePath)) {
1527
+ await cleanupWorktree(repoPath, worktreePath);
1528
+ }
1424
1529
  }
1425
1530
  const prefixes = Object.values(WORKTREE_PREFIXES);
1426
1531
  const worktrees = await listWorktrees(repoPath);
1427
1532
  for (const wt of worktrees) {
1428
- if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync5(wt)) {
1533
+ if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync4(wt)) {
1429
1534
  await removeWorktree(repoPath, wt);
1430
1535
  }
1431
1536
  }
@@ -1447,56 +1552,15 @@ async function handleAbort(signal) {
1447
1552
  );
1448
1553
  }
1449
1554
  }
1450
- process6.exit(1);
1555
+ process7.exit(1);
1451
1556
  }
1452
1557
  function registerSignalHandlers() {
1453
1558
  if (cleanupRegistered) return;
1454
- process6.on("SIGINT", () => handleAbort("SIGINT"));
1455
- process6.on("SIGTERM", () => handleAbort("SIGTERM"));
1559
+ process7.on("SIGINT", () => handleAbort("SIGINT"));
1560
+ process7.on("SIGTERM", () => handleAbort("SIGTERM"));
1456
1561
  cleanupRegistered = true;
1457
1562
  }
1458
1563
 
1459
- // src/utils/diff.ts
1460
- import { spawnSync as spawnSync2 } from "child_process";
1461
- import { existsSync as existsSync6, mkdtempSync, writeFileSync as writeFileSync2 } from "fs";
1462
- import { tmpdir as tmpdir2 } from "os";
1463
- import { join as join8, resolve as resolve3 } from "path";
1464
- function gitDiffFile(cwd, range, filePath, options) {
1465
- const args = ["diff"];
1466
- if (options.color === "always") args.push("--color=always");
1467
- if (options.color === "never") args.push("--no-color");
1468
- args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`, range, "--", filePath);
1469
- const result = spawnSync2("git", args, { cwd, maxBuffer: 50 * 1024 * 1024 });
1470
- if (result.status !== 0) {
1471
- const stderr = result.stderr?.toString().trim();
1472
- throw new Error(stderr || `failed to diff ${filePath}`);
1473
- }
1474
- return result.stdout;
1475
- }
1476
- function openVsCodeDiff(forkPath, upstreamRef, viewPath, filePath) {
1477
- const forkFile = resolve3(forkPath, filePath);
1478
- if (viewPath) {
1479
- const upstreamFile = resolve3(viewPath, filePath);
1480
- if (existsSync6(upstreamFile)) {
1481
- spawnSync2("code", ["--diff", upstreamFile, forkFile], { stdio: "ignore" });
1482
- return;
1483
- }
1484
- }
1485
- const tmpDir = mkdtempSync(join8(tmpdir2(), "cella-analyze-diff-"));
1486
- const fileName = filePath.split("/").pop() || "file";
1487
- const tmpFile = join8(tmpDir, `upstream-${fileName}`);
1488
- const result = spawnSync2("git", ["show", `${upstreamRef}:${filePath}`], {
1489
- cwd: forkPath,
1490
- stdio: ["pipe", "pipe", "pipe"]
1491
- });
1492
- if (result.status !== 0) {
1493
- const stderr = result.stderr.toString().trim();
1494
- throw new Error(stderr || `failed to read upstream version for ${filePath}`);
1495
- }
1496
- writeFileSync2(tmpFile, result.stdout);
1497
- spawnSync2("code", ["--diff", tmpFile, forkFile], { stdio: "ignore" });
1498
- }
1499
-
1500
1564
  // src/services/analyze-core.ts
1501
1565
  async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predicates, onProgress) {
1502
1566
  onProgress?.("collecting file hashes (batch)...");
@@ -2009,8 +2073,7 @@ function findTargetFile(files, targetPath) {
2009
2073
  }
2010
2074
  function printUnifiedDiff(config, filePath) {
2011
2075
  const diff = gitDiffFile(config.forkPath, `${config.upstreamRef}..HEAD`, filePath, {
2012
- dstPrefix: basename2(config.forkPath),
2013
- color: "never"
2076
+ dstPrefix: basename3(config.forkPath)
2014
2077
  });
2015
2078
  writeStdout(diff.toString());
2016
2079
  }
@@ -2052,19 +2115,32 @@ async function runAnalyze(config) {
2052
2115
  }
2053
2116
  return result;
2054
2117
  }
2055
- const upstreamViewPath = await refreshViewWorktree(config.forkPath, config.upstreamRef);
2056
2118
  if (config.openDiff) {
2057
2119
  const file = findTargetFile(scopedFiles, config.openDiff) ?? findTargetFile(result.files, config.openDiff);
2058
2120
  if (!file) throw new Error(`file not found in analysis results: ${config.openDiff}`);
2059
- openVsCodeDiff(config.forkPath, config.upstreamRef, upstreamViewPath, file.path);
2060
- console.info(`${colors_default.green("\u2713")} opened VS Code diff for ${file.path}`);
2121
+ const patch = gitDiffFile(config.forkPath, config.upstreamRef, file.path);
2122
+ if (patch.length === 0) {
2123
+ console.info(`${colors_default.green("\u2713")} ${file.path} is identical to upstream`);
2124
+ return result;
2125
+ }
2126
+ const pagePath = await openDiffInBrowser(
2127
+ patch.toString(),
2128
+ {
2129
+ filePath: file.path,
2130
+ srcLabel: "cella",
2131
+ dstLabel: basename3(config.forkPath),
2132
+ note: "local working tree"
2133
+ },
2134
+ config.forkPath
2135
+ );
2136
+ console.info(`${colors_default.green("\u2713")} opened browser diff for ${file.path}`);
2137
+ console.info(colors_default.dim(` ${pagePath}`));
2061
2138
  return result;
2062
2139
  }
2063
2140
  const linkOptions = {
2064
2141
  upstreamGitHubUrl: result.upstreamGitHubUrl,
2065
2142
  upstreamBranch: result.upstreamBranch,
2066
2143
  fileLinkMode: config.settings.fileLinkMode,
2067
- upstreamViewPath,
2068
2144
  forkPath: config.forkPath
2069
2145
  };
2070
2146
  printAnalysisFileGroups(result.files, linkOptions);
@@ -2079,7 +2155,7 @@ async function runAnalyze(config) {
2079
2155
  }
2080
2156
 
2081
2157
  // src/services/audit.ts
2082
- import { spawnSync as spawnSync3 } from "child_process";
2158
+ import { spawnSync } from "child_process";
2083
2159
  import * as fs2 from "fs";
2084
2160
  import * as path2 from "path";
2085
2161
  import { checkbox, confirm, Separator } from "@inquirer/prompts";
@@ -2370,7 +2446,7 @@ async function runAudit(config, options = {}) {
2370
2446
  if (options.force) {
2371
2447
  spinnerText("clearing pnpm metadata cache...");
2372
2448
  clearCache();
2373
- spawnSync3("pnpm", ["cache", "delete", "*"], {
2449
+ spawnSync("pnpm", ["cache", "delete", "*"], {
2374
2450
  cwd: forkPath,
2375
2451
  stdio: "ignore"
2376
2452
  });
@@ -2660,7 +2736,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
2660
2736
  }
2661
2737
  if (runAuditFix) {
2662
2738
  console.info(colors_default.dim("running pnpm audit --fix override..."));
2663
- const auditFixResult = spawnSync3("pnpm", ["audit", "--fix", "override"], {
2739
+ const auditFixResult = spawnSync("pnpm", ["audit", "--fix", "override"], {
2664
2740
  cwd: forkPath,
2665
2741
  stdio: "inherit"
2666
2742
  });
@@ -2680,7 +2756,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
2680
2756
  const selectedCatalogPackages = selectedPackages.filter((pkgName) => catalogPackages.has(pkgName));
2681
2757
  if (selectedCatalogPackages.length > 0) {
2682
2758
  console.info(colors_default.dim(`normalizing catalog consumers: ${selectedCatalogPackages.join(", ")}`));
2683
- const normalizeResult = spawnSync3("pnpm", ["-r", "up", ...selectedCatalogPackages], {
2759
+ const normalizeResult = spawnSync("pnpm", ["-r", "up", ...selectedCatalogPackages], {
2684
2760
  cwd: forkPath,
2685
2761
  stdio: "inherit"
2686
2762
  });
@@ -2690,7 +2766,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
2690
2766
  return;
2691
2767
  }
2692
2768
  }
2693
- const result = spawnSync3("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
2769
+ const result = spawnSync("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
2694
2770
  cwd: forkPath,
2695
2771
  stdio: "inherit"
2696
2772
  });
@@ -2967,16 +3043,16 @@ ${commitBody}`;
2967
3043
  }
2968
3044
 
2969
3045
  // src/services/fork-utils.ts
2970
- import { existsSync as existsSync8 } from "fs";
2971
- import { resolve as resolve4 } from "path";
3046
+ import { existsSync as existsSync6 } from "fs";
3047
+ import { resolve as resolve3 } from "path";
2972
3048
  function validateForkPath(fork, basePath, requireConfig = false) {
2973
- const resolvedPath = resolve4(basePath, fork.localPath);
2974
- if (!resolvedPath.startsWith(resolve4(basePath, ".."))) {
3049
+ const resolvedPath = resolve3(basePath, fork.localPath);
3050
+ if (!resolvedPath.startsWith(resolve3(basePath, ".."))) {
2975
3051
  return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
2976
3052
  }
2977
- if (!existsSync8(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
2978
- if (!existsSync8(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
2979
- if (requireConfig && !existsSync8(`${resolvedPath}/cella.config.ts`)) {
3053
+ if (!existsSync6(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
3054
+ if (!existsSync6(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
3055
+ if (requireConfig && !existsSync6(`${resolvedPath}/cella.config.ts`)) {
2980
3056
  return { fork, resolvedPath, valid: false, error: "missing cella.config.ts" };
2981
3057
  }
2982
3058
  return { fork, resolvedPath, valid: true };
@@ -2988,9 +3064,10 @@ function printNoForksHint(action) {
2988
3064
  }
2989
3065
 
2990
3066
  // src/services/contributions.ts
2991
- function showContribDiff(item, baseRef, cwd, forkName) {
2992
- const diff = gitDiffFile(cwd, `${baseRef}..${item.ref}`, item.path, { dstPrefix: forkName, color: "always" });
2993
- showDiffInPager(diff);
3067
+ async function showContribDiff(item, baseRef, cwd, forkName) {
3068
+ const patch = gitDiffFile(cwd, `${baseRef}..${item.ref}`, item.path);
3069
+ if (patch.length === 0) return null;
3070
+ return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
2994
3071
  }
2995
3072
  function shouldApplyContributionsUnstaged(repoPath) {
2996
3073
  return JSON.parse(readFileSync4(join12(repoPath, "package.json"), "utf8")).name === "cella";
@@ -3053,8 +3130,12 @@ var contribPrompt = createPrompt((config, done) => {
3053
3130
  return;
3054
3131
  }
3055
3132
  if (key.name === "d") {
3056
- showContribDiff(items[active], baseRef, cwd, forkName);
3057
- setStatusMsg(`viewed ${items[active].path}`);
3133
+ try {
3134
+ const pagePath = await showContribDiff(items[active], baseRef, cwd, forkName);
3135
+ setStatusMsg(pagePath ? `opened ${items[active].path} in browser` : `no changes for ${items[active].path}`);
3136
+ } catch (error) {
3137
+ setStatusMsg(`diff failed: ${error instanceof Error ? error.message : "unknown error"}`);
3138
+ }
3058
3139
  return;
3059
3140
  }
3060
3141
  if (key.name === "a") {
@@ -3095,7 +3176,7 @@ var contribPrompt = createPrompt((config, done) => {
3095
3176
  const header = `${colors_default.cyan("\u2B07")} ${config.message} ${colors_default.dim(`(${countInfo})`)}`;
3096
3177
  const keys = [
3097
3178
  ["\u2191\u2193", "navigate"],
3098
- ["d", "diff"],
3179
+ ["d", "diff in browser"],
3099
3180
  ["a", "(de)select all"],
3100
3181
  ["space", "select"],
3101
3182
  ["\u23CE", "accept"],
@@ -3350,15 +3431,15 @@ async function runContributions(config) {
3350
3431
  }
3351
3432
 
3352
3433
  // src/services/forks.ts
3353
- import { resolve as resolve5 } from "path";
3354
- import process7 from "process";
3434
+ import { resolve as resolve4 } from "path";
3435
+ import process8 from "process";
3355
3436
  import { Separator as Separator2, select as select3 } from "@inquirer/prompts";
3356
3437
 
3357
3438
  // src/services/sync.ts
3358
- import { spawnSync as spawnSync4 } from "child_process";
3439
+ import { spawnSync as spawnSync2 } from "child_process";
3359
3440
 
3360
3441
  // src/services/packages.ts
3361
- import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
3442
+ import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
3362
3443
  import { join as join13 } from "path";
3363
3444
  function parseComparableVersion(version) {
3364
3445
  const trimmed = version.trim();
@@ -3463,7 +3544,7 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
3463
3544
  return { merged, changed };
3464
3545
  }
3465
3546
  function readPackageJson(filePath) {
3466
- if (!existsSync9(filePath)) return null;
3547
+ if (!existsSync7(filePath)) return null;
3467
3548
  try {
3468
3549
  return JSON.parse(readFileSync5(filePath, "utf-8"));
3469
3550
  } catch {
@@ -3485,7 +3566,7 @@ async function getUpstreamPackageJson(forkPath, upstreamRef, relativePath) {
3485
3566
  async function discoverPackageLocations(forkPath, upstreamRef) {
3486
3567
  const stdout = await git(["ls-tree", "-r", "--name-only", upstreamRef], forkPath, { ignoreErrors: true });
3487
3568
  const upstreamPaths = stdout.split("\n").filter((p) => p.endsWith("/package.json") || p === "package.json").map((p) => p === "package.json" ? "" : p.replace("/package.json", ""));
3488
- return upstreamPaths.filter((loc) => existsSync9(join13(forkPath, loc, "package.json")));
3569
+ return upstreamPaths.filter((loc) => existsSync7(join13(forkPath, loc, "package.json")));
3489
3570
  }
3490
3571
  async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync) {
3491
3572
  const changes = [];
@@ -3544,7 +3625,7 @@ async function runPackages(config, options = {}) {
3544
3625
  const keysToSync = config.settings.packageJsonSync || ["dependencies", "devDependencies"];
3545
3626
  const conflictedSet = new Set(options.conflictedFiles ?? []);
3546
3627
  const locations = await discoverPackageLocations(config.forkPath, config.upstreamRef);
3547
- const results = [];
3628
+ let changedCount = 0;
3548
3629
  const skipped = [];
3549
3630
  for (const location of locations) {
3550
3631
  const pkgRelPath = location ? `${location}/package.json` : "package.json";
@@ -3553,22 +3634,15 @@ async function runPackages(config, options = {}) {
3553
3634
  continue;
3554
3635
  }
3555
3636
  spinnerText(`syncing ${location || "root"}/package.json...`);
3556
- const { updated, changes } = await syncPackageJson(config.forkPath, config.upstreamRef, location, keysToSync);
3637
+ const { updated } = await syncPackageJson(config.forkPath, config.upstreamRef, location, keysToSync);
3557
3638
  if (updated) {
3558
- results.push({ location: location || "root", changes });
3559
- }
3560
- }
3561
- spinnerSuccess("package sync complete");
3562
- if (results.length === 0) {
3563
- console.info(colors_default.dim(" no package.json changes needed"));
3564
- } else {
3565
- for (const { location, changes } of results) {
3566
- console.info(` ${colors_default.green("\u2713")} ${location}/package.json`);
3567
- for (const change of changes) {
3568
- console.info(` ${colors_default.dim("\u2192")} ${change}`);
3569
- }
3639
+ changedCount += 1;
3570
3640
  }
3571
3641
  }
3642
+ spinnerSuccess(
3643
+ "package sync complete",
3644
+ `${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
3645
+ );
3572
3646
  if (skipped.length > 0) {
3573
3647
  console.info();
3574
3648
  console.warn(
@@ -3633,13 +3707,13 @@ async function runSync(config, options) {
3633
3707
  spinnerFail("sync completed with conflicts");
3634
3708
  }
3635
3709
  printSummary(result.summary, "merge summary");
3636
- options?.onAfterSummary?.(result);
3637
3710
  if (config.logFile) {
3638
3711
  const logPath = writeLogFile(config.forkPath, result.files);
3639
3712
  console.info();
3640
3713
  console.info(colors_default.dim(`full file list written to: ${logPath}`));
3641
3714
  }
3642
- printSyncComplete(result);
3715
+ const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
3716
+ printSyncComplete(result, { stagedBranch });
3643
3717
  printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
3644
3718
  return result;
3645
3719
  }
@@ -3657,21 +3731,35 @@ function printFinishSteps() {
3657
3731
  function hasStagedSyncChanges(result) {
3658
3732
  return result.files.some((file) => ["behind", "diverged", "renamed", "ignored", "pinned"].includes(file.status));
3659
3733
  }
3660
- function printCleanSyncNextStep(branch) {
3661
- console.info();
3662
- console.info(`${checkMark} ${colors_default.green(`Sync merge staged on '${branch}'. No conflicts. Review, then finish with:`)}`);
3663
- printFinishSteps();
3664
- console.info(colors_default.dim(" rerun commits the sync, pushes the branch, and opens a PR."));
3665
- }
3666
3734
  function ghAvailable() {
3667
- return spawnSync4("gh", ["--version"], { stdio: "ignore" }).status === 0;
3735
+ return spawnSync2("gh", ["--version"], { stdio: "ignore" }).status === 0;
3736
+ }
3737
+ function extractFirstUrl(output) {
3738
+ return output.match(/https?:\/\/\S+/)?.[0];
3739
+ }
3740
+ async function flattenSyncBranch(forkPath, branch, base) {
3741
+ const mergeCommits = await listBranchMergeCommits(forkPath, base);
3742
+ if (mergeCommits.length === 0) return false;
3743
+ const upstreamSha = await getShortSha(forkPath, `${mergeCommits[0]}^2`).catch(() => "");
3744
+ const message = upstreamSha ? `${SYNC_PR_TITLE} ${upstreamSha}` : SYNC_PR_TITLE;
3745
+ console.info(
3746
+ colors_default.yellow(
3747
+ `'${branch}' contains ${mergeCommits.length} merge commit(s) \u2014 the PR would list the entire upstream history.`
3748
+ )
3749
+ );
3750
+ console.info(colors_default.dim(`flattening '${branch}' to a single commit (same content)...`));
3751
+ await flattenBranch(forkPath, base, message);
3752
+ return true;
3668
3753
  }
3669
3754
  async function shipSyncBranch(config, branch) {
3670
3755
  const { forkPath, settings } = config;
3671
3756
  const base = resolveReleaseBase(settings);
3757
+ const flattened = await flattenSyncBranch(forkPath, branch, base);
3758
+ let prUrl;
3759
+ let prOpened = false;
3672
3760
  console.info(colors_default.dim(`pushing '${branch}' to origin...`));
3673
3761
  try {
3674
- await pushBranch(forkPath, "origin", branch);
3762
+ await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
3675
3763
  } catch (error) {
3676
3764
  const detail = error instanceof Error ? error.message : String(error);
3677
3765
  console.info(colors_default.yellow(`push failed (${detail.split("\n")[0]}). finish manually:`));
@@ -3680,12 +3768,17 @@ async function shipSyncBranch(config, branch) {
3680
3768
  }
3681
3769
  if (ghAvailable()) {
3682
3770
  console.info(colors_default.dim("opening a pull request..."));
3683
- const pr = spawnSync4("gh", ["pr", "create", "--base", base, "--head", branch, "--title", SYNC_PR_TITLE, "--fill"], {
3771
+ const pr = spawnSync2("gh", ["pr", "create", "--base", base, "--head", branch, "--title", SYNC_PR_TITLE, "--fill"], {
3684
3772
  cwd: forkPath,
3685
- stdio: "inherit"
3773
+ encoding: "utf-8",
3774
+ stdio: ["ignore", "pipe", "pipe"]
3686
3775
  });
3687
- if (pr.status !== 0) {
3776
+ prUrl = extractFirstUrl(`${pr.stdout ?? ""}
3777
+ ${pr.stderr ?? ""}`);
3778
+ prOpened = pr.status === 0;
3779
+ if (!prOpened) {
3688
3780
  console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
3781
+ if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
3689
3782
  printPrCreateStep(branch, base);
3690
3783
  }
3691
3784
  } else {
@@ -3696,12 +3789,19 @@ async function shipSyncBranch(config, branch) {
3696
3789
  await switchBranch(forkPath, base);
3697
3790
  await pullFastForward(forkPath).catch(() => {
3698
3791
  });
3699
- console.info(colors_default.green(`done \u2014 '${branch}' is pushed and you're back on '${base}'.`));
3792
+ console.info();
3793
+ if (prUrl) {
3794
+ console.info(`${colors_default.green("\u2713")} Sync pull request ${prOpened ? "opened" : "ready"}`);
3795
+ console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, back on '${base}'`));
3796
+ } else {
3797
+ console.info(`${colors_default.green("\u2713")} Sync branch pushed`);
3798
+ console.info(colors_default.dim(` '${branch}' is on origin, back on '${base}'`));
3799
+ }
3700
3800
  }
3701
3801
  function finalizeWorkspace(forkPath) {
3702
3802
  for (const args of [["install"], ["check"]]) {
3703
3803
  console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
3704
- const result = spawnSync4("pnpm", args, { cwd: forkPath, stdio: "inherit" });
3804
+ const result = spawnSync2("pnpm", args, { cwd: forkPath, stdio: "inherit" });
3705
3805
  if (result.status !== 0) return false;
3706
3806
  }
3707
3807
  return true;
@@ -3709,13 +3809,7 @@ function finalizeWorkspace(forkPath) {
3709
3809
  async function runSyncCycle(config) {
3710
3810
  const { forkPath } = config;
3711
3811
  const branch = await setupTemporarySyncBranch(config);
3712
- const result = await runSync(config, {
3713
- onAfterSummary: (syncResult) => {
3714
- if (syncResult.conflicts.length === 0 && hasStagedSyncChanges(syncResult)) {
3715
- printCleanSyncNextStep(branch.temporaryBranch);
3716
- }
3717
- }
3718
- });
3812
+ const result = await runSync(config, { stagedBranch: branch.temporaryBranch });
3719
3813
  if (config.settings.syncWithPackages !== false) {
3720
3814
  await runPackages(config, { conflictedFiles: result.conflicts });
3721
3815
  }
@@ -3734,7 +3828,7 @@ async function resumeSyncMerge(config, branch) {
3734
3828
  if (conflicts.length > 0) {
3735
3829
  console.info(colors_default.yellow(`${conflicts.length} file(s) still conflict on '${branch}':`));
3736
3830
  for (const file of conflicts) console.info(colors_default.dim(` ${file}`));
3737
- console.info(colors_default.dim("resolve and stage them, then re-run `pnpm cella sync` to finish."));
3831
+ console.info(colors_default.dim("resolve and stage them, then re-run `pnpm cella sync` to finish (it commits for you)."));
3738
3832
  return;
3739
3833
  }
3740
3834
  await stageAll(forkPath);
@@ -3766,14 +3860,12 @@ async function runSyncCommand(config) {
3766
3860
  console.info();
3767
3861
  console.info(
3768
3862
  colors_default.yellow(
3769
- `sync branch '${currentBranch}' has uncommitted changes that would not be shipped.
3770
- commit them (\`git commit --amend --no-edit\` or a new commit), then re-run \`pnpm cella sync\`.`
3863
+ `sync branch '${currentBranch}' has uncommitted changes.
3864
+ commit them first (\`git commit --amend --no-edit\` or a new commit).`
3771
3865
  )
3772
3866
  );
3773
3867
  return;
3774
3868
  }
3775
- console.info();
3776
- console.info(colors_default.green(`sync branch '${currentBranch}' is already committed.`));
3777
3869
  await shipSyncBranch(config, currentBranch);
3778
3870
  return;
3779
3871
  }
@@ -3786,8 +3878,10 @@ commit them (\`git commit --amend --no-edit\` or a new commit), then re-run \`pn
3786
3878
  }
3787
3879
  const { temporaryBranch } = outcome.branch;
3788
3880
  if (outcome.status === "conflicts") {
3789
- console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve them and then:`)}`);
3881
+ console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve and stage them, then:`)}`);
3790
3882
  printFinishSteps();
3883
+ console.info(colors_default.dim(" rerun commits the sync, pushes the branch, and opens a PR."));
3884
+ console.info(colors_default.dim(" let the rerun commit \u2014 a manual `git commit` records a merge commit that bloats the PR."));
3791
3885
  }
3792
3886
  }
3793
3887
 
@@ -3877,7 +3971,7 @@ async function runForks(config) {
3877
3971
  console.error(colors_default.red(`fork '${config.fork}' not found in config`));
3878
3972
  return;
3879
3973
  }
3880
- const resolvedPath = resolve5(config.forkPath, match.localPath);
3974
+ const resolvedPath = resolve4(config.forkPath, match.localPath);
3881
3975
  await syncFork(config, match, resolvedPath);
3882
3976
  return;
3883
3977
  }
@@ -3890,9 +3984,9 @@ async function runForks(config) {
3890
3984
  loop: false
3891
3985
  });
3892
3986
  if (selectedPath === "_exit") {
3893
- process7.exit(0);
3987
+ process8.exit(0);
3894
3988
  }
3895
- const resolvedForkPath = resolve5(config.forkPath, selectedPath);
3989
+ const resolvedForkPath = resolve4(config.forkPath, selectedPath);
3896
3990
  const selectedFork = forks.find((f) => f.localPath === selectedPath);
3897
3991
  if (!selectedFork) {
3898
3992
  console.error(colors_default.red(`fork '${selectedPath}' not found in config`));
@@ -3908,7 +4002,7 @@ import { readFile as readFile2 } from "fs/promises";
3908
4002
  import { join as join15 } from "path";
3909
4003
 
3910
4004
  // src/utils/coverage-utils.ts
3911
- import { spawnSync as spawnSync5 } from "child_process";
4005
+ import { spawnSync as spawnSync3 } from "child_process";
3912
4006
  import * as fs3 from "fs";
3913
4007
  import * as path3 from "path";
3914
4008
  var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -3931,7 +4025,7 @@ function formatAge(ms) {
3931
4025
  }
3932
4026
  function refreshCoverage(forkPath) {
3933
4027
  console.info(colors_default.dim("running `pnpm test` (this spins up Docker and runs the full suite)..."));
3934
- const result = spawnSync5("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
4028
+ const result = spawnSync3("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
3935
4029
  return result.status === 0;
3936
4030
  }
3937
4031
  function printCoverageSummary(forkPath, options = {}) {
@@ -4148,27 +4242,27 @@ async function runStats(forkPath, options = {}) {
4148
4242
 
4149
4243
  // src/cella-cli.ts
4150
4244
  function getForkPath() {
4151
- const envPath = process8.env.CELLA_FORK_PATH;
4245
+ const envPath = getEnv("CELLA_FORK_PATH");
4152
4246
  if (envPath) {
4153
- const resolved = resolve6(envPath);
4154
- if (!existsSync11(resolved) || !statSync2(resolved).isDirectory()) {
4247
+ const resolved = resolve5(envPath);
4248
+ if (!existsSync9(resolved) || !statSync2(resolved).isDirectory()) {
4155
4249
  throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
4156
4250
  }
4157
4251
  return resolved;
4158
4252
  }
4159
- let cwd = process8.cwd();
4253
+ let cwd = process9.cwd();
4160
4254
  if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
4161
- cwd = resolve6(cwd, "../..");
4255
+ cwd = resolve5(cwd, "../..");
4162
4256
  }
4163
4257
  return cwd;
4164
4258
  }
4165
4259
  async function preflight(forkPath) {
4166
- if (!existsSync11(join16(forkPath, ".git"))) {
4260
+ if (!existsSync9(join16(forkPath, ".git"))) {
4167
4261
  throw new Error(`not a git repository: ${forkPath}`);
4168
4262
  }
4169
4263
  }
4170
4264
  function isHelpOrVersionRequest() {
4171
- return process8.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process8.argv[2] === "help";
4265
+ return process9.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process9.argv[2] === "help";
4172
4266
  }
4173
4267
  async function main() {
4174
4268
  registerSignalHandlers();
@@ -4209,7 +4303,7 @@ async function main() {
4209
4303
  } catch (error) {
4210
4304
  console.error();
4211
4305
  console.error(`${colors_default.red("\u2717")} ${error instanceof Error ? error.message : "unknown error"}`);
4212
- process8.exit(1);
4306
+ process9.exit(1);
4213
4307
  }
4214
4308
  }
4215
4309
  main();