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