@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 +207 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
5
|
-
import { join as
|
|
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
|
|
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 {
|
|
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 =
|
|
149
|
-
if (!
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
655
|
-
import { join as
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
1022
|
+
const dir = join6(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1013
1023
|
mkdirSync(dir, { recursive: true });
|
|
1014
|
-
const outPath =
|
|
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
|
|
1031
|
+
import { existsSync as existsSync5, rmSync } from "fs";
|
|
1022
1032
|
import { tmpdir as tmpdir2 } from "os";
|
|
1023
|
-
import { basename as
|
|
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
|
|
1038
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1029
1039
|
import { mkdir, readdir, rmdir, unlink } from "fs/promises";
|
|
1030
|
-
import { dirname, join as
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
1124
|
-
if (
|
|
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
|
|
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(
|
|
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
|
|
1377
|
+
return existsSync4(join8(cwd, filePath));
|
|
1368
1378
|
}
|
|
1369
1379
|
async function removeFileFromWorktree(cwd, filePath) {
|
|
1370
|
-
const fullPath =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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)) && !
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 (!
|
|
3054
|
-
if (!
|
|
3055
|
-
if (requireConfig && !
|
|
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(
|
|
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
|
|
3443
|
-
import { join as
|
|
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 (!
|
|
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) =>
|
|
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 =
|
|
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
|
-
|
|
3722
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
4380
|
+
if (!existsSync10(join17(forkPath, ".git"))) {
|
|
4261
4381
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
4262
4382
|
}
|
|
4263
4383
|
}
|