@cellajs/cli 0.0.1 → 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/README.md +5 -8
- package/dist/config.d.ts +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +475 -261
- package/dist/index.js.map +1 -1
- package/package.json +17 -19
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
|
|
5
|
-
import { join as
|
|
6
|
-
import
|
|
4
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "fs";
|
|
5
|
+
import { join as join17, resolve as resolve5 } from "path";
|
|
6
|
+
import process9 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import { readFileSync } from "fs";
|
|
10
|
-
import { join as
|
|
11
|
-
import
|
|
10
|
+
import { join as join5 } from "path";
|
|
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
|
|
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"
|
|
20
|
-
if ("FORCE_COLOR"
|
|
21
|
-
return !!
|
|
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;
|
|
@@ -39,8 +53,8 @@ var pc = {
|
|
|
39
53
|
var colors_default = pc;
|
|
40
54
|
|
|
41
55
|
// src/utils/config.ts
|
|
42
|
-
import { existsSync, realpathSync } from "fs";
|
|
43
|
-
import { join, resolve } from "path";
|
|
56
|
+
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
57
|
+
import { join as join2, resolve } from "path";
|
|
44
58
|
import { z as z2 } from "zod";
|
|
45
59
|
|
|
46
60
|
// src/config/types.ts
|
|
@@ -82,9 +96,11 @@ var cellaConfigSchema = z.object({
|
|
|
82
96
|
}).strict();
|
|
83
97
|
|
|
84
98
|
// src/utils/module-territory.ts
|
|
85
|
-
import {
|
|
99
|
+
import { existsSync } from "fs";
|
|
100
|
+
import { basename, join, relative, sep } from "path";
|
|
86
101
|
import { Project, SyntaxKind } from "ts-morph";
|
|
87
102
|
var MODULE_FILE_GLOBS = ["backend/src/modules/*/*-module.ts", "frontend/src/modules/*/*-module.ts"];
|
|
103
|
+
var FRONTEND_STATIC_ROOT = "frontend/public/static";
|
|
88
104
|
function readOwner(call) {
|
|
89
105
|
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
90
106
|
const initializer = arg?.getProperty("owner")?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
|
|
@@ -98,11 +114,19 @@ function resolveAppModuleFolders(repoPath) {
|
|
|
98
114
|
});
|
|
99
115
|
const sourceFiles = project.addSourceFilesAtPaths(MODULE_FILE_GLOBS.map((glob) => `${repoPath}/${glob}`));
|
|
100
116
|
const folders = /* @__PURE__ */ new Set();
|
|
117
|
+
const moduleNames = /* @__PURE__ */ new Set();
|
|
101
118
|
for (const sourceFile of sourceFiles) {
|
|
102
119
|
const ownsApp = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).some((call) => call.getExpression().getText() === "registerModule" && readOwner(call) === "app");
|
|
103
120
|
if (!ownsApp) continue;
|
|
104
121
|
const folder = relative(repoPath, sourceFile.getDirectoryPath()).split(sep).join("/");
|
|
105
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
|
+
}
|
|
106
130
|
}
|
|
107
131
|
return [...folders];
|
|
108
132
|
}
|
|
@@ -131,8 +155,8 @@ function resolveUpstream(settings) {
|
|
|
131
155
|
return { track, branchRef };
|
|
132
156
|
}
|
|
133
157
|
async function loadConfig(forkPath) {
|
|
134
|
-
const configPath =
|
|
135
|
-
if (!
|
|
158
|
+
const configPath = join2(forkPath, "cella.config.ts");
|
|
159
|
+
if (!existsSync2(configPath)) {
|
|
136
160
|
throw new Error(`config file not found: ${configPath}`);
|
|
137
161
|
}
|
|
138
162
|
const realConfigPath = realpathSync(configPath);
|
|
@@ -165,18 +189,17 @@ ${z2.prettifyError(result.error)}`);
|
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
// src/utils/display.ts
|
|
168
|
-
import {
|
|
169
|
-
import {
|
|
170
|
-
import
|
|
171
|
-
import process3 from "process";
|
|
192
|
+
import { writeFileSync } from "fs";
|
|
193
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
194
|
+
import process4 from "process";
|
|
172
195
|
|
|
173
196
|
// package.json
|
|
174
197
|
var package_default = {
|
|
175
198
|
name: "@cellajs/cli",
|
|
176
|
-
version: "0.0.
|
|
199
|
+
version: "0.0.4",
|
|
177
200
|
private: false,
|
|
178
201
|
license: "MIT",
|
|
179
|
-
description: "CLI for
|
|
202
|
+
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
180
203
|
repository: {
|
|
181
204
|
type: "git",
|
|
182
205
|
url: "https://github.com/cellajs/cella-cli"
|
|
@@ -225,6 +248,7 @@ var package_default = {
|
|
|
225
248
|
dependencies: {
|
|
226
249
|
"@inquirer/core": "^11.2.1",
|
|
227
250
|
"@inquirer/prompts": "^8.5.2",
|
|
251
|
+
"@pierre/diffs": "^1.2.12",
|
|
228
252
|
commander: "^15.0.0",
|
|
229
253
|
"ts-morph": "^28.0.0",
|
|
230
254
|
zod: "^4.4.3"
|
|
@@ -311,7 +335,7 @@ var checkMark = colors_default.green("\u2713");
|
|
|
311
335
|
var TerminalSpinner = class {
|
|
312
336
|
constructor(text, isSilent) {
|
|
313
337
|
this.text = text;
|
|
314
|
-
this.enabled = !isSilent && !!
|
|
338
|
+
this.enabled = !isSilent && !!process4.stdout.isTTY;
|
|
315
339
|
}
|
|
316
340
|
text;
|
|
317
341
|
frames = ["-", "\\", "|", "/"];
|
|
@@ -333,10 +357,10 @@ var TerminalSpinner = class {
|
|
|
333
357
|
this.timer = null;
|
|
334
358
|
}
|
|
335
359
|
if (!this.enabled) return;
|
|
336
|
-
|
|
360
|
+
process4.stdout.write("\r\x1B[2K");
|
|
337
361
|
}
|
|
338
362
|
render() {
|
|
339
|
-
|
|
363
|
+
process4.stdout.write(`\r${colors_default.cyan(this.frames[this.frameIndex])} ${this.text}`);
|
|
340
364
|
}
|
|
341
365
|
};
|
|
342
366
|
var activeSpinner = null;
|
|
@@ -345,7 +369,7 @@ function setJsonMode(enabled) {
|
|
|
345
369
|
jsonMode = enabled;
|
|
346
370
|
if (!enabled) return;
|
|
347
371
|
const toStderr = (...args) => {
|
|
348
|
-
|
|
372
|
+
process4.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
|
|
349
373
|
`);
|
|
350
374
|
};
|
|
351
375
|
console.info = toStderr;
|
|
@@ -353,7 +377,7 @@ function setJsonMode(enabled) {
|
|
|
353
377
|
console.warn = toStderr;
|
|
354
378
|
}
|
|
355
379
|
function writeStdout(text) {
|
|
356
|
-
|
|
380
|
+
process4.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
357
381
|
`);
|
|
358
382
|
}
|
|
359
383
|
function getHeader() {
|
|
@@ -376,7 +400,7 @@ function printStep(label, detail) {
|
|
|
376
400
|
}
|
|
377
401
|
}
|
|
378
402
|
function createSpinner(text) {
|
|
379
|
-
const isTestEnv = !!
|
|
403
|
+
const isTestEnv = !!getEnv("VITEST") || getEnv("NODE_ENV") === "test";
|
|
380
404
|
activeSpinner = new TerminalSpinner(text, isTestEnv || jsonMode);
|
|
381
405
|
activeSpinner.start();
|
|
382
406
|
return activeSpinner;
|
|
@@ -402,23 +426,6 @@ function stopSpinner() {
|
|
|
402
426
|
activeSpinner = null;
|
|
403
427
|
}
|
|
404
428
|
}
|
|
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
429
|
function hyperlink(label, url) {
|
|
423
430
|
if (!url) return label;
|
|
424
431
|
const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
|
|
@@ -447,18 +454,12 @@ function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, ma
|
|
|
447
454
|
return lines.join("\n");
|
|
448
455
|
}
|
|
449
456
|
function getFileLink(filePath, commitHash, options) {
|
|
450
|
-
const { upstreamGitHubUrl, upstreamBranch, fileLinkMode = "commit"
|
|
451
|
-
|
|
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
|
-
}
|
|
457
|
+
const { upstreamGitHubUrl, upstreamBranch, fileLinkMode = "commit" } = options;
|
|
458
|
+
const mode = fileLinkMode === "local" ? "file" : fileLinkMode;
|
|
458
459
|
if (!upstreamGitHubUrl) {
|
|
459
460
|
return { label: commitHash?.slice(0, 9) || "" };
|
|
460
461
|
}
|
|
461
|
-
if (
|
|
462
|
+
if (mode === "file" && upstreamBranch) {
|
|
462
463
|
const url = `${upstreamGitHubUrl}/blob/${upstreamBranch}/${filePath}`;
|
|
463
464
|
return { label: filePath.split("/").pop() || filePath, url };
|
|
464
465
|
}
|
|
@@ -514,7 +515,7 @@ var summaryStatusConfig = {
|
|
|
514
515
|
icon: colors_default.cyan("\u25C7"),
|
|
515
516
|
label: "managed",
|
|
516
517
|
color: colors_default.cyan,
|
|
517
|
-
description: "package
|
|
518
|
+
description: "package.jsons & cella.config.ts handled separately"
|
|
518
519
|
},
|
|
519
520
|
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
520
521
|
};
|
|
@@ -602,7 +603,7 @@ function printAnalysisFileGroups(files, linkOptions) {
|
|
|
602
603
|
});
|
|
603
604
|
}
|
|
604
605
|
function writeLogFile(forkPath, files) {
|
|
605
|
-
const logPath =
|
|
606
|
+
const logPath = join3(forkPath, "cella-sync.log");
|
|
606
607
|
const lines = [
|
|
607
608
|
`cella sync analysis - ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
608
609
|
DIVIDER,
|
|
@@ -625,14 +626,23 @@ function writeLogFile(forkPath, files) {
|
|
|
625
626
|
writeFileSync(logPath, lines.join("\n"), "utf-8");
|
|
626
627
|
return logPath;
|
|
627
628
|
}
|
|
628
|
-
function printSyncComplete(result) {
|
|
629
|
+
function printSyncComplete(result, options = {}) {
|
|
629
630
|
const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
|
|
630
631
|
const diverged = result.files.filter((file) => file.status === "diverged").length;
|
|
631
632
|
const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);
|
|
632
633
|
const conflicts = result.conflicts.length;
|
|
633
634
|
console.info();
|
|
634
|
-
|
|
635
|
-
|
|
635
|
+
if (options.stagedBranch) {
|
|
636
|
+
console.info(`${colors_default.green("\u2713")} Sync merge staged on '${options.stagedBranch}'`);
|
|
637
|
+
console.info(
|
|
638
|
+
colors_default.dim(
|
|
639
|
+
` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. Review, then rerun \`pnpm cella sync\` to finish.`
|
|
640
|
+
)
|
|
641
|
+
);
|
|
642
|
+
} else {
|
|
643
|
+
console.info(`${colors_default.green("\u2713")} sync complete`);
|
|
644
|
+
console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
|
|
645
|
+
}
|
|
636
646
|
console.info();
|
|
637
647
|
}
|
|
638
648
|
function printFlagWarnings(options) {
|
|
@@ -652,7 +662,7 @@ function printFlagWarnings(options) {
|
|
|
652
662
|
|
|
653
663
|
// src/utils/overrides.ts
|
|
654
664
|
import { existsSync as existsSync3 } from "fs";
|
|
655
|
-
import { join as
|
|
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 (!existsSync3(
|
|
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 (!existsSync3(
|
|
713
|
+
} else if (!existsSync3(join4(forkPath, entry.replace(/\/+$/, "")))) {
|
|
704
714
|
warnings.push({
|
|
705
715
|
type: "ignored-not-found",
|
|
706
716
|
pattern: entry,
|
|
@@ -751,7 +761,7 @@ var serviceDefinitions = [
|
|
|
751
761
|
{ flags: "--scope <scope>", description: "analyze scope for --list/--json: all|risk|protected" },
|
|
752
762
|
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
|
|
753
763
|
{ flags: "--diff <path>", description: "print unified diff for one file, then exit" },
|
|
754
|
-
{ flags: "--open-diff <path>", description: "open
|
|
764
|
+
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
755
765
|
],
|
|
756
766
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
757
767
|
},
|
|
@@ -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) {
|
|
@@ -878,7 +888,7 @@ async function promptForService(userConfig, forkPath) {
|
|
|
878
888
|
if (selected === "exit") {
|
|
879
889
|
console.info(colors_default.dim("exiting..."));
|
|
880
890
|
console.info();
|
|
881
|
-
|
|
891
|
+
process5.exit(0);
|
|
882
892
|
}
|
|
883
893
|
console.info();
|
|
884
894
|
return selected;
|
|
@@ -894,7 +904,7 @@ function buildRuntimeConfig(userConfig, forkPath, selection) {
|
|
|
894
904
|
};
|
|
895
905
|
}
|
|
896
906
|
async function parseCli(userConfig, forkPath) {
|
|
897
|
-
const selection = parseCommandLine(
|
|
907
|
+
const selection = parseCommandLine(process5.argv);
|
|
898
908
|
if (selection.options.json || selection.options.diff) setJsonMode(true);
|
|
899
909
|
printHeader();
|
|
900
910
|
const warnings = validateOverrides(userConfig, forkPath);
|
|
@@ -909,29 +919,134 @@ 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
|
-
// src/utils/
|
|
915
|
-
import {
|
|
924
|
+
// src/utils/diff.ts
|
|
925
|
+
import { execFileSync } from "child_process";
|
|
926
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
916
927
|
import { tmpdir } from "os";
|
|
917
|
-
import { basename, join as
|
|
928
|
+
import { basename as basename2, join as join6 } from "path";
|
|
918
929
|
import process6 from "process";
|
|
930
|
+
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
931
|
+
const args = ["diff", "--no-color"];
|
|
932
|
+
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
933
|
+
args.push(range, "--", filePath);
|
|
934
|
+
try {
|
|
935
|
+
return execFileSync("git", args, { cwd, maxBuffer: 50 * 1024 * 1024 });
|
|
936
|
+
} catch (error) {
|
|
937
|
+
const stderr = error.stderr?.toString().trim();
|
|
938
|
+
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function escapeHtml(text) {
|
|
942
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
943
|
+
}
|
|
944
|
+
async function renderDiffPage(patch, meta) {
|
|
945
|
+
const { preloadPatchDiff } = await import("@pierre/diffs/ssr");
|
|
946
|
+
const { prerenderedHTML } = await preloadPatchDiff({
|
|
947
|
+
patch,
|
|
948
|
+
options: {
|
|
949
|
+
diffStyle: "split",
|
|
950
|
+
themeType: "system",
|
|
951
|
+
lineDiffType: "word-alt",
|
|
952
|
+
hunkSeparators: "line-info",
|
|
953
|
+
overflow: "wrap",
|
|
954
|
+
disableFileHeader: true
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
const title = `${escapeHtml(meta.filePath)} \xB7 ${escapeHtml(meta.srcLabel)} vs ${escapeHtml(meta.dstLabel)}`;
|
|
958
|
+
const note = meta.note ? `<span class="note">\xB7 ${escapeHtml(meta.note)}</span>` : "";
|
|
959
|
+
return `<!doctype html>
|
|
960
|
+
<html lang="en">
|
|
961
|
+
<head>
|
|
962
|
+
<meta charset="utf-8">
|
|
963
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
964
|
+
<title>${title}</title>
|
|
965
|
+
<style>
|
|
966
|
+
:root { color-scheme: light dark; }
|
|
967
|
+
* { box-sizing: border-box; }
|
|
968
|
+
body {
|
|
969
|
+
margin: 0;
|
|
970
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
971
|
+
background: light-dark(#fafafa, #111);
|
|
972
|
+
color: light-dark(#111, #eee);
|
|
973
|
+
}
|
|
974
|
+
header {
|
|
975
|
+
display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap;
|
|
976
|
+
padding: 0.9rem 1.25rem;
|
|
977
|
+
border-bottom: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
978
|
+
position: sticky; top: 0;
|
|
979
|
+
background: inherit;
|
|
980
|
+
}
|
|
981
|
+
header .brand { font-weight: 600; color: light-dark(#0891b2, #22d3ee); }
|
|
982
|
+
header code { font-family: ui-monospace, monospace; font-size: 0.9rem; }
|
|
983
|
+
header .sides { margin-left: auto; font-size: 0.8rem; opacity: 0.7; }
|
|
984
|
+
main { max-width: 1400px; margin: 1rem auto; padding: 0 1rem 2rem; }
|
|
985
|
+
.diff-shell {
|
|
986
|
+
border: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
987
|
+
border-radius: 8px; overflow: hidden;
|
|
988
|
+
background: light-dark(#fff, #161616);
|
|
989
|
+
}
|
|
990
|
+
</style>
|
|
991
|
+
</head>
|
|
992
|
+
<body>
|
|
993
|
+
<header>
|
|
994
|
+
<span class="brand">\u29C8 cella diff</span>
|
|
995
|
+
<code>${escapeHtml(meta.filePath)}</code>
|
|
996
|
+
<span class="sides">${escapeHtml(meta.srcLabel)} \u2190 \u2192 ${escapeHtml(meta.dstLabel)} ${note}</span>
|
|
997
|
+
</header>
|
|
998
|
+
<main>
|
|
999
|
+
<!-- Declarative shadow DOM: the library's CSS targets :host, which only
|
|
1000
|
+
matches inside a shadow root (its browser component renders into one). -->
|
|
1001
|
+
<div class="diff-shell">
|
|
1002
|
+
<template shadowrootmode="open">
|
|
1003
|
+
${prerenderedHTML}
|
|
1004
|
+
</template>
|
|
1005
|
+
</div>
|
|
1006
|
+
</main>
|
|
1007
|
+
</body>
|
|
1008
|
+
</html>`;
|
|
1009
|
+
}
|
|
1010
|
+
function openWithDefaultApp(target) {
|
|
1011
|
+
try {
|
|
1012
|
+
if (process6.platform === "win32") {
|
|
1013
|
+
execFileSync("rundll32", ["url.dll,FileProtocolHandler", target], { stdio: "ignore" });
|
|
1014
|
+
} else {
|
|
1015
|
+
execFileSync(process6.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
1016
|
+
}
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
1021
|
+
const html = await renderDiffPage(patch, meta);
|
|
1022
|
+
const dir = join6(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1023
|
+
mkdirSync(dir, { recursive: true });
|
|
1024
|
+
const outPath = join6(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
1025
|
+
writeFileSync2(outPath, html);
|
|
1026
|
+
openWithDefaultApp(outPath);
|
|
1027
|
+
return outPath;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/utils/cleanup.ts
|
|
1031
|
+
import { existsSync as existsSync5, rmSync } from "fs";
|
|
1032
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1033
|
+
import { basename as basename3, join as join9 } from "path";
|
|
1034
|
+
import process7 from "process";
|
|
919
1035
|
|
|
920
1036
|
// src/utils/git.ts
|
|
921
1037
|
import { execFile } from "child_process";
|
|
922
1038
|
import { existsSync as existsSync4 } from "fs";
|
|
923
1039
|
import { mkdir, readdir, rmdir, unlink } from "fs/promises";
|
|
924
|
-
import { dirname, join as
|
|
925
|
-
import process5 from "process";
|
|
1040
|
+
import { dirname, join as join8 } from "path";
|
|
926
1041
|
import { promisify } from "util";
|
|
927
1042
|
|
|
928
1043
|
// src/utils/manifest.ts
|
|
929
1044
|
import { readFile, writeFile } from "fs/promises";
|
|
930
|
-
import { join as
|
|
1045
|
+
import { join as join7 } from "path";
|
|
931
1046
|
var MANIFEST_FILE = "cella.manifest.json";
|
|
932
1047
|
async function readSyncManifest(cwd) {
|
|
933
1048
|
try {
|
|
934
|
-
const parsed = JSON.parse(await readFile(
|
|
1049
|
+
const parsed = JSON.parse(await readFile(join7(cwd, MANIFEST_FILE), "utf8"));
|
|
935
1050
|
const commit = parsed?.upstream?.commit;
|
|
936
1051
|
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
937
1052
|
return parsed;
|
|
@@ -944,7 +1059,7 @@ async function readManifestBase(cwd) {
|
|
|
944
1059
|
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
945
1060
|
}
|
|
946
1061
|
async function writeSyncManifest(cwd, manifest) {
|
|
947
|
-
await writeFile(
|
|
1062
|
+
await writeFile(join7(cwd, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
948
1063
|
`, "utf8");
|
|
949
1064
|
}
|
|
950
1065
|
|
|
@@ -958,7 +1073,7 @@ function sanitizeCredentials(text) {
|
|
|
958
1073
|
}
|
|
959
1074
|
async function git(args, cwd, options = {}) {
|
|
960
1075
|
const env = {
|
|
961
|
-
...
|
|
1076
|
+
...getEnvSnapshot(),
|
|
962
1077
|
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
963
1078
|
...options.env
|
|
964
1079
|
};
|
|
@@ -1000,12 +1115,22 @@ async function getUpstreamStatus(cwd) {
|
|
|
1000
1115
|
const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
1001
1116
|
return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
1002
1117
|
}
|
|
1003
|
-
async function pushBranch(cwd, remote, branch) {
|
|
1004
|
-
await git(["push", "-u", remote, branch], cwd);
|
|
1118
|
+
async function pushBranch(cwd, remote, branch, options = {}) {
|
|
1119
|
+
await git(["push", "-u", ...options.forceWithLease ? ["--force-with-lease"] : [], remote, branch], cwd);
|
|
1120
|
+
}
|
|
1121
|
+
async function listBranchMergeCommits(cwd, baseRef) {
|
|
1122
|
+
const out = await git(["rev-list", "--merges", "--first-parent", `${baseRef}..HEAD`], cwd, { ignoreErrors: true });
|
|
1123
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
1124
|
+
}
|
|
1125
|
+
async function flattenBranch(cwd, baseRef, message) {
|
|
1126
|
+
const parent = await getMergeBase(cwd, baseRef, "HEAD");
|
|
1127
|
+
const tree = await git(["rev-parse", "HEAD^{tree}"], cwd);
|
|
1128
|
+
const commit = await git(["commit-tree", tree, "-p", parent, "-m", message], cwd);
|
|
1129
|
+
await git(["reset", "--hard", commit], cwd);
|
|
1005
1130
|
}
|
|
1006
1131
|
async function commitSquash(cwd, message) {
|
|
1007
1132
|
for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
|
|
1008
|
-
const path4 =
|
|
1133
|
+
const path4 = join8(cwd, ".git", file);
|
|
1009
1134
|
if (existsSync4(path4)) await unlink(path4);
|
|
1010
1135
|
}
|
|
1011
1136
|
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
@@ -1014,7 +1139,7 @@ async function stageAll(cwd) {
|
|
|
1014
1139
|
await git(["add", "-A"], cwd);
|
|
1015
1140
|
}
|
|
1016
1141
|
function mergeInProgress(cwd) {
|
|
1017
|
-
return existsSync4(
|
|
1142
|
+
return existsSync4(join8(cwd, ".git", "MERGE_HEAD"));
|
|
1018
1143
|
}
|
|
1019
1144
|
async function getShortSha(cwd, ref) {
|
|
1020
1145
|
return git(["rev-parse", "--short", ref], cwd);
|
|
@@ -1244,15 +1369,15 @@ async function batchUnstageFiles(cwd, filePaths) {
|
|
|
1244
1369
|
await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1245
1370
|
}
|
|
1246
1371
|
async function gitMv(cwd, oldPath, newPath) {
|
|
1247
|
-
const newDir = dirname(
|
|
1372
|
+
const newDir = dirname(join8(cwd, newPath));
|
|
1248
1373
|
await mkdir(newDir, { recursive: true });
|
|
1249
1374
|
await git(["mv", "-f", oldPath, newPath], cwd);
|
|
1250
1375
|
}
|
|
1251
1376
|
async function fileExistsInWorktree(cwd, filePath) {
|
|
1252
|
-
return existsSync4(
|
|
1377
|
+
return existsSync4(join8(cwd, filePath));
|
|
1253
1378
|
}
|
|
1254
1379
|
async function removeFileFromWorktree(cwd, filePath) {
|
|
1255
|
-
const fullPath =
|
|
1380
|
+
const fullPath = join8(cwd, filePath);
|
|
1256
1381
|
try {
|
|
1257
1382
|
await unlink(fullPath);
|
|
1258
1383
|
await cleanupEmptyParentDirs(cwd, dirname(filePath));
|
|
@@ -1265,7 +1390,7 @@ async function removeFileFully(cwd, filePath) {
|
|
|
1265
1390
|
}
|
|
1266
1391
|
async function cleanupEmptyParentDirs(cwd, relativePath) {
|
|
1267
1392
|
if (!relativePath || relativePath === ".") return;
|
|
1268
|
-
const fullPath =
|
|
1393
|
+
const fullPath = join8(cwd, relativePath);
|
|
1269
1394
|
try {
|
|
1270
1395
|
const entries = await readdir(fullPath);
|
|
1271
1396
|
if (entries.length === 0) {
|
|
@@ -1293,14 +1418,28 @@ async function getStoredSyncHead(cwd) {
|
|
|
1293
1418
|
const ref = await git(["rev-parse", "refs/cella/last-sync-head"], cwd, { ignoreErrors: true });
|
|
1294
1419
|
return ref || null;
|
|
1295
1420
|
}
|
|
1296
|
-
async function
|
|
1421
|
+
async function readManifestAtRef(cwd, ref) {
|
|
1297
1422
|
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
1298
1423
|
if (!raw) return null;
|
|
1299
1424
|
try {
|
|
1300
1425
|
const parsed = JSON.parse(raw);
|
|
1301
1426
|
const commit = parsed?.upstream?.commit;
|
|
1302
1427
|
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1303
|
-
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;
|
|
1304
1443
|
} catch {
|
|
1305
1444
|
return null;
|
|
1306
1445
|
}
|
|
@@ -1318,28 +1457,82 @@ async function getRootCommit(cwd, ref) {
|
|
|
1318
1457
|
const roots = out.split("\n").filter(Boolean);
|
|
1319
1458
|
return roots.length > 0 ? roots[roots.length - 1] : null;
|
|
1320
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
|
+
}
|
|
1321
1506
|
async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
1322
1507
|
const nativeBase = await git(["merge-base", headRef, upstreamRef], cwd, { ignoreErrors: true });
|
|
1323
1508
|
if (nativeBase) return;
|
|
1324
|
-
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
|
+
}
|
|
1325
1524
|
if (!baseSha) {
|
|
1326
1525
|
throw new Error(
|
|
1327
|
-
`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.
|
|
1328
1527
|
|
|
1329
1528
|
This fork has unrelated history with upstream \u2014 typical for a create-cella scaffold, or after
|
|
1330
|
-
upstream squashed its history
|
|
1331
|
-
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:
|
|
1332
1531
|
|
|
1333
1532
|
git update-ref refs/cella/last-sync <upstream-sha>
|
|
1334
1533
|
`
|
|
1335
1534
|
);
|
|
1336
1535
|
}
|
|
1337
|
-
if (!await commitObjectExists(cwd, baseSha)) {
|
|
1338
|
-
throw new Error(
|
|
1339
|
-
`recorded sync base ${baseSha.slice(0, 12)} is not present after fetching '${upstreamRef}'.
|
|
1340
|
-
It may belong to a different upstream, or have been dropped by an upstream history rewrite.`
|
|
1341
|
-
);
|
|
1342
|
-
}
|
|
1343
1536
|
const root = await getRootCommit(cwd, headRef);
|
|
1344
1537
|
if (!root) throw new Error("could not determine the fork's root commit to bootstrap sync ancestry");
|
|
1345
1538
|
await git(["replace", "-f", "--graft", root, baseSha], cwd);
|
|
@@ -1382,23 +1575,11 @@ var WORKTREE_PREFIXES = {
|
|
|
1382
1575
|
view: "cella-view-"
|
|
1383
1576
|
};
|
|
1384
1577
|
function buildWorktreePath(kind, repoPath) {
|
|
1385
|
-
return
|
|
1578
|
+
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
1386
1579
|
}
|
|
1387
1580
|
function getWorktreePath(repoPath) {
|
|
1388
1581
|
return buildWorktreePath("sync", repoPath);
|
|
1389
1582
|
}
|
|
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
1583
|
var cleanupRegistered = false;
|
|
1403
1584
|
var currentWorktreePath = null;
|
|
1404
1585
|
var currentRepoPath = null;
|
|
@@ -1418,9 +1599,11 @@ async function cleanupWorktree(repoPath, worktreePath) {
|
|
|
1418
1599
|
unregisterWorktree();
|
|
1419
1600
|
}
|
|
1420
1601
|
async function cleanupLeftoverWorktrees(repoPath) {
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1602
|
+
for (const kind of Object.keys(WORKTREE_PREFIXES)) {
|
|
1603
|
+
const worktreePath = buildWorktreePath(kind, repoPath);
|
|
1604
|
+
if (existsSync5(worktreePath)) {
|
|
1605
|
+
await cleanupWorktree(repoPath, worktreePath);
|
|
1606
|
+
}
|
|
1424
1607
|
}
|
|
1425
1608
|
const prefixes = Object.values(WORKTREE_PREFIXES);
|
|
1426
1609
|
const worktrees = await listWorktrees(repoPath);
|
|
@@ -1447,56 +1630,15 @@ async function handleAbort(signal) {
|
|
|
1447
1630
|
);
|
|
1448
1631
|
}
|
|
1449
1632
|
}
|
|
1450
|
-
|
|
1633
|
+
process7.exit(1);
|
|
1451
1634
|
}
|
|
1452
1635
|
function registerSignalHandlers() {
|
|
1453
1636
|
if (cleanupRegistered) return;
|
|
1454
|
-
|
|
1455
|
-
|
|
1637
|
+
process7.on("SIGINT", () => handleAbort("SIGINT"));
|
|
1638
|
+
process7.on("SIGTERM", () => handleAbort("SIGTERM"));
|
|
1456
1639
|
cleanupRegistered = true;
|
|
1457
1640
|
}
|
|
1458
1641
|
|
|
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
1642
|
// src/services/analyze-core.ts
|
|
1501
1643
|
async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predicates, onProgress) {
|
|
1502
1644
|
onProgress?.("collecting file hashes (batch)...");
|
|
@@ -2009,8 +2151,7 @@ function findTargetFile(files, targetPath) {
|
|
|
2009
2151
|
}
|
|
2010
2152
|
function printUnifiedDiff(config, filePath) {
|
|
2011
2153
|
const diff = gitDiffFile(config.forkPath, `${config.upstreamRef}..HEAD`, filePath, {
|
|
2012
|
-
dstPrefix:
|
|
2013
|
-
color: "never"
|
|
2154
|
+
dstPrefix: basename4(config.forkPath)
|
|
2014
2155
|
});
|
|
2015
2156
|
writeStdout(diff.toString());
|
|
2016
2157
|
}
|
|
@@ -2052,19 +2193,32 @@ async function runAnalyze(config) {
|
|
|
2052
2193
|
}
|
|
2053
2194
|
return result;
|
|
2054
2195
|
}
|
|
2055
|
-
const upstreamViewPath = await refreshViewWorktree(config.forkPath, config.upstreamRef);
|
|
2056
2196
|
if (config.openDiff) {
|
|
2057
2197
|
const file = findTargetFile(scopedFiles, config.openDiff) ?? findTargetFile(result.files, config.openDiff);
|
|
2058
2198
|
if (!file) throw new Error(`file not found in analysis results: ${config.openDiff}`);
|
|
2059
|
-
|
|
2060
|
-
|
|
2199
|
+
const patch = gitDiffFile(config.forkPath, config.upstreamRef, file.path);
|
|
2200
|
+
if (patch.length === 0) {
|
|
2201
|
+
console.info(`${colors_default.green("\u2713")} ${file.path} is identical to upstream`);
|
|
2202
|
+
return result;
|
|
2203
|
+
}
|
|
2204
|
+
const pagePath = await openDiffInBrowser(
|
|
2205
|
+
patch.toString(),
|
|
2206
|
+
{
|
|
2207
|
+
filePath: file.path,
|
|
2208
|
+
srcLabel: "cella",
|
|
2209
|
+
dstLabel: basename4(config.forkPath),
|
|
2210
|
+
note: "local working tree"
|
|
2211
|
+
},
|
|
2212
|
+
config.forkPath
|
|
2213
|
+
);
|
|
2214
|
+
console.info(`${colors_default.green("\u2713")} opened browser diff for ${file.path}`);
|
|
2215
|
+
console.info(colors_default.dim(` ${pagePath}`));
|
|
2061
2216
|
return result;
|
|
2062
2217
|
}
|
|
2063
2218
|
const linkOptions = {
|
|
2064
2219
|
upstreamGitHubUrl: result.upstreamGitHubUrl,
|
|
2065
2220
|
upstreamBranch: result.upstreamBranch,
|
|
2066
2221
|
fileLinkMode: config.settings.fileLinkMode,
|
|
2067
|
-
upstreamViewPath,
|
|
2068
2222
|
forkPath: config.forkPath
|
|
2069
2223
|
};
|
|
2070
2224
|
printAnalysisFileGroups(result.files, linkOptions);
|
|
@@ -2079,7 +2233,7 @@ async function runAnalyze(config) {
|
|
|
2079
2233
|
}
|
|
2080
2234
|
|
|
2081
2235
|
// src/services/audit.ts
|
|
2082
|
-
import { spawnSync
|
|
2236
|
+
import { spawnSync } from "child_process";
|
|
2083
2237
|
import * as fs2 from "fs";
|
|
2084
2238
|
import * as path2 from "path";
|
|
2085
2239
|
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
@@ -2370,7 +2524,7 @@ async function runAudit(config, options = {}) {
|
|
|
2370
2524
|
if (options.force) {
|
|
2371
2525
|
spinnerText("clearing pnpm metadata cache...");
|
|
2372
2526
|
clearCache();
|
|
2373
|
-
|
|
2527
|
+
spawnSync("pnpm", ["cache", "delete", "*"], {
|
|
2374
2528
|
cwd: forkPath,
|
|
2375
2529
|
stdio: "ignore"
|
|
2376
2530
|
});
|
|
@@ -2660,7 +2814,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
2660
2814
|
}
|
|
2661
2815
|
if (runAuditFix) {
|
|
2662
2816
|
console.info(colors_default.dim("running pnpm audit --fix override..."));
|
|
2663
|
-
const auditFixResult =
|
|
2817
|
+
const auditFixResult = spawnSync("pnpm", ["audit", "--fix", "override"], {
|
|
2664
2818
|
cwd: forkPath,
|
|
2665
2819
|
stdio: "inherit"
|
|
2666
2820
|
});
|
|
@@ -2680,7 +2834,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
2680
2834
|
const selectedCatalogPackages = selectedPackages.filter((pkgName) => catalogPackages.has(pkgName));
|
|
2681
2835
|
if (selectedCatalogPackages.length > 0) {
|
|
2682
2836
|
console.info(colors_default.dim(`normalizing catalog consumers: ${selectedCatalogPackages.join(", ")}`));
|
|
2683
|
-
const normalizeResult =
|
|
2837
|
+
const normalizeResult = spawnSync("pnpm", ["-r", "up", ...selectedCatalogPackages], {
|
|
2684
2838
|
cwd: forkPath,
|
|
2685
2839
|
stdio: "inherit"
|
|
2686
2840
|
});
|
|
@@ -2690,7 +2844,7 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
2690
2844
|
return;
|
|
2691
2845
|
}
|
|
2692
2846
|
}
|
|
2693
|
-
const result =
|
|
2847
|
+
const result = spawnSync("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
|
|
2694
2848
|
cwd: forkPath,
|
|
2695
2849
|
stdio: "inherit"
|
|
2696
2850
|
});
|
|
@@ -2847,7 +3001,7 @@ async function checkOverrides(forkPath) {
|
|
|
2847
3001
|
|
|
2848
3002
|
// src/services/contributions.ts
|
|
2849
3003
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2850
|
-
import { join as
|
|
3004
|
+
import { join as join13 } from "path";
|
|
2851
3005
|
import {
|
|
2852
3006
|
createPrompt,
|
|
2853
3007
|
isDownKey,
|
|
@@ -2862,7 +3016,7 @@ import {
|
|
|
2862
3016
|
import { select as select2 } from "@inquirer/prompts";
|
|
2863
3017
|
|
|
2864
3018
|
// src/services/contrib-core.ts
|
|
2865
|
-
import { dirname as dirname2, join as
|
|
3019
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
2866
3020
|
async function listFilesAtRef(repoPath, ref) {
|
|
2867
3021
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
2868
3022
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -2924,7 +3078,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
2924
3078
|
}
|
|
2925
3079
|
async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
|
|
2926
3080
|
const branch = `contrib/${forkName}`;
|
|
2927
|
-
const tmpIndex =
|
|
3081
|
+
const tmpIndex = join12(repoPath, ".git", `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
2928
3082
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
2929
3083
|
try {
|
|
2930
3084
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -2967,16 +3121,16 @@ ${commitBody}`;
|
|
|
2967
3121
|
}
|
|
2968
3122
|
|
|
2969
3123
|
// src/services/fork-utils.ts
|
|
2970
|
-
import { existsSync as
|
|
2971
|
-
import { resolve as
|
|
3124
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3125
|
+
import { resolve as resolve3 } from "path";
|
|
2972
3126
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
2973
|
-
const resolvedPath =
|
|
2974
|
-
if (!resolvedPath.startsWith(
|
|
3127
|
+
const resolvedPath = resolve3(basePath, fork.localPath);
|
|
3128
|
+
if (!resolvedPath.startsWith(resolve3(basePath, ".."))) {
|
|
2975
3129
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
2976
3130
|
}
|
|
2977
|
-
if (!
|
|
2978
|
-
if (!
|
|
2979
|
-
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`)) {
|
|
2980
3134
|
return { fork, resolvedPath, valid: false, error: "missing cella.config.ts" };
|
|
2981
3135
|
}
|
|
2982
3136
|
return { fork, resolvedPath, valid: true };
|
|
@@ -2988,12 +3142,13 @@ function printNoForksHint(action) {
|
|
|
2988
3142
|
}
|
|
2989
3143
|
|
|
2990
3144
|
// src/services/contributions.ts
|
|
2991
|
-
function showContribDiff(item, baseRef, cwd, forkName) {
|
|
2992
|
-
const
|
|
2993
|
-
|
|
3145
|
+
async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
3146
|
+
const patch = gitDiffFile(cwd, `${baseRef}..${item.ref}`, item.path);
|
|
3147
|
+
if (patch.length === 0) return null;
|
|
3148
|
+
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
2994
3149
|
}
|
|
2995
3150
|
function shouldApplyContributionsUnstaged(repoPath) {
|
|
2996
|
-
return JSON.parse(readFileSync4(
|
|
3151
|
+
return JSON.parse(readFileSync4(join13(repoPath, "package.json"), "utf8")).name === "cella";
|
|
2997
3152
|
}
|
|
2998
3153
|
var contribPrompt = createPrompt((config, done) => {
|
|
2999
3154
|
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
@@ -3053,8 +3208,12 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
3053
3208
|
return;
|
|
3054
3209
|
}
|
|
3055
3210
|
if (key.name === "d") {
|
|
3056
|
-
|
|
3057
|
-
|
|
3211
|
+
try {
|
|
3212
|
+
const pagePath = await showContribDiff(items[active], baseRef, cwd, forkName);
|
|
3213
|
+
setStatusMsg(pagePath ? `opened ${items[active].path} in browser` : `no changes for ${items[active].path}`);
|
|
3214
|
+
} catch (error) {
|
|
3215
|
+
setStatusMsg(`diff failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
3216
|
+
}
|
|
3058
3217
|
return;
|
|
3059
3218
|
}
|
|
3060
3219
|
if (key.name === "a") {
|
|
@@ -3095,7 +3254,7 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
3095
3254
|
const header = `${colors_default.cyan("\u2B07")} ${config.message} ${colors_default.dim(`(${countInfo})`)}`;
|
|
3096
3255
|
const keys = [
|
|
3097
3256
|
["\u2191\u2193", "navigate"],
|
|
3098
|
-
["d", "diff"],
|
|
3257
|
+
["d", "diff in browser"],
|
|
3099
3258
|
["a", "(de)select all"],
|
|
3100
3259
|
["space", "select"],
|
|
3101
3260
|
["\u23CE", "accept"],
|
|
@@ -3336,11 +3495,6 @@ async function runContributions(config) {
|
|
|
3336
3495
|
spinnerSuccess(`applied ${applied} files ${colors_default.dim(applyUnstaged ? "(unstaged)" : "(staged)")}`);
|
|
3337
3496
|
}
|
|
3338
3497
|
console.info();
|
|
3339
|
-
console.info(` ${colors_default.cyan(forkName)} ${colors_default.dim(`(${selected.length} files)`)}`);
|
|
3340
|
-
for (const item of selected) {
|
|
3341
|
-
console.info(` ${colors_default.dim("\u2192")} ${item.deleted ? `(deleted) ${item.path}` : item.path}`);
|
|
3342
|
-
}
|
|
3343
|
-
console.info();
|
|
3344
3498
|
console.info(
|
|
3345
3499
|
colors_default.dim(
|
|
3346
3500
|
applyUnstaged ? " files are unstaged \u2014 review, stage what you want, and commit when ready" : " files are staged \u2014 review and commit when ready"
|
|
@@ -3350,16 +3504,16 @@ async function runContributions(config) {
|
|
|
3350
3504
|
}
|
|
3351
3505
|
|
|
3352
3506
|
// src/services/forks.ts
|
|
3353
|
-
import { resolve as
|
|
3354
|
-
import
|
|
3507
|
+
import { resolve as resolve4 } from "path";
|
|
3508
|
+
import process8 from "process";
|
|
3355
3509
|
import { Separator as Separator2, select as select3 } from "@inquirer/prompts";
|
|
3356
3510
|
|
|
3357
3511
|
// src/services/sync.ts
|
|
3358
|
-
import { spawnSync as
|
|
3512
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3359
3513
|
|
|
3360
3514
|
// src/services/packages.ts
|
|
3361
|
-
import { existsSync as
|
|
3362
|
-
import { join as
|
|
3515
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
3516
|
+
import { join as join14 } from "path";
|
|
3363
3517
|
function parseComparableVersion(version) {
|
|
3364
3518
|
const trimmed = version.trim();
|
|
3365
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("||")) {
|
|
@@ -3463,7 +3617,7 @@ function safeMergePnpm(forkPnpm, upstreamPnpm) {
|
|
|
3463
3617
|
return { merged, changed };
|
|
3464
3618
|
}
|
|
3465
3619
|
function readPackageJson(filePath) {
|
|
3466
|
-
if (!
|
|
3620
|
+
if (!existsSync8(filePath)) return null;
|
|
3467
3621
|
try {
|
|
3468
3622
|
return JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
3469
3623
|
} catch {
|
|
@@ -3485,11 +3639,11 @@ async function getUpstreamPackageJson(forkPath, upstreamRef, relativePath) {
|
|
|
3485
3639
|
async function discoverPackageLocations(forkPath, upstreamRef) {
|
|
3486
3640
|
const stdout = await git(["ls-tree", "-r", "--name-only", upstreamRef], forkPath, { ignoreErrors: true });
|
|
3487
3641
|
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) =>
|
|
3642
|
+
return upstreamPaths.filter((loc) => existsSync8(join14(forkPath, loc, "package.json")));
|
|
3489
3643
|
}
|
|
3490
3644
|
async function syncPackageJson(forkPath, upstreamRef, relativePath, keysToSync) {
|
|
3491
3645
|
const changes = [];
|
|
3492
|
-
const pkgPath =
|
|
3646
|
+
const pkgPath = join14(forkPath, relativePath, "package.json");
|
|
3493
3647
|
const forkPkg = readPackageJson(pkgPath);
|
|
3494
3648
|
const upstreamPkg = await getUpstreamPackageJson(forkPath, upstreamRef, relativePath);
|
|
3495
3649
|
if (!forkPkg || !upstreamPkg) {
|
|
@@ -3544,7 +3698,7 @@ async function runPackages(config, options = {}) {
|
|
|
3544
3698
|
const keysToSync = config.settings.packageJsonSync || ["dependencies", "devDependencies"];
|
|
3545
3699
|
const conflictedSet = new Set(options.conflictedFiles ?? []);
|
|
3546
3700
|
const locations = await discoverPackageLocations(config.forkPath, config.upstreamRef);
|
|
3547
|
-
|
|
3701
|
+
let changedCount = 0;
|
|
3548
3702
|
const skipped = [];
|
|
3549
3703
|
for (const location of locations) {
|
|
3550
3704
|
const pkgRelPath = location ? `${location}/package.json` : "package.json";
|
|
@@ -3553,22 +3707,15 @@ async function runPackages(config, options = {}) {
|
|
|
3553
3707
|
continue;
|
|
3554
3708
|
}
|
|
3555
3709
|
spinnerText(`syncing ${location || "root"}/package.json...`);
|
|
3556
|
-
const { updated
|
|
3710
|
+
const { updated } = await syncPackageJson(config.forkPath, config.upstreamRef, location, keysToSync);
|
|
3557
3711
|
if (updated) {
|
|
3558
|
-
|
|
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
|
-
}
|
|
3712
|
+
changedCount += 1;
|
|
3570
3713
|
}
|
|
3571
3714
|
}
|
|
3715
|
+
spinnerSuccess(
|
|
3716
|
+
"package sync complete",
|
|
3717
|
+
`${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
|
|
3718
|
+
);
|
|
3572
3719
|
if (skipped.length > 0) {
|
|
3573
3720
|
console.info();
|
|
3574
3721
|
console.warn(
|
|
@@ -3633,23 +3780,68 @@ async function runSync(config, options) {
|
|
|
3633
3780
|
spinnerFail("sync completed with conflicts");
|
|
3634
3781
|
}
|
|
3635
3782
|
printSummary(result.summary, "merge summary");
|
|
3636
|
-
options?.onAfterSummary?.(result);
|
|
3637
3783
|
if (config.logFile) {
|
|
3638
3784
|
const logPath = writeLogFile(config.forkPath, result.files);
|
|
3639
3785
|
console.info();
|
|
3640
3786
|
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
3641
3787
|
}
|
|
3642
|
-
|
|
3788
|
+
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
|
|
3789
|
+
printSyncComplete(result, { stagedBranch });
|
|
3643
3790
|
printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
|
|
3644
3791
|
return result;
|
|
3645
3792
|
}
|
|
3646
3793
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
3647
|
-
|
|
3648
|
-
|
|
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`));
|
|
3649
3841
|
}
|
|
3650
|
-
function printShipSteps(temporaryBranch, base) {
|
|
3842
|
+
function printShipSteps(temporaryBranch, base, title) {
|
|
3651
3843
|
console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
|
|
3652
|
-
printPrCreateStep(temporaryBranch, base);
|
|
3844
|
+
printPrCreateStep(temporaryBranch, base, title);
|
|
3653
3845
|
}
|
|
3654
3846
|
function printFinishSteps() {
|
|
3655
3847
|
console.info(colors_default.dim(" pnpm cella sync"));
|
|
@@ -3657,51 +3849,80 @@ function printFinishSteps() {
|
|
|
3657
3849
|
function hasStagedSyncChanges(result) {
|
|
3658
3850
|
return result.files.some((file) => ["behind", "diverged", "renamed", "ignored", "pinned"].includes(file.status));
|
|
3659
3851
|
}
|
|
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
3852
|
function ghAvailable() {
|
|
3667
|
-
return
|
|
3853
|
+
return spawnSync2("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
3854
|
+
}
|
|
3855
|
+
function extractFirstUrl(output) {
|
|
3856
|
+
return output.match(/https?:\/\/\S+/)?.[0];
|
|
3857
|
+
}
|
|
3858
|
+
async function flattenSyncBranch(forkPath, branch, base) {
|
|
3859
|
+
const mergeCommits = await listBranchMergeCommits(forkPath, base);
|
|
3860
|
+
if (mergeCommits.length === 0) return false;
|
|
3861
|
+
const message = await buildSyncCommitMessage(forkPath, `${mergeCommits[0]}^2`);
|
|
3862
|
+
console.info(
|
|
3863
|
+
colors_default.yellow(
|
|
3864
|
+
`'${branch}' contains ${mergeCommits.length} merge commit(s) \u2014 the PR would list the entire upstream history.`
|
|
3865
|
+
)
|
|
3866
|
+
);
|
|
3867
|
+
console.info(colors_default.dim(`flattening '${branch}' to a single commit (same content)...`));
|
|
3868
|
+
await flattenBranch(forkPath, base, message);
|
|
3869
|
+
return true;
|
|
3668
3870
|
}
|
|
3669
3871
|
async function shipSyncBranch(config, branch) {
|
|
3670
3872
|
const { forkPath, settings } = config;
|
|
3671
3873
|
const base = resolveReleaseBase(settings);
|
|
3874
|
+
const flattened = await flattenSyncBranch(forkPath, branch, base);
|
|
3875
|
+
let prUrl;
|
|
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;
|
|
3672
3879
|
console.info(colors_default.dim(`pushing '${branch}' to origin...`));
|
|
3673
3880
|
try {
|
|
3674
|
-
await pushBranch(forkPath, "origin", branch);
|
|
3881
|
+
await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
|
|
3675
3882
|
} catch (error) {
|
|
3676
3883
|
const detail = error instanceof Error ? error.message : String(error);
|
|
3677
3884
|
console.info(colors_default.yellow(`push failed (${detail.split("\n")[0]}). finish manually:`));
|
|
3678
|
-
printShipSteps(branch, base);
|
|
3885
|
+
printShipSteps(branch, base, prTitle);
|
|
3679
3886
|
return;
|
|
3680
3887
|
}
|
|
3681
3888
|
if (ghAvailable()) {
|
|
3682
3889
|
console.info(colors_default.dim("opening a pull request..."));
|
|
3683
|
-
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], {
|
|
3684
3893
|
cwd: forkPath,
|
|
3685
|
-
|
|
3894
|
+
encoding: "utf-8",
|
|
3895
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3686
3896
|
});
|
|
3687
|
-
|
|
3897
|
+
prUrl = extractFirstUrl(`${pr.stdout ?? ""}
|
|
3898
|
+
${pr.stderr ?? ""}`);
|
|
3899
|
+
prOpened = pr.status === 0;
|
|
3900
|
+
if (!prOpened) {
|
|
3688
3901
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
3689
|
-
|
|
3902
|
+
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
3903
|
+
printPrCreateStep(branch, base, prTitle);
|
|
3690
3904
|
}
|
|
3691
3905
|
} else {
|
|
3692
3906
|
console.info(colors_default.yellow("`gh` not found \u2014 open the PR manually:"));
|
|
3693
|
-
printPrCreateStep(branch, base);
|
|
3907
|
+
printPrCreateStep(branch, base, prTitle);
|
|
3694
3908
|
}
|
|
3695
3909
|
console.info(colors_default.dim(`switching back to '${base}'...`));
|
|
3696
3910
|
await switchBranch(forkPath, base);
|
|
3697
3911
|
await pullFastForward(forkPath).catch(() => {
|
|
3698
3912
|
});
|
|
3699
|
-
console.info(
|
|
3913
|
+
console.info();
|
|
3914
|
+
if (prUrl) {
|
|
3915
|
+
console.info(`${colors_default.green("\u2713")} Sync pull request ${prOpened ? "opened" : "ready"}`);
|
|
3916
|
+
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, back on '${base}'`));
|
|
3917
|
+
} else {
|
|
3918
|
+
console.info(`${colors_default.green("\u2713")} Sync branch pushed`);
|
|
3919
|
+
console.info(colors_default.dim(` '${branch}' is on origin, back on '${base}'`));
|
|
3920
|
+
}
|
|
3700
3921
|
}
|
|
3701
3922
|
function finalizeWorkspace(forkPath) {
|
|
3702
3923
|
for (const args of [["install"], ["check"]]) {
|
|
3703
3924
|
console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
|
|
3704
|
-
const result =
|
|
3925
|
+
const result = spawnSync2("pnpm", args, { cwd: forkPath, stdio: "inherit" });
|
|
3705
3926
|
if (result.status !== 0) return false;
|
|
3706
3927
|
}
|
|
3707
3928
|
return true;
|
|
@@ -3709,13 +3930,7 @@ function finalizeWorkspace(forkPath) {
|
|
|
3709
3930
|
async function runSyncCycle(config) {
|
|
3710
3931
|
const { forkPath } = config;
|
|
3711
3932
|
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
|
-
});
|
|
3933
|
+
const result = await runSync(config, { stagedBranch: branch.temporaryBranch });
|
|
3719
3934
|
if (config.settings.syncWithPackages !== false) {
|
|
3720
3935
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
3721
3936
|
}
|
|
@@ -3734,7 +3949,7 @@ async function resumeSyncMerge(config, branch) {
|
|
|
3734
3949
|
if (conflicts.length > 0) {
|
|
3735
3950
|
console.info(colors_default.yellow(`${conflicts.length} file(s) still conflict on '${branch}':`));
|
|
3736
3951
|
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."));
|
|
3952
|
+
console.info(colors_default.dim("resolve and stage them, then re-run `pnpm cella sync` to finish (it commits for you)."));
|
|
3738
3953
|
return;
|
|
3739
3954
|
}
|
|
3740
3955
|
await stageAll(forkPath);
|
|
@@ -3745,9 +3960,8 @@ async function resumeSyncMerge(config, branch) {
|
|
|
3745
3960
|
);
|
|
3746
3961
|
return;
|
|
3747
3962
|
}
|
|
3748
|
-
const
|
|
3963
|
+
const message = await buildSyncCommitMessage(forkPath, "MERGE_HEAD");
|
|
3749
3964
|
await stageAll(forkPath);
|
|
3750
|
-
const message = upstreamSha ? `${SYNC_PR_TITLE} ${upstreamSha}` : SYNC_PR_TITLE;
|
|
3751
3965
|
await commitSquash(forkPath, message);
|
|
3752
3966
|
console.info();
|
|
3753
3967
|
console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
|
|
@@ -3766,14 +3980,12 @@ async function runSyncCommand(config) {
|
|
|
3766
3980
|
console.info();
|
|
3767
3981
|
console.info(
|
|
3768
3982
|
colors_default.yellow(
|
|
3769
|
-
`sync branch '${currentBranch}' has uncommitted changes
|
|
3770
|
-
commit them (\`git commit --amend --no-edit\` or a new commit)
|
|
3983
|
+
`sync branch '${currentBranch}' has uncommitted changes.
|
|
3984
|
+
commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
3771
3985
|
)
|
|
3772
3986
|
);
|
|
3773
3987
|
return;
|
|
3774
3988
|
}
|
|
3775
|
-
console.info();
|
|
3776
|
-
console.info(colors_default.green(`sync branch '${currentBranch}' is already committed.`));
|
|
3777
3989
|
await shipSyncBranch(config, currentBranch);
|
|
3778
3990
|
return;
|
|
3779
3991
|
}
|
|
@@ -3786,8 +3998,10 @@ commit them (\`git commit --amend --no-edit\` or a new commit), then re-run \`pn
|
|
|
3786
3998
|
}
|
|
3787
3999
|
const { temporaryBranch } = outcome.branch;
|
|
3788
4000
|
if (outcome.status === "conflicts") {
|
|
3789
|
-
console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve them
|
|
4001
|
+
console.info(`${warningMark} ${colors_default.yellow(`conflicts on '${temporaryBranch}'. Resolve and stage them, then:`)}`);
|
|
3790
4002
|
printFinishSteps();
|
|
4003
|
+
console.info(colors_default.dim(" rerun commits the sync, pushes the branch, and opens a PR."));
|
|
4004
|
+
console.info(colors_default.dim(" let the rerun commit \u2014 a manual `git commit` records a merge commit that bloats the PR."));
|
|
3791
4005
|
}
|
|
3792
4006
|
}
|
|
3793
4007
|
|
|
@@ -3877,7 +4091,7 @@ async function runForks(config) {
|
|
|
3877
4091
|
console.error(colors_default.red(`fork '${config.fork}' not found in config`));
|
|
3878
4092
|
return;
|
|
3879
4093
|
}
|
|
3880
|
-
const resolvedPath =
|
|
4094
|
+
const resolvedPath = resolve4(config.forkPath, match.localPath);
|
|
3881
4095
|
await syncFork(config, match, resolvedPath);
|
|
3882
4096
|
return;
|
|
3883
4097
|
}
|
|
@@ -3890,9 +4104,9 @@ async function runForks(config) {
|
|
|
3890
4104
|
loop: false
|
|
3891
4105
|
});
|
|
3892
4106
|
if (selectedPath === "_exit") {
|
|
3893
|
-
|
|
4107
|
+
process8.exit(0);
|
|
3894
4108
|
}
|
|
3895
|
-
const resolvedForkPath =
|
|
4109
|
+
const resolvedForkPath = resolve4(config.forkPath, selectedPath);
|
|
3896
4110
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
3897
4111
|
if (!selectedFork) {
|
|
3898
4112
|
console.error(colors_default.red(`fork '${selectedPath}' not found in config`));
|
|
@@ -3905,10 +4119,10 @@ async function runForks(config) {
|
|
|
3905
4119
|
|
|
3906
4120
|
// src/services/stats.ts
|
|
3907
4121
|
import { readFile as readFile2 } from "fs/promises";
|
|
3908
|
-
import { join as
|
|
4122
|
+
import { join as join16 } from "path";
|
|
3909
4123
|
|
|
3910
4124
|
// src/utils/coverage-utils.ts
|
|
3911
|
-
import { spawnSync as
|
|
4125
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3912
4126
|
import * as fs3 from "fs";
|
|
3913
4127
|
import * as path3 from "path";
|
|
3914
4128
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -3931,7 +4145,7 @@ function formatAge(ms) {
|
|
|
3931
4145
|
}
|
|
3932
4146
|
function refreshCoverage(forkPath) {
|
|
3933
4147
|
console.info(colors_default.dim("running `pnpm test` (this spins up Docker and runs the full suite)..."));
|
|
3934
|
-
const result =
|
|
4148
|
+
const result = spawnSync3("pnpm", ["test"], { cwd: forkPath, stdio: "inherit" });
|
|
3935
4149
|
return result.status === 0;
|
|
3936
4150
|
}
|
|
3937
4151
|
function printCoverageSummary(forkPath, options = {}) {
|
|
@@ -3993,7 +4207,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
3993
4207
|
// src/services/stats.ts
|
|
3994
4208
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
3995
4209
|
async function getWorkspacePackages(forkPath) {
|
|
3996
|
-
const content = await readFile2(
|
|
4210
|
+
const content = await readFile2(join16(forkPath, "pnpm-workspace.yaml"), "utf-8");
|
|
3997
4211
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
3998
4212
|
if (pattern.endsWith("/*")) {
|
|
3999
4213
|
const base = pattern.slice(0, -2);
|
|
@@ -4148,27 +4362,27 @@ async function runStats(forkPath, options = {}) {
|
|
|
4148
4362
|
|
|
4149
4363
|
// src/cella-cli.ts
|
|
4150
4364
|
function getForkPath() {
|
|
4151
|
-
const envPath =
|
|
4365
|
+
const envPath = getEnv("CELLA_FORK_PATH");
|
|
4152
4366
|
if (envPath) {
|
|
4153
|
-
const resolved =
|
|
4154
|
-
if (!
|
|
4367
|
+
const resolved = resolve5(envPath);
|
|
4368
|
+
if (!existsSync10(resolved) || !statSync2(resolved).isDirectory()) {
|
|
4155
4369
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
4156
4370
|
}
|
|
4157
4371
|
return resolved;
|
|
4158
4372
|
}
|
|
4159
|
-
let cwd =
|
|
4373
|
+
let cwd = process9.cwd();
|
|
4160
4374
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
4161
|
-
cwd =
|
|
4375
|
+
cwd = resolve5(cwd, "../..");
|
|
4162
4376
|
}
|
|
4163
4377
|
return cwd;
|
|
4164
4378
|
}
|
|
4165
4379
|
async function preflight(forkPath) {
|
|
4166
|
-
if (!
|
|
4380
|
+
if (!existsSync10(join17(forkPath, ".git"))) {
|
|
4167
4381
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
4168
4382
|
}
|
|
4169
4383
|
}
|
|
4170
4384
|
function isHelpOrVersionRequest() {
|
|
4171
|
-
return
|
|
4385
|
+
return process9.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process9.argv[2] === "help";
|
|
4172
4386
|
}
|
|
4173
4387
|
async function main() {
|
|
4174
4388
|
registerSignalHandlers();
|
|
@@ -4209,7 +4423,7 @@ async function main() {
|
|
|
4209
4423
|
} catch (error) {
|
|
4210
4424
|
console.error();
|
|
4211
4425
|
console.error(`${colors_default.red("\u2717")} ${error instanceof Error ? error.message : "unknown error"}`);
|
|
4212
|
-
|
|
4426
|
+
process9.exit(1);
|
|
4213
4427
|
}
|
|
4214
4428
|
}
|
|
4215
4429
|
main();
|