@cellajs/cli 0.0.3 → 0.0.4

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,13 +1,13 @@
1
1
  #!/usr/bin/env tsx
2
2
 
3
3
  // src/cella-cli.ts
4
- import { existsSync as existsSync9, statSync as statSync2 } from "fs";
5
- import { join as join16, resolve as resolve5 } from "path";
4
+ import { existsSync as existsSync10, statSync as statSync2 } from "fs";
5
+ import { join as join17, resolve as resolve5 } from "path";
6
6
  import process9 from "process";
7
7
 
8
8
  // src/cli.ts
9
9
  import { readFileSync } from "fs";
10
- import { join as join4 } from "path";
10
+ import { join as join5 } from "path";
11
11
  import process5 from "process";
12
12
  import { select } from "@inquirer/prompts";
13
13
  import { Command } from "commander";
@@ -53,8 +53,8 @@ var pc = {
53
53
  var colors_default = pc;
54
54
 
55
55
  // src/utils/config.ts
56
- import { existsSync, realpathSync } from "fs";
57
- import { join, resolve } from "path";
56
+ import { existsSync as existsSync2, realpathSync } from "fs";
57
+ import { join as join2, resolve } from "path";
58
58
  import { z as z2 } from "zod";
59
59
 
60
60
  // src/config/types.ts
@@ -96,9 +96,11 @@ var cellaConfigSchema = z.object({
96
96
  }).strict();
97
97
 
98
98
  // src/utils/module-territory.ts
99
- import { relative, sep } from "path";
99
+ import { existsSync } from "fs";
100
+ import { basename, join, relative, sep } from "path";
100
101
  import { Project, SyntaxKind } from "ts-morph";
101
102
  var MODULE_FILE_GLOBS = ["backend/src/modules/*/*-module.ts", "frontend/src/modules/*/*-module.ts"];
103
+ var FRONTEND_STATIC_ROOT = "frontend/public/static";
102
104
  function readOwner(call) {
103
105
  const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
104
106
  const initializer = arg?.getProperty("owner")?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
@@ -112,11 +114,19 @@ function resolveAppModuleFolders(repoPath) {
112
114
  });
113
115
  const sourceFiles = project.addSourceFilesAtPaths(MODULE_FILE_GLOBS.map((glob) => `${repoPath}/${glob}`));
114
116
  const folders = /* @__PURE__ */ new Set();
117
+ const moduleNames = /* @__PURE__ */ new Set();
115
118
  for (const sourceFile of sourceFiles) {
116
119
  const ownsApp = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => call.getExpression().getText() === "registerModule" && readOwner(call) === "app");
117
120
  if (!ownsApp) continue;
118
121
  const folder = relative(repoPath, sourceFile.getDirectoryPath()).split(sep).join("/");
119
122
  folders.add(folder);
123
+ moduleNames.add(basename(folder));
124
+ }
125
+ for (const moduleName of moduleNames) {
126
+ const staticFolder = `${FRONTEND_STATIC_ROOT}/${moduleName}`;
127
+ if (existsSync(join(repoPath, staticFolder))) {
128
+ folders.add(staticFolder);
129
+ }
120
130
  }
121
131
  return [...folders];
122
132
  }
@@ -145,8 +155,8 @@ function resolveUpstream(settings) {
145
155
  return { track, branchRef };
146
156
  }
147
157
  async function loadConfig(forkPath) {
148
- const configPath = join(forkPath, "cella.config.ts");
149
- if (!existsSync(configPath)) {
158
+ const configPath = join2(forkPath, "cella.config.ts");
159
+ if (!existsSync2(configPath)) {
150
160
  throw new Error(`config file not found: ${configPath}`);
151
161
  }
152
162
  const realConfigPath = realpathSync(configPath);
@@ -180,13 +190,13 @@ ${z2.prettifyError(result.error)}`);
180
190
 
181
191
  // src/utils/display.ts
182
192
  import { writeFileSync } from "fs";
183
- import { join as join2, resolve as resolve2 } from "path";
193
+ import { join as join3, resolve as resolve2 } from "path";
184
194
  import process4 from "process";
185
195
 
186
196
  // package.json
187
197
  var package_default = {
188
198
  name: "@cellajs/cli",
189
- version: "0.0.3",
199
+ version: "0.0.4",
190
200
  private: false,
191
201
  license: "MIT",
192
202
  description: "CLI for managing your monorepo and syncing with upstream Cella.",
@@ -505,7 +515,7 @@ var summaryStatusConfig = {
505
515
  icon: colors_default.cyan("\u25C7"),
506
516
  label: "managed",
507
517
  color: colors_default.cyan,
508
- description: "package/config files handled separately"
518
+ description: "package.jsons & cella.config.ts handled separately"
509
519
  },
510
520
  ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
511
521
  };
@@ -593,7 +603,7 @@ function printAnalysisFileGroups(files, linkOptions) {
593
603
  });
594
604
  }
595
605
  function writeLogFile(forkPath, files) {
596
- const logPath = join2(forkPath, "cella-sync.log");
606
+ const logPath = join3(forkPath, "cella-sync.log");
597
607
  const lines = [
598
608
  `cella sync analysis - ${(/* @__PURE__ */ new Date()).toISOString()}`,
599
609
  DIVIDER,
@@ -651,8 +661,8 @@ function printFlagWarnings(options) {
651
661
  }
652
662
 
653
663
  // src/utils/overrides.ts
654
- import { existsSync as existsSync2 } from "fs";
655
- import { join as join3 } from "path";
664
+ import { existsSync as existsSync3 } from "fs";
665
+ import { join as join4 } from "path";
656
666
  function isUnderAnyFolder(filePath, folders) {
657
667
  return folders.some((folder) => {
658
668
  const entry = folder.replace(/\/+$/, "");
@@ -684,7 +694,7 @@ function validateOverrides(config, forkPath) {
684
694
  pattern: entry,
685
695
  message: `pin uses glob (not supported, use a path or folder): ${entry}`
686
696
  });
687
- } else if (!existsSync2(join3(forkPath, entry))) {
697
+ } else if (!existsSync3(join4(forkPath, entry))) {
688
698
  warnings.push({
689
699
  type: "pinned-not-found",
690
700
  pattern: entry,
@@ -700,7 +710,7 @@ function validateOverrides(config, forkPath) {
700
710
  pattern: entry,
701
711
  message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
702
712
  });
703
- } else if (!existsSync2(join3(forkPath, entry.replace(/\/+$/, "")))) {
713
+ } else if (!existsSync3(join4(forkPath, entry.replace(/\/+$/, "")))) {
704
714
  warnings.push({
705
715
  type: "ignored-not-found",
706
716
  pattern: entry,
@@ -810,7 +820,7 @@ var serviceDefinitions = [
810
820
  async function getMenuContext(userConfig, forkPath) {
811
821
  return {
812
822
  hasForks: (userConfig.forks?.length ?? 0) > 0,
813
- isUpstreamRepo: JSON.parse(readFileSync(join4(forkPath, "package.json"), "utf8")).name === "cella"
823
+ isUpstreamRepo: JSON.parse(readFileSync(join5(forkPath, "package.json"), "utf8")).name === "cella"
814
824
  };
815
825
  }
816
826
  function buildServiceChoices(context) {
@@ -909,13 +919,13 @@ async function parseCli(userConfig, forkPath) {
909
919
  }
910
920
 
911
921
  // src/services/analyze.ts
912
- import { basename as basename3 } from "path";
922
+ import { basename as basename4 } from "path";
913
923
 
914
924
  // src/utils/diff.ts
915
925
  import { execFileSync } from "child_process";
916
926
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
917
927
  import { tmpdir } from "os";
918
- import { basename, join as join5 } from "path";
928
+ import { basename as basename2, join as join6 } from "path";
919
929
  import process6 from "process";
920
930
  function gitDiffFile(cwd, range, filePath, options = {}) {
921
931
  const args = ["diff", "--no-color"];
@@ -1009,34 +1019,34 @@ function openWithDefaultApp(target) {
1009
1019
  }
1010
1020
  async function openDiffInBrowser(patch, meta, repoPath) {
1011
1021
  const html = await renderDiffPage(patch, meta);
1012
- const dir = join5(tmpdir(), `cella-diff-${basename(repoPath)}`);
1022
+ const dir = join6(tmpdir(), `cella-diff-${basename2(repoPath)}`);
1013
1023
  mkdirSync(dir, { recursive: true });
1014
- const outPath = join5(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
1024
+ const outPath = join6(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
1015
1025
  writeFileSync2(outPath, html);
1016
1026
  openWithDefaultApp(outPath);
1017
1027
  return outPath;
1018
1028
  }
1019
1029
 
1020
1030
  // src/utils/cleanup.ts
1021
- import { existsSync as existsSync4, rmSync } from "fs";
1031
+ import { existsSync as existsSync5, rmSync } from "fs";
1022
1032
  import { tmpdir as tmpdir2 } from "os";
1023
- import { basename as basename2, join as join8 } from "path";
1033
+ import { basename as basename3, join as join9 } from "path";
1024
1034
  import process7 from "process";
1025
1035
 
1026
1036
  // src/utils/git.ts
1027
1037
  import { execFile } from "child_process";
1028
- import { existsSync as existsSync3 } from "fs";
1038
+ import { existsSync as existsSync4 } from "fs";
1029
1039
  import { mkdir, readdir, rmdir, unlink } from "fs/promises";
1030
- import { dirname, join as join7 } from "path";
1040
+ import { dirname, join as join8 } from "path";
1031
1041
  import { promisify } from "util";
1032
1042
 
1033
1043
  // src/utils/manifest.ts
1034
1044
  import { readFile, writeFile } from "fs/promises";
1035
- import { join as join6 } from "path";
1045
+ import { join as join7 } from "path";
1036
1046
  var MANIFEST_FILE = "cella.manifest.json";
1037
1047
  async function readSyncManifest(cwd) {
1038
1048
  try {
1039
- const parsed = JSON.parse(await readFile(join6(cwd, MANIFEST_FILE), "utf8"));
1049
+ const parsed = JSON.parse(await readFile(join7(cwd, MANIFEST_FILE), "utf8"));
1040
1050
  const commit = parsed?.upstream?.commit;
1041
1051
  if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
1042
1052
  return parsed;
@@ -1049,7 +1059,7 @@ async function readManifestBase(cwd) {
1049
1059
  return manifest ? manifest.upstream.commit.toLowerCase() : null;
1050
1060
  }
1051
1061
  async function writeSyncManifest(cwd, manifest) {
1052
- await writeFile(join6(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
1062
+ await writeFile(join7(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
1053
1063
  `, "utf8");
1054
1064
  }
1055
1065
 
@@ -1120,8 +1130,8 @@ async function flattenBranch(cwd, baseRef, message) {
1120
1130
  }
1121
1131
  async function commitSquash(cwd, message) {
1122
1132
  for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
1123
- const path4 = join7(cwd, ".git", file);
1124
- if (existsSync3(path4)) await unlink(path4);
1133
+ const path4 = join8(cwd, ".git", file);
1134
+ if (existsSync4(path4)) await unlink(path4);
1125
1135
  }
1126
1136
  await git(["commit", "-m", message], cwd, { skipEditor: true });
1127
1137
  }
@@ -1129,7 +1139,7 @@ async function stageAll(cwd) {
1129
1139
  await git(["add", "-A"], cwd);
1130
1140
  }
1131
1141
  function mergeInProgress(cwd) {
1132
- return existsSync3(join7(cwd, ".git", "MERGE_HEAD"));
1142
+ return existsSync4(join8(cwd, ".git", "MERGE_HEAD"));
1133
1143
  }
1134
1144
  async function getShortSha(cwd, ref) {
1135
1145
  return git(["rev-parse", "--short", ref], cwd);
@@ -1359,15 +1369,15 @@ async function batchUnstageFiles(cwd, filePaths) {
1359
1369
  await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
1360
1370
  }
1361
1371
  async function gitMv(cwd, oldPath, newPath) {
1362
- const newDir = dirname(join7(cwd, newPath));
1372
+ const newDir = dirname(join8(cwd, newPath));
1363
1373
  await mkdir(newDir, { recursive: true });
1364
1374
  await git(["mv", "-f", oldPath, newPath], cwd);
1365
1375
  }
1366
1376
  async function fileExistsInWorktree(cwd, filePath) {
1367
- return existsSync3(join7(cwd, filePath));
1377
+ return existsSync4(join8(cwd, filePath));
1368
1378
  }
1369
1379
  async function removeFileFromWorktree(cwd, filePath) {
1370
- const fullPath = join7(cwd, filePath);
1380
+ const fullPath = join8(cwd, filePath);
1371
1381
  try {
1372
1382
  await unlink(fullPath);
1373
1383
  await cleanupEmptyParentDirs(cwd, dirname(filePath));
@@ -1380,7 +1390,7 @@ async function removeFileFully(cwd, filePath) {
1380
1390
  }
1381
1391
  async function cleanupEmptyParentDirs(cwd, relativePath) {
1382
1392
  if (!relativePath || relativePath === ".") return;
1383
- const fullPath = join7(cwd, relativePath);
1393
+ const fullPath = join8(cwd, relativePath);
1384
1394
  try {
1385
1395
  const entries = await readdir(fullPath);
1386
1396
  if (entries.length === 0) {
@@ -1408,14 +1418,28 @@ async function getStoredSyncHead(cwd) {
1408
1418
  const ref = await git(["rev-parse", "refs/cella/last-sync-head"], cwd, { ignoreErrors: true });
1409
1419
  return ref || null;
1410
1420
  }
1411
- async function readManifestBaseAtRef(cwd, ref) {
1421
+ async function readManifestAtRef(cwd, ref) {
1412
1422
  const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
1413
1423
  if (!raw) return null;
1414
1424
  try {
1415
1425
  const parsed = JSON.parse(raw);
1416
1426
  const commit = parsed?.upstream?.commit;
1417
1427
  if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
1418
- return commit.toLowerCase();
1428
+ return parsed;
1429
+ } catch {
1430
+ return null;
1431
+ }
1432
+ }
1433
+ async function readManifestBaseAtRef(cwd, ref) {
1434
+ const manifest = await readManifestAtRef(cwd, ref);
1435
+ return manifest ? manifest.upstream.commit.toLowerCase() : null;
1436
+ }
1437
+ async function readPackageVersionAtRef(cwd, ref) {
1438
+ const raw = await git(["show", `${ref}:package.json`], cwd, { ignoreErrors: true });
1439
+ if (!raw) return null;
1440
+ try {
1441
+ const version = JSON.parse(raw).version;
1442
+ return typeof version === "string" && version ? version : null;
1419
1443
  } catch {
1420
1444
  return null;
1421
1445
  }
@@ -1433,28 +1457,82 @@ async function getRootCommit(cwd, ref) {
1433
1457
  const roots = out.split("\n").filter(Boolean);
1434
1458
  return roots.length > 0 ? roots[roots.length - 1] : null;
1435
1459
  }
1460
+ var SCAFFOLD_CANDIDATE_LIMIT = 400;
1461
+ var SCAFFOLD_SCORE_CONCURRENCY = 10;
1462
+ var SCAFFOLD_MATCH_MAX_DIFF_RATIO = 0.3;
1463
+ async function inferScaffoldBase(cwd, headRef, upstreamRef) {
1464
+ const root = await getRootCommit(cwd, headRef);
1465
+ if (!root) return null;
1466
+ const rootDate = await git(["show", "-s", "--format=%cI", root], cwd, { ignoreErrors: true });
1467
+ if (!rootDate) return null;
1468
+ const before = new Date(new Date(rootDate).getTime() + 24 * 60 * 60 * 1e3).toISOString();
1469
+ const revList = await git(
1470
+ ["rev-list", `--max-count=${SCAFFOLD_CANDIDATE_LIMIT}`, `--before=${before}`, upstreamRef],
1471
+ cwd,
1472
+ { ignoreErrors: true }
1473
+ );
1474
+ const candidates = revList.split("\n").filter(Boolean);
1475
+ if (candidates.length === 0) return null;
1476
+ const totalPaths = (await git(["ls-tree", "-r", "--name-only", root], cwd)).split("\n").filter(Boolean).length;
1477
+ if (totalPaths === 0) return null;
1478
+ let best = null;
1479
+ outer: for (let i = 0; i < candidates.length; i += SCAFFOLD_SCORE_CONCURRENCY) {
1480
+ const chunk = candidates.slice(i, i + SCAFFOLD_SCORE_CONCURRENCY);
1481
+ const scores = await Promise.all(
1482
+ chunk.map(async (sha) => {
1483
+ try {
1484
+ const out = await git(["diff-tree", "-r", "--name-only", sha, root], cwd);
1485
+ return { sha, diff: out ? out.split("\n").length : 0 };
1486
+ } catch {
1487
+ return { sha, diff: Number.POSITIVE_INFINITY };
1488
+ }
1489
+ })
1490
+ );
1491
+ for (const score of scores) {
1492
+ if (!best || score.diff < best.diff) best = score;
1493
+ if (best.diff === 0) break outer;
1494
+ }
1495
+ }
1496
+ if (!best || best.diff / totalPaths > SCAFFOLD_MATCH_MAX_DIFF_RATIO) return null;
1497
+ return best.sha;
1498
+ }
1499
+ async function readRootTrailerBase(cwd, headRef) {
1500
+ const root = await getRootCommit(cwd, headRef);
1501
+ if (!root) return null;
1502
+ const message = await git(["show", "-s", "--format=%B", root], cwd, { ignoreErrors: true });
1503
+ const match = message.match(/^cella-base:\s*([0-9a-f]{40})$/im);
1504
+ return match ? match[1].toLowerCase() : null;
1505
+ }
1436
1506
  async function ensureSyncBase(cwd, headRef, upstreamRef) {
1437
1507
  const nativeBase = await git(["merge-base", headRef, upstreamRef], cwd, { ignoreErrors: true });
1438
1508
  if (nativeBase) return;
1439
- const baseSha = await getStoredSyncRef(cwd) ?? await readManifestBaseAtRef(cwd, headRef) ?? await readManifestBase(cwd);
1509
+ const sources = [
1510
+ () => getStoredSyncRef(cwd),
1511
+ () => readManifestBaseAtRef(cwd, headRef),
1512
+ () => readManifestBase(cwd),
1513
+ () => readRootTrailerBase(cwd, headRef),
1514
+ () => inferScaffoldBase(cwd, headRef, upstreamRef)
1515
+ ];
1516
+ let baseSha = null;
1517
+ for (const source of sources) {
1518
+ const sha = await source();
1519
+ if (sha && await commitObjectExists(cwd, sha)) {
1520
+ baseSha = sha;
1521
+ break;
1522
+ }
1523
+ }
1440
1524
  if (!baseSha) {
1441
1525
  throw new Error(
1442
- `no common ancestor between the fork and '${upstreamRef}', and no sync base recorded.
1526
+ `no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
1443
1527
 
1444
1528
  This fork has unrelated history with upstream \u2014 typical for a create-cella scaffold, or after
1445
- upstream squashed its history. Seed the base with the upstream commit the fork was created from,
1446
- then re-run sync:
1529
+ upstream squashed its history \u2014 and no recorded or inferable base commit exists after fetching
1530
+ upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
1447
1531
 
1448
1532
  git update-ref refs/cella/last-sync <upstream-sha>
1449
1533
  `
1450
1534
  );
1451
1535
  }
1452
- if (!await commitObjectExists(cwd, baseSha)) {
1453
- throw new Error(
1454
- `recorded sync base ${baseSha.slice(0, 12)} is not present after fetching '${upstreamRef}'.
1455
- It may belong to a different upstream, or have been dropped by an upstream history rewrite.`
1456
- );
1457
- }
1458
1536
  const root = await getRootCommit(cwd, headRef);
1459
1537
  if (!root) throw new Error("could not determine the fork's root commit to bootstrap sync ancestry");
1460
1538
  await git(["replace", "-f", "--graft", root, baseSha], cwd);
@@ -1497,7 +1575,7 @@ var WORKTREE_PREFIXES = {
1497
1575
  view: "cella-view-"
1498
1576
  };
1499
1577
  function buildWorktreePath(kind, repoPath) {
1500
- return join8(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename2(repoPath)}`);
1578
+ return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
1501
1579
  }
1502
1580
  function getWorktreePath(repoPath) {
1503
1581
  return buildWorktreePath("sync", repoPath);
@@ -1515,7 +1593,7 @@ function unregisterWorktree() {
1515
1593
  }
1516
1594
  async function cleanupWorktree(repoPath, worktreePath) {
1517
1595
  await removeWorktree(repoPath, worktreePath);
1518
- if (existsSync4(worktreePath)) {
1596
+ if (existsSync5(worktreePath)) {
1519
1597
  rmSync(worktreePath, { recursive: true, force: true });
1520
1598
  }
1521
1599
  unregisterWorktree();
@@ -1523,14 +1601,14 @@ async function cleanupWorktree(repoPath, worktreePath) {
1523
1601
  async function cleanupLeftoverWorktrees(repoPath) {
1524
1602
  for (const kind of Object.keys(WORKTREE_PREFIXES)) {
1525
1603
  const worktreePath = buildWorktreePath(kind, repoPath);
1526
- if (existsSync4(worktreePath)) {
1604
+ if (existsSync5(worktreePath)) {
1527
1605
  await cleanupWorktree(repoPath, worktreePath);
1528
1606
  }
1529
1607
  }
1530
1608
  const prefixes = Object.values(WORKTREE_PREFIXES);
1531
1609
  const worktrees = await listWorktrees(repoPath);
1532
1610
  for (const wt of worktrees) {
1533
- if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync4(wt)) {
1611
+ if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync5(wt)) {
1534
1612
  await removeWorktree(repoPath, wt);
1535
1613
  }
1536
1614
  }
@@ -2073,7 +2151,7 @@ function findTargetFile(files, targetPath) {
2073
2151
  }
2074
2152
  function printUnifiedDiff(config, filePath) {
2075
2153
  const diff = gitDiffFile(config.forkPath, `${config.upstreamRef}..HEAD`, filePath, {
2076
- dstPrefix: basename3(config.forkPath)
2154
+ dstPrefix: basename4(config.forkPath)
2077
2155
  });
2078
2156
  writeStdout(diff.toString());
2079
2157
  }
@@ -2128,7 +2206,7 @@ async function runAnalyze(config) {
2128
2206
  {
2129
2207
  filePath: file.path,
2130
2208
  srcLabel: "cella",
2131
- dstLabel: basename3(config.forkPath),
2209
+ dstLabel: basename4(config.forkPath),
2132
2210
  note: "local working tree"
2133
2211
  },
2134
2212
  config.forkPath
@@ -2923,7 +3001,7 @@ async function checkOverrides(forkPath) {
2923
3001
 
2924
3002
  // src/services/contributions.ts
2925
3003
  import { readFileSync as readFileSync4 } from "fs";
2926
- import { join as join12 } from "path";
3004
+ import { join as join13 } from "path";
2927
3005
  import {
2928
3006
  createPrompt,
2929
3007
  isDownKey,
@@ -2938,7 +3016,7 @@ import {
2938
3016
  import { select as select2 } from "@inquirer/prompts";
2939
3017
 
2940
3018
  // src/services/contrib-core.ts
2941
- import { dirname as dirname2, join as join11 } from "path";
3019
+ import { dirname as dirname2, join as join12 } from "path";
2942
3020
  async function listFilesAtRef(repoPath, ref) {
2943
3021
  const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
2944
3022
  return new Set(raw.split("\n").filter(Boolean));
@@ -3000,7 +3078,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
3000
3078
  }
3001
3079
  async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
3002
3080
  const branch = `contrib/${forkName}`;
3003
- const tmpIndex = join11(repoPath, ".git", `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
3081
+ const tmpIndex = join12(repoPath, ".git", `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
3004
3082
  const indexEnv = { GIT_INDEX_FILE: tmpIndex };
3005
3083
  try {
3006
3084
  await git(["read-tree", baseRef], repoPath, { env: indexEnv });
@@ -3043,16 +3121,16 @@ ${commitBody}`;
3043
3121
  }
3044
3122
 
3045
3123
  // src/services/fork-utils.ts
3046
- import { existsSync as existsSync6 } from "fs";
3124
+ import { existsSync as existsSync7 } from "fs";
3047
3125
  import { resolve as resolve3 } from "path";
3048
3126
  function validateForkPath(fork, basePath, requireConfig = false) {
3049
3127
  const resolvedPath = resolve3(basePath, fork.localPath);
3050
3128
  if (!resolvedPath.startsWith(resolve3(basePath, ".."))) {
3051
3129
  return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
3052
3130
  }
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`)) {
3131
+ if (!existsSync7(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
3132
+ if (!existsSync7(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
3133
+ if (requireConfig && !existsSync7(`${resolvedPath}/cella.config.ts`)) {
3056
3134
  return { fork, resolvedPath, valid: false, error: "missing cella.config.ts" };
3057
3135
  }
3058
3136
  return { fork, resolvedPath, valid: true };
@@ -3070,7 +3148,7 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
3070
3148
  return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
3071
3149
  }
3072
3150
  function shouldApplyContributionsUnstaged(repoPath) {
3073
- return JSON.parse(readFileSync4(join12(repoPath, "package.json"), "utf8")).name === "cella";
3151
+ return JSON.parse(readFileSync4(join13(repoPath, "package.json"), "utf8")).name === "cella";
3074
3152
  }
3075
3153
  var contribPrompt = createPrompt((config, done) => {
3076
3154
  const { pageSize = 20, baseRef, cwd, forkName } = config;
@@ -3417,11 +3495,6 @@ async function runContributions(config) {
3417
3495
  spinnerSuccess(`applied ${applied} files ${colors_default.dim(applyUnstaged ? "(unstaged)" : "(staged)")}`);
3418
3496
  }
3419
3497
  console.info();
3420
- console.info(` ${colors_default.cyan(forkName)} ${colors_default.dim(`(${selected.length} files)`)}`);
3421
- for (const item of selected) {
3422
- console.info(` ${colors_default.dim("\u2192")} ${item.deleted ? `(deleted) ${item.path}` : item.path}`);
3423
- }
3424
- console.info();
3425
3498
  console.info(
3426
3499
  colors_default.dim(
3427
3500
  applyUnstaged ? " files are unstaged \u2014 review, stage what you want, and commit when ready" : " files are staged \u2014 review and commit when ready"
@@ -3439,8 +3512,8 @@ import { Separator as Separator2, select as select3 } from "@inquirer/prompts";
3439
3512
  import { spawnSync as spawnSync2 } from "child_process";
3440
3513
 
3441
3514
  // src/services/packages.ts
3442
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
3443
- import { join as join13 } from "path";
3515
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
3516
+ import { join as join14 } from "path";
3444
3517
  function parseComparableVersion(version) {
3445
3518
  const trimmed = version.trim();
3446
3519
  if (trimmed === "" || trimmed === "*" || trimmed.includes("workspace:") || trimmed.includes("catalog:") || trimmed.includes("file:") || trimmed.includes("link:") || trimmed.includes("git+") || trimmed.includes("github:") || trimmed.includes("http://") || trimmed.includes("https://") || trimmed.includes("||")) {
@@ -3544,7 +3617,7 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
3544
3617
  return { merged, changed };
3545
3618
  }
3546
3619
  function readPackageJson(filePath) {
3547
- if (!existsSync7(filePath)) return null;
3620
+ if (!existsSync8(filePath)) return null;
3548
3621
  try {
3549
3622
  return JSON.parse(readFileSync5(filePath, "utf-8"));
3550
3623
  } catch {
@@ -3566,11 +3639,11 @@ async function getUpstreamPackageJson(forkPath, upstreamRef, relativePath) {
3566
3639
  async function discoverPackageLocations(forkPath, upstreamRef) {
3567
3640
  const stdout = await git(["ls-tree", "-r", "--name-only", upstreamRef], forkPath, { ignoreErrors: true });
3568
3641
  const upstreamPaths = stdout.split("\n").filter((p) => p.endsWith("/package.json") || p === "package.json").map((p) => p === "package.json" ? "" : p.replace("/package.json", ""));
3569
- return upstreamPaths.filter((loc) => existsSync7(join13(forkPath, loc, "package.json")));
3642
+ return upstreamPaths.filter((loc) => existsSync8(join14(forkPath, loc, "package.json")));
3570
3643
  }
3571
3644
  async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync) {
3572
3645
  const changes = [];
3573
- const pkgPath = join13(forkPath, relativePath, "package.json");
3646
+ const pkgPath = join14(forkPath, relativePath, "package.json");
3574
3647
  const forkPkg = readPackageJson(pkgPath);
3575
3648
  const upstreamPkg = await getUpstreamPackageJson(forkPath, upstreamRef, relativePath);
3576
3649
  if (!forkPkg || !upstreamPkg) {
@@ -3718,12 +3791,57 @@ async function runSync(config, options) {
3718
3791
  return result;
3719
3792
  }
3720
3793
  var SYNC_PR_TITLE = "chore: sync upstream cella";
3721
- function printPrCreateStep(branch, base) {
3722
- console.info(colors_default.dim(` gh pr create --base ${base} --head ${branch} --title "${SYNC_PR_TITLE}" --fill`));
3794
+ var PR_BODY_COMMIT_MAX = 50;
3795
+ async function buildSyncCommitMessage(forkPath, upstreamRef) {
3796
+ const shortSha = await getShortSha(forkPath, upstreamRef).catch(() => "");
3797
+ if (!shortSha) return SYNC_PR_TITLE;
3798
+ const manifest = await readSyncManifest(forkPath);
3799
+ const version = manifest?.upstream.release ?? await readPackageVersionAtRef(forkPath, upstreamRef).catch(() => null);
3800
+ return version ? `${SYNC_PR_TITLE} v${version.replace(/^v/, "")} (${shortSha})` : `${SYNC_PR_TITLE} ${shortSha}`;
3801
+ }
3802
+ function qualifyPrRefs(subject, repoSlug) {
3803
+ if (!repoSlug) return subject;
3804
+ return subject.replace(/(^|[^\w/])#(\d+)\b/g, `$1${repoSlug}#$2`);
3805
+ }
3806
+ function buildSyncPrBody(input) {
3807
+ const { repoSlug, version, fromSha, toSha, commits, totalCount } = input;
3808
+ const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
3809
+ const short = (sha) => sha.slice(0, 7);
3810
+ const commitRef = (sha) => githubUrl ? `[\`${short(sha)}\`](${githubUrl}/commit/${sha})` : `\`${short(sha)}\``;
3811
+ const upstreamName = githubUrl ? `[${repoSlug}](${githubUrl})` : "cella";
3812
+ const versionSuffix = version ? ` (v${version.replace(/^v/, "")})` : "";
3813
+ const lines = [`Syncs upstream ${upstreamName} to ${commitRef(toSha)}${versionSuffix}.`];
3814
+ if (totalCount > 0 && fromSha) {
3815
+ const compare = githubUrl ? ` ([compare](${githubUrl}/compare/${short(fromSha)}...${short(toSha)}))` : "";
3816
+ lines.push("", `**${totalCount} upstream commit${totalCount === 1 ? "" : "s"} since last sync**${compare}:`, "");
3817
+ if (totalCount > commits.length) lines.push(`- \u2026${totalCount - commits.length} earlier commit(s) not shown`);
3818
+ for (const commit of commits) {
3819
+ lines.push(`- ${commitRef(commit.hash)} ${qualifyPrRefs(commit.message, repoSlug)}`);
3820
+ }
3821
+ }
3822
+ return `${lines.join("\n")}
3823
+ `;
3824
+ }
3825
+ async function buildSyncPrBodyForBranch(forkPath, base) {
3826
+ const manifest = await readManifestAtRef(forkPath, "HEAD");
3827
+ if (!manifest) return void 0;
3828
+ const toSha = manifest.upstream.commit.toLowerCase();
3829
+ const fromSha = await readManifestBaseAtRef(forkPath, base) ?? await getMergeBase(forkPath, base, toSha).catch(() => null);
3830
+ const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
3831
+ const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
3832
+ const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, {
3833
+ oldestFirst: true,
3834
+ skip: totalCount > PR_BODY_COMMIT_MAX ? totalCount - PR_BODY_COMMIT_MAX : 0,
3835
+ limit: PR_BODY_COMMIT_MAX
3836
+ }) : [];
3837
+ return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount });
3838
+ }
3839
+ function printPrCreateStep(branch, base, title = SYNC_PR_TITLE) {
3840
+ console.info(colors_default.dim(` gh pr create --base ${base} --head ${branch} --title "${title}" --fill`));
3723
3841
  }
3724
- function printShipSteps(temporaryBranch, base) {
3842
+ function printShipSteps(temporaryBranch, base, title) {
3725
3843
  console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
3726
- printPrCreateStep(temporaryBranch, base);
3844
+ printPrCreateStep(temporaryBranch, base, title);
3727
3845
  }
3728
3846
  function printFinishSteps() {
3729
3847
  console.info(colors_default.dim(" pnpm cella sync"));
@@ -3740,8 +3858,7 @@ function extractFirstUrl(output) {
3740
3858
  async function flattenSyncBranch(forkPath, branch, base) {
3741
3859
  const mergeCommits = await listBranchMergeCommits(forkPath, base);
3742
3860
  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;
3861
+ const message = await buildSyncCommitMessage(forkPath, `${mergeCommits[0]}^2`);
3745
3862
  console.info(
3746
3863
  colors_default.yellow(
3747
3864
  `'${branch}' contains ${mergeCommits.length} merge commit(s) \u2014 the PR would list the entire upstream history.`
@@ -3757,18 +3874,22 @@ async function shipSyncBranch(config, branch) {
3757
3874
  const flattened = await flattenSyncBranch(forkPath, branch, base);
3758
3875
  let prUrl;
3759
3876
  let prOpened = false;
3877
+ const headSubject = (await getCommitInfo(forkPath, "HEAD").catch(() => null))?.message;
3878
+ const prTitle = headSubject?.startsWith(SYNC_PR_TITLE) ? headSubject : SYNC_PR_TITLE;
3760
3879
  console.info(colors_default.dim(`pushing '${branch}' to origin...`));
3761
3880
  try {
3762
3881
  await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
3763
3882
  } catch (error) {
3764
3883
  const detail = error instanceof Error ? error.message : String(error);
3765
3884
  console.info(colors_default.yellow(`push failed (${detail.split("\n")[0]}). finish manually:`));
3766
- printShipSteps(branch, base);
3885
+ printShipSteps(branch, base, prTitle);
3767
3886
  return;
3768
3887
  }
3769
3888
  if (ghAvailable()) {
3770
3889
  console.info(colors_default.dim("opening a pull request..."));
3771
- const pr = spawnSync2("gh", ["pr", "create", "--base", base, "--head", branch, "--title", SYNC_PR_TITLE, "--fill"], {
3890
+ const prBody = await buildSyncPrBodyForBranch(forkPath, base);
3891
+ const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
3892
+ const pr = spawnSync2("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
3772
3893
  cwd: forkPath,
3773
3894
  encoding: "utf-8",
3774
3895
  stdio: ["ignore", "pipe", "pipe"]
@@ -3779,11 +3900,11 @@ ${pr.stderr ?? ""}`);
3779
3900
  if (!prOpened) {
3780
3901
  console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
3781
3902
  if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
3782
- printPrCreateStep(branch, base);
3903
+ printPrCreateStep(branch, base, prTitle);
3783
3904
  }
3784
3905
  } else {
3785
3906
  console.info(colors_default.yellow("`gh` not found \u2014 open the PR manually:"));
3786
- printPrCreateStep(branch, base);
3907
+ printPrCreateStep(branch, base, prTitle);
3787
3908
  }
3788
3909
  console.info(colors_default.dim(`switching back to '${base}'...`));
3789
3910
  await switchBranch(forkPath, base);
@@ -3839,9 +3960,8 @@ async function resumeSyncMerge(config, branch) {
3839
3960
  );
3840
3961
  return;
3841
3962
  }
3842
- const upstreamSha = await getShortSha(forkPath, "MERGE_HEAD").catch(() => "");
3963
+ const message = await buildSyncCommitMessage(forkPath, "MERGE_HEAD");
3843
3964
  await stageAll(forkPath);
3844
- const message = upstreamSha ? `${SYNC_PR_TITLE} ${upstreamSha}` : SYNC_PR_TITLE;
3845
3965
  await commitSquash(forkPath, message);
3846
3966
  console.info();
3847
3967
  console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
@@ -3999,7 +4119,7 @@ async function runForks(config) {
3999
4119
 
4000
4120
  // src/services/stats.ts
4001
4121
  import { readFile as readFile2 } from "fs/promises";
4002
- import { join as join15 } from "path";
4122
+ import { join as join16 } from "path";
4003
4123
 
4004
4124
  // src/utils/coverage-utils.ts
4005
4125
  import { spawnSync as spawnSync3 } from "child_process";
@@ -4087,7 +4207,7 @@ function printCoverageSummary(forkPath, options = {}) {
4087
4207
  // src/services/stats.ts
4088
4208
  var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
4089
4209
  async function getWorkspacePackages(forkPath) {
4090
- const content = await readFile2(join15(forkPath, "pnpm-workspace.yaml"), "utf-8");
4210
+ const content = await readFile2(join16(forkPath, "pnpm-workspace.yaml"), "utf-8");
4091
4211
  return parseYamlBlockList(content, "packages").map((pattern) => {
4092
4212
  if (pattern.endsWith("/*")) {
4093
4213
  const base = pattern.slice(0, -2);
@@ -4245,7 +4365,7 @@ function getForkPath() {
4245
4365
  const envPath = getEnv("CELLA_FORK_PATH");
4246
4366
  if (envPath) {
4247
4367
  const resolved = resolve5(envPath);
4248
- if (!existsSync9(resolved) || !statSync2(resolved).isDirectory()) {
4368
+ if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
4249
4369
  throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
4250
4370
  }
4251
4371
  return resolved;
@@ -4257,7 +4377,7 @@ function getForkPath() {
4257
4377
  return cwd;
4258
4378
  }
4259
4379
  async function preflight(forkPath) {
4260
- if (!existsSync9(join16(forkPath, ".git"))) {
4380
+ if (!existsSync10(join17(forkPath, ".git"))) {
4261
4381
  throw new Error(`not a git repository: ${forkPath}`);
4262
4382
  }
4263
4383
  }