@cellajs/cli 0.2.3 → 0.3.0
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 +30 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +1303 -971
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
// src/cella-cli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { existsSync as existsSync13, statSync as statSync3 } from "fs";
|
|
5
|
+
import { join as join21, resolve as resolve7 } from "path";
|
|
6
6
|
import process9 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import { readFileSync } from "fs";
|
|
10
|
-
import { join as
|
|
10
|
+
import { join as join8 } from "path";
|
|
11
11
|
import process5 from "process";
|
|
12
12
|
import { select } from "@inquirer/prompts";
|
|
13
13
|
import { Command } from "commander";
|
|
@@ -220,7 +220,7 @@ import process4 from "process";
|
|
|
220
220
|
// package.json
|
|
221
221
|
var package_default = {
|
|
222
222
|
name: "@cellajs/cli",
|
|
223
|
-
version: "0.
|
|
223
|
+
version: "0.3.0",
|
|
224
224
|
private: false,
|
|
225
225
|
license: "MIT",
|
|
226
226
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -270,25 +270,25 @@ var package_default = {
|
|
|
270
270
|
prepare: "lefthook install"
|
|
271
271
|
},
|
|
272
272
|
dependencies: {
|
|
273
|
-
"@inquirer/core": "^
|
|
273
|
+
"@inquirer/core": "^12.0.3",
|
|
274
274
|
"@inquirer/prompts": "^8.7.2",
|
|
275
|
-
"@pierre/diffs": "^1.
|
|
275
|
+
"@pierre/diffs": "^1.5.1",
|
|
276
276
|
commander: "^15.0.0",
|
|
277
277
|
"ts-morph": "^28.0.0",
|
|
278
278
|
zod: "^4.6.5"
|
|
279
279
|
},
|
|
280
280
|
devDependencies: {
|
|
281
|
-
"@biomejs/biome": "^2.5.
|
|
281
|
+
"@biomejs/biome": "^2.5.15",
|
|
282
282
|
"@commitlint/cli": "^21.2.3",
|
|
283
283
|
"@commitlint/config-conventional": "^21.2.3",
|
|
284
|
-
"@evilmartians/lefthook": "^2.1.
|
|
285
|
-
"@types/node": "26.
|
|
286
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
284
|
+
"@evilmartians/lefthook": "^2.1.15",
|
|
285
|
+
"@types/node": "26.6.3",
|
|
286
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
287
287
|
rimraf: "^6.1.3",
|
|
288
288
|
tsup: "^8.5.1",
|
|
289
289
|
tsx: "^4.23.15",
|
|
290
290
|
typescript: "^6.0.3",
|
|
291
|
-
vitest: "^
|
|
291
|
+
vitest: "^5.0.3"
|
|
292
292
|
},
|
|
293
293
|
publishConfig: {
|
|
294
294
|
access: "public"
|
|
@@ -800,415 +800,425 @@ function printFlagWarnings(options) {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
// src/utils/overrides.ts
|
|
803
|
+
import { existsSync as existsSync5 } from "fs";
|
|
804
|
+
import { join as join7 } from "path";
|
|
805
|
+
|
|
806
|
+
// src/utils/migration-notes.ts
|
|
807
|
+
import { existsSync as existsSync4 } from "fs";
|
|
808
|
+
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
809
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
810
|
+
|
|
811
|
+
// src/utils/git.ts
|
|
812
|
+
import { execFile } from "child_process";
|
|
803
813
|
import { existsSync as existsSync3 } from "fs";
|
|
804
|
-
import {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
814
|
+
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
815
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
816
|
+
import { promisify } from "util";
|
|
817
|
+
|
|
818
|
+
// src/utils/manifest.ts
|
|
819
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
820
|
+
import { dirname, join as join4 } from "path";
|
|
821
|
+
var MANIFEST_FILE = "cella/cella.manifest.json";
|
|
822
|
+
async function readSyncManifest(cwd) {
|
|
823
|
+
try {
|
|
824
|
+
const parsed = JSON.parse(await readFile(join4(cwd, MANIFEST_FILE), "utf8"));
|
|
825
|
+
const commit = parsed?.upstream?.commit;
|
|
826
|
+
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
827
|
+
return parsed;
|
|
828
|
+
} catch {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
810
831
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
832
|
+
async function readManifestBase(cwd) {
|
|
833
|
+
const manifest = await readSyncManifest(cwd);
|
|
834
|
+
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
813
835
|
}
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
|
|
836
|
+
async function writeSyncManifest(cwd, manifest) {
|
|
837
|
+
const manifestPath = join4(cwd, MANIFEST_FILE);
|
|
838
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
839
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
840
|
+
`, "utf8");
|
|
817
841
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
842
|
+
|
|
843
|
+
// src/utils/git.ts
|
|
844
|
+
var execFileAsync = promisify(execFile);
|
|
845
|
+
function sanitizeCredentials(text) {
|
|
846
|
+
return text.replace(/https?:\/\/[^@\s]+@/g, (match) => {
|
|
847
|
+
const protocol = match.startsWith("https") ? "https" : "http";
|
|
848
|
+
return `${protocol}://***@`;
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
async function git(args, cwd, options = {}) {
|
|
852
|
+
const env = {
|
|
853
|
+
...getEnvSnapshot(),
|
|
854
|
+
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
855
|
+
...options.env
|
|
856
|
+
};
|
|
857
|
+
const maxBuffer = options.maxBuffer ?? 50 * 1024 * 1024;
|
|
858
|
+
try {
|
|
859
|
+
const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer });
|
|
860
|
+
return stdout.trim();
|
|
861
|
+
} catch (error) {
|
|
862
|
+
if (options.ignoreErrors) return "";
|
|
863
|
+
if (error instanceof Error) {
|
|
864
|
+
error.message = sanitizeCredentials(error.message);
|
|
865
|
+
}
|
|
866
|
+
throw error;
|
|
831
867
|
}
|
|
832
|
-
for (const group of groups.values()) group.paths.sort();
|
|
833
|
-
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
834
868
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
if (unpinned) return false;
|
|
838
|
-
return isPinned(filePath, config);
|
|
869
|
+
async function getCurrentBranch(cwd) {
|
|
870
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
839
871
|
}
|
|
840
|
-
function
|
|
841
|
-
|
|
872
|
+
async function switchBranch(cwd, branch) {
|
|
873
|
+
await git(["switch", branch], cwd);
|
|
842
874
|
}
|
|
843
|
-
function
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
875
|
+
async function createBranchFrom(cwd, branch, startPoint) {
|
|
876
|
+
await git(["switch", "-c", branch, startPoint], cwd);
|
|
877
|
+
}
|
|
878
|
+
async function deleteBranch(cwd, branch) {
|
|
879
|
+
await git(["branch", "-D", branch], cwd, { ignoreErrors: true });
|
|
880
|
+
}
|
|
881
|
+
async function pullFastForward(cwd) {
|
|
882
|
+
await git(["pull", "--ff-only"], cwd, { ignoreErrors: true, skipEditor: true });
|
|
883
|
+
}
|
|
884
|
+
async function getUpstreamStatus(cwd) {
|
|
885
|
+
const upstream = await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], cwd, {
|
|
886
|
+
ignoreErrors: true
|
|
887
|
+
});
|
|
888
|
+
if (!upstream) return { upstream: null, ahead: 0, behind: 0 };
|
|
889
|
+
const remote = upstream.split("/")[0];
|
|
890
|
+
await git(["fetch", remote], cwd, { ignoreErrors: true, skipEditor: true });
|
|
891
|
+
const counts = await git(["rev-list", "--left-right", "--count", `HEAD...${upstream}`], cwd, { ignoreErrors: true });
|
|
892
|
+
const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
893
|
+
return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
894
|
+
}
|
|
895
|
+
async function pushBranch(cwd, remote, branch, options = {}) {
|
|
896
|
+
await git(["push", "-u", ...options.forceWithLease ? ["--force-with-lease"] : [], remote, branch], cwd);
|
|
897
|
+
}
|
|
898
|
+
async function listBranchMergeCommits(cwd, baseRef) {
|
|
899
|
+
const out = await git(["rev-list", "--merges", "--first-parent", `${baseRef}..HEAD`], cwd, { ignoreErrors: true });
|
|
900
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
901
|
+
}
|
|
902
|
+
async function flattenBranch(cwd, baseRef, message) {
|
|
903
|
+
const parent = await getMergeBase(cwd, baseRef, "HEAD");
|
|
904
|
+
const tree = await git(["rev-parse", "HEAD^{tree}"], cwd);
|
|
905
|
+
const commit = await git(["commit-tree", tree, "-p", parent, "-m", message], cwd);
|
|
906
|
+
await git(["reset", "--hard", commit], cwd);
|
|
907
|
+
}
|
|
908
|
+
async function commitSquash(cwd, message) {
|
|
909
|
+
for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
|
|
910
|
+
const path4 = join5(cwd, ".git", file);
|
|
911
|
+
if (existsSync3(path4)) await unlink(path4);
|
|
860
912
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
913
|
+
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
914
|
+
}
|
|
915
|
+
async function stageAll(cwd) {
|
|
916
|
+
await git(["add", "-A"], cwd);
|
|
917
|
+
}
|
|
918
|
+
function mergeInProgress(cwd) {
|
|
919
|
+
return existsSync3(join5(cwd, ".git", "MERGE_HEAD"));
|
|
920
|
+
}
|
|
921
|
+
async function getShortSha(cwd, ref) {
|
|
922
|
+
return git(["rev-parse", "--short", ref], cwd);
|
|
923
|
+
}
|
|
924
|
+
async function getWorkingTreeChangeCount(cwd) {
|
|
925
|
+
const status = await git(["status", "--porcelain"], cwd);
|
|
926
|
+
if (!status) return 0;
|
|
927
|
+
return status.split("\n").filter(Boolean).length;
|
|
928
|
+
}
|
|
929
|
+
async function remoteExists(cwd, remoteName) {
|
|
930
|
+
const remotes = await git(["remote"], cwd);
|
|
931
|
+
return remotes.split("\n").includes(remoteName);
|
|
932
|
+
}
|
|
933
|
+
async function ensureRemote(cwd, remoteName, url) {
|
|
934
|
+
if (await remoteExists(cwd, remoteName)) {
|
|
935
|
+
await git(["remote", "set-url", remoteName, url], cwd);
|
|
936
|
+
} else {
|
|
937
|
+
await git(["remote", "add", remoteName, url], cwd);
|
|
876
938
|
}
|
|
877
|
-
return warnings;
|
|
878
939
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
940
|
+
async function fetch2(cwd, remoteName) {
|
|
941
|
+
await git(["fetch", remoteName], cwd);
|
|
942
|
+
}
|
|
943
|
+
async function fetchUpstreamTags(cwd, remoteName) {
|
|
944
|
+
await git(["fetch", remoteName, "--no-tags", `+refs/tags/*:refs/${remoteName}/tags/*`], cwd);
|
|
945
|
+
}
|
|
946
|
+
async function resolveLatestReleaseTag(cwd, remoteName) {
|
|
947
|
+
const prefix = `refs/${remoteName}/tags/`;
|
|
948
|
+
const output = await git(["for-each-ref", "--sort=-version:refname", "--format=%(refname)", prefix], cwd, {
|
|
949
|
+
ignoreErrors: true
|
|
950
|
+
});
|
|
951
|
+
if (!output) return null;
|
|
952
|
+
for (const line of output.split("\n")) {
|
|
953
|
+
const ref = line.trim();
|
|
954
|
+
if (!ref) continue;
|
|
955
|
+
const tag = ref.slice(prefix.length);
|
|
956
|
+
if (/^v\d/.test(tag)) return { tag, ref };
|
|
882
957
|
}
|
|
958
|
+
return null;
|
|
883
959
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
960
|
+
async function getCommitInfo(cwd, ref) {
|
|
961
|
+
const format = "%H%n%s%n%ar";
|
|
962
|
+
const output = await git(["log", "-1", `--format=${format}`, ref], cwd);
|
|
963
|
+
const [hash, message, date] = output.split("\n");
|
|
964
|
+
return { hash, message, date };
|
|
965
|
+
}
|
|
966
|
+
async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
967
|
+
const output = await git(["rev-list", "--count", `${fromRef}..${toRef}`], cwd);
|
|
968
|
+
return Number.parseInt(output.trim(), 10) || 0;
|
|
969
|
+
}
|
|
970
|
+
async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
971
|
+
const { oldestFirst = true, skip = 0, limit } = options;
|
|
972
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar"];
|
|
973
|
+
if (oldestFirst) args.push("--reverse");
|
|
974
|
+
args.push(`${fromRef}..${toRef}`);
|
|
975
|
+
const output = await git(args, cwd, { ignoreErrors: true });
|
|
976
|
+
if (!output) return [];
|
|
977
|
+
const commits = [];
|
|
978
|
+
for (const line of output.split("\n")) {
|
|
979
|
+
if (!line) continue;
|
|
980
|
+
const [hash, message, date] = line.split("");
|
|
981
|
+
if (!hash || !message || !date) continue;
|
|
982
|
+
commits.push({ hash, message, date });
|
|
890
983
|
}
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
scope: normalizedScope,
|
|
900
|
-
fork: typeof opts.fork === "string" ? opts.fork : void 0,
|
|
901
|
-
hard: opts.hard === true,
|
|
902
|
-
unpinned: opts.unpinned === true,
|
|
903
|
-
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
904
|
-
force: opts.force === true,
|
|
905
|
-
checkOverrides: opts.checkOverrides === true,
|
|
906
|
-
coverage: opts.coverage === true,
|
|
907
|
-
directMerge: opts.directMerge === true
|
|
908
|
-
};
|
|
984
|
+
const normalizedSkip = Math.max(0, skip);
|
|
985
|
+
const normalizedLimit = typeof limit === "number" && limit > 0 ? limit : void 0;
|
|
986
|
+
if (normalizedSkip === 0 && normalizedLimit === void 0) {
|
|
987
|
+
return commits;
|
|
988
|
+
}
|
|
989
|
+
const start = Math.min(normalizedSkip, commits.length);
|
|
990
|
+
const end = normalizedLimit === void 0 ? commits.length : Math.min(start + normalizedLimit, commits.length);
|
|
991
|
+
return commits.slice(start, end);
|
|
909
992
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
{ flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" },
|
|
931
|
-
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
932
|
-
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
|
|
933
|
-
{
|
|
934
|
-
flags: "--direct-merge",
|
|
935
|
-
description: "auto-merge (squash) the sync PR once checks pass \u2014 needs auto-merge enabled on the repo"
|
|
993
|
+
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
994
|
+
const info = /* @__PURE__ */ new Map();
|
|
995
|
+
try {
|
|
996
|
+
const output = await git(["log", "--name-only", "--format=%h %ct %ar", `${fromRef}..${toRef}`], cwd, {
|
|
997
|
+
ignoreErrors: true
|
|
998
|
+
});
|
|
999
|
+
if (!output.trim()) return info;
|
|
1000
|
+
const lines = output.split("\n");
|
|
1001
|
+
let currentHash = "";
|
|
1002
|
+
let currentDate = "";
|
|
1003
|
+
let currentTimestamp = 0;
|
|
1004
|
+
for (const line of lines) {
|
|
1005
|
+
if (!line) continue;
|
|
1006
|
+
const match = line.match(/^([a-f0-9]{7,})\s+(\d+)\s+(.+)$/);
|
|
1007
|
+
if (match) {
|
|
1008
|
+
currentHash = match[1];
|
|
1009
|
+
currentTimestamp = Number(match[2]);
|
|
1010
|
+
currentDate = match[3];
|
|
1011
|
+
} else if (currentHash && currentDate && !info.has(line)) {
|
|
1012
|
+
info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });
|
|
936
1013
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
menuDescription: () => "merge upstream changes + sync package.json"
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
name: "audit",
|
|
943
|
-
description: "check for outdated packages & vulnerabilities",
|
|
944
|
-
options: [
|
|
945
|
-
{ flags: "--list", description: "skip interactive update prompts after printing audit results" },
|
|
946
|
-
{ flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
|
|
947
|
-
{ flags: "--check-overrides", description: "check which pnpm.overrides are still needed" }
|
|
948
|
-
]
|
|
949
|
-
},
|
|
950
|
-
{
|
|
951
|
-
name: "forks",
|
|
952
|
-
description: "sync downstream to local fork repositories",
|
|
953
|
-
options: [
|
|
954
|
-
{ flags: "--fork <name>", description: "pre-select fork by name (skips fork selection prompt)" },
|
|
955
|
-
{ flags: "--log", description: "write complete file list to cella-sync.log for each synced fork" },
|
|
956
|
-
{ flags: "-V, --verbose", description: "show detailed output during operations" },
|
|
957
|
-
{ flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" }
|
|
958
|
-
],
|
|
959
|
-
includeInMenu: (context) => context.hasForks
|
|
960
|
-
},
|
|
961
|
-
{
|
|
962
|
-
name: "contributions",
|
|
963
|
-
description: "pull and adopt changes from forks",
|
|
964
|
-
options: [
|
|
965
|
-
{ flags: "--fork <name>", description: "select a specific fork directly (skips fork selection)" },
|
|
966
|
-
{ flags: "--list", description: "non-interactive output (one file per line)" },
|
|
967
|
-
{ flags: "--json", description: "machine-readable JSON output for tooling/agents" },
|
|
968
|
-
{ flags: "--diff <path>", description: "print the unified diff for a single contributed file, then exit" }
|
|
969
|
-
],
|
|
970
|
-
includeInMenu: (context) => context.hasForks
|
|
971
|
-
},
|
|
972
|
-
{
|
|
973
|
-
name: "stats",
|
|
974
|
-
description: "count files by category and workspace package",
|
|
975
|
-
options: [
|
|
976
|
-
{ flags: "-V, --verbose", description: "show detailed output during operations" },
|
|
977
|
-
{ flags: "--coverage", description: "regenerate test coverage before showing the stats summary" }
|
|
978
|
-
]
|
|
979
|
-
}
|
|
980
|
-
];
|
|
981
|
-
async function getMenuContext(userConfig, forkPath) {
|
|
982
|
-
return {
|
|
983
|
-
hasForks: (userConfig.forks?.length ?? 0) > 0,
|
|
984
|
-
isUpstreamRepo: JSON.parse(readFileSync(join5(forkPath, "package.json"), "utf8")).name === "cella"
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
function buildServiceChoices(context) {
|
|
988
|
-
const label = (name) => name.padEnd(14);
|
|
989
|
-
const baseChoices = serviceDefinitions.filter((service) => service.includeInMenu?.(context) ?? true).map((service) => ({
|
|
990
|
-
value: service.name,
|
|
991
|
-
name: `${label(service.name)}${colors_default.dim(service.menuDescription?.(context) ?? service.description)}`
|
|
992
|
-
}));
|
|
993
|
-
return [
|
|
994
|
-
...baseChoices,
|
|
995
|
-
{ type: "separator", separator: "\u2500".repeat(40) },
|
|
996
|
-
{ value: "exit", name: colors_default.red(`${label("exit")}${colors_default.dim("quit without doing anything")}`) }
|
|
997
|
-
];
|
|
998
|
-
}
|
|
999
|
-
function addServiceCommand(program, definition, setSelection) {
|
|
1000
|
-
const command = program.command(definition.name).description(definition.description);
|
|
1001
|
-
for (const option of definition.options ?? []) {
|
|
1002
|
-
command.option(option.flags, option.description);
|
|
1014
|
+
}
|
|
1015
|
+
} catch {
|
|
1003
1016
|
}
|
|
1004
|
-
|
|
1005
|
-
setSelection({
|
|
1006
|
-
service: definition.name,
|
|
1007
|
-
options: readOptions(opts)
|
|
1008
|
-
});
|
|
1009
|
-
});
|
|
1017
|
+
return info;
|
|
1010
1018
|
}
|
|
1011
|
-
function
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
[
|
|
1015
|
-
"",
|
|
1016
|
-
"Examples:",
|
|
1017
|
-
" $ cella analyze",
|
|
1018
|
-
" $ cella analyze --json --scope risk",
|
|
1019
|
-
" $ cella analyze --open-diff frontend/src/routes/index.tsx",
|
|
1020
|
-
" $ cella sync --hard",
|
|
1021
|
-
" $ cella sync --unpinned",
|
|
1022
|
-
" $ cella sync --track branch",
|
|
1023
|
-
" $ cella sync --direct-merge",
|
|
1024
|
-
" $ cella audit --check-overrides",
|
|
1025
|
-
" $ cella contributions --fork raak --json"
|
|
1026
|
-
].join("\n")
|
|
1027
|
-
);
|
|
1028
|
-
for (const definition of serviceDefinitions) {
|
|
1029
|
-
addServiceCommand(program, definition, setSelection);
|
|
1030
|
-
}
|
|
1031
|
-
return program;
|
|
1019
|
+
async function isClean(cwd) {
|
|
1020
|
+
const status = await git(["status", "--porcelain"], cwd);
|
|
1021
|
+
return !status;
|
|
1032
1022
|
}
|
|
1033
|
-
function
|
|
1034
|
-
if (
|
|
1035
|
-
|
|
1023
|
+
async function assertClean(cwd, subject = "working directory") {
|
|
1024
|
+
if (!await isClean(cwd)) {
|
|
1025
|
+
throw new Error(`${subject} has uncommitted changes. please commit or stash before syncing.`);
|
|
1036
1026
|
}
|
|
1037
|
-
let selection = { options: readOptions({}) };
|
|
1038
|
-
const program = buildProgram((nextSelection) => {
|
|
1039
|
-
selection = nextSelection;
|
|
1040
|
-
});
|
|
1041
|
-
program.parse(argv);
|
|
1042
|
-
return selection;
|
|
1043
1027
|
}
|
|
1044
|
-
async function
|
|
1045
|
-
|
|
1046
|
-
message: "choose a service:",
|
|
1047
|
-
choices: buildServiceChoices(await getMenuContext(userConfig, forkPath)),
|
|
1048
|
-
loop: false
|
|
1049
|
-
});
|
|
1050
|
-
if (selected === "exit") {
|
|
1051
|
-
console.info(colors_default.dim("exiting..."));
|
|
1052
|
-
console.info();
|
|
1053
|
-
process5.exit(0);
|
|
1054
|
-
}
|
|
1055
|
-
console.info();
|
|
1056
|
-
return selected;
|
|
1028
|
+
async function createWorktree(cwd, worktreePath, commitRef) {
|
|
1029
|
+
await git(["worktree", "add", "--detach", worktreePath, commitRef], cwd);
|
|
1057
1030
|
}
|
|
1058
|
-
function
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
...userConfig,
|
|
1062
|
-
forkPath,
|
|
1063
|
-
upstreamRef: branchRef,
|
|
1064
|
-
service: selection.service ?? "analyze",
|
|
1065
|
-
...selection.options
|
|
1066
|
-
};
|
|
1031
|
+
async function removeWorktree(cwd, worktreePath) {
|
|
1032
|
+
await git(["worktree", "remove", worktreePath, "--force"], cwd, { ignoreErrors: true });
|
|
1033
|
+
await git(["worktree", "prune"], cwd, { ignoreErrors: true });
|
|
1067
1034
|
}
|
|
1068
|
-
async function
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
if (!selection.service) {
|
|
1078
|
-
selection.service = await promptForService(userConfig, forkPath);
|
|
1035
|
+
async function listWorktrees(cwd) {
|
|
1036
|
+
const output = await git(["worktree", "list", "--porcelain"], cwd);
|
|
1037
|
+
const lines = output.split("\n");
|
|
1038
|
+
const paths = [];
|
|
1039
|
+
for (const line of lines) {
|
|
1040
|
+
if (line.startsWith("worktree ")) {
|
|
1041
|
+
paths.push(line.slice(9));
|
|
1042
|
+
}
|
|
1079
1043
|
}
|
|
1080
|
-
return
|
|
1044
|
+
return paths;
|
|
1081
1045
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
// src/utils/diff.ts
|
|
1087
|
-
import { execFileSync } from "child_process";
|
|
1088
|
-
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1089
|
-
import { tmpdir } from "os";
|
|
1090
|
-
import { basename as basename2, join as join6 } from "path";
|
|
1091
|
-
import process6 from "process";
|
|
1092
|
-
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
1093
|
-
const args = ["diff", "--no-color"];
|
|
1094
|
-
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
1095
|
-
args.push(range, "--", filePath);
|
|
1046
|
+
async function merge(cwd, ref, options = {}) {
|
|
1047
|
+
const args = ["merge", "--no-ff", ref];
|
|
1048
|
+
if (options.noCommit) args.push("--no-commit");
|
|
1049
|
+
if (options.noEdit) args.push("--no-edit");
|
|
1096
1050
|
try {
|
|
1097
|
-
|
|
1051
|
+
await git(args, cwd, { skipEditor: true });
|
|
1052
|
+
return { success: true, conflicts: [] };
|
|
1098
1053
|
} catch (error) {
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1054
|
+
const conflicts = await getConflictedFiles(cwd);
|
|
1055
|
+
if (conflicts.length > 0) {
|
|
1056
|
+
return { success: false, conflicts };
|
|
1057
|
+
}
|
|
1058
|
+
throw error;
|
|
1101
1059
|
}
|
|
1102
1060
|
}
|
|
1103
|
-
function
|
|
1104
|
-
|
|
1061
|
+
async function getConflictedFiles(cwd) {
|
|
1062
|
+
const output = await git(["diff", "--name-only", "--diff-filter=U"], cwd, { ignoreErrors: true });
|
|
1063
|
+
if (!output) return [];
|
|
1064
|
+
return output.split("\n").filter(Boolean);
|
|
1105
1065
|
}
|
|
1106
|
-
async function
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1066
|
+
async function mergeAbort(cwd) {
|
|
1067
|
+
await git(["merge", "--abort"], cwd, { ignoreErrors: true });
|
|
1068
|
+
}
|
|
1069
|
+
async function fileExistsAtRef(cwd, ref, filePath) {
|
|
1070
|
+
const result = await git(["ls-tree", "--name-only", ref, "--", filePath], cwd, { ignoreErrors: true });
|
|
1071
|
+
return result !== "";
|
|
1072
|
+
}
|
|
1073
|
+
async function checkoutFromRef(cwd, ref, filePath) {
|
|
1074
|
+
await git(["checkout", ref, "--", filePath], cwd);
|
|
1075
|
+
}
|
|
1076
|
+
async function restoreWorktreeFromRef(cwd, ref, filePath) {
|
|
1077
|
+
await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
|
|
1078
|
+
}
|
|
1079
|
+
async function getMergeBase(cwd, ref1, ref2) {
|
|
1080
|
+
return git(["merge-base", ref1, ref2], cwd);
|
|
1081
|
+
}
|
|
1082
|
+
async function getFileChanges(cwd, baseRef, targetRef) {
|
|
1083
|
+
const output = await git(["diff-tree", "-r", "-M90%", "--no-commit-id", baseRef, targetRef], cwd, {
|
|
1084
|
+
ignoreErrors: true
|
|
1085
|
+
});
|
|
1086
|
+
const changes = /* @__PURE__ */ new Map();
|
|
1087
|
+
if (!output) return changes;
|
|
1088
|
+
for (const line of output.split("\n")) {
|
|
1089
|
+
if (!line) continue;
|
|
1090
|
+
const renameMatch = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) R\d*\s+(.+)\t(.+)$/);
|
|
1091
|
+
if (renameMatch) {
|
|
1092
|
+
const [, baseHash, targetHash, oldPath, newPath] = renameMatch;
|
|
1093
|
+
changes.set(newPath, {
|
|
1094
|
+
status: "R",
|
|
1095
|
+
baseHash: baseHash === "0".repeat(40) ? null : baseHash,
|
|
1096
|
+
targetHash: targetHash === "0".repeat(40) ? null : targetHash,
|
|
1097
|
+
oldPath,
|
|
1098
|
+
newPath
|
|
1099
|
+
});
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
const match = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) ([ADMT])\t(.+)$/);
|
|
1103
|
+
if (match) {
|
|
1104
|
+
const [, baseHash, targetHash, status, filePath] = match;
|
|
1105
|
+
changes.set(filePath, {
|
|
1106
|
+
status,
|
|
1107
|
+
baseHash: baseHash === "0".repeat(40) ? null : baseHash,
|
|
1108
|
+
targetHash: targetHash === "0".repeat(40) ? null : targetHash
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1135
1111
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1112
|
+
return changes;
|
|
1113
|
+
}
|
|
1114
|
+
async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
1115
|
+
const args = ["diff", "--numstat", "--no-renames", fromRef, toRef];
|
|
1116
|
+
if (paths.length > 0) args.push("--", ...paths);
|
|
1117
|
+
const out = await git(args, cwd, { ignoreErrors: true });
|
|
1118
|
+
const stat = /* @__PURE__ */ new Map();
|
|
1119
|
+
for (const line of out.split("\n")) {
|
|
1120
|
+
if (!line.trim()) continue;
|
|
1121
|
+
const [a, d, ...rest] = line.split(" ");
|
|
1122
|
+
const path4 = rest.join(" ");
|
|
1123
|
+
if (!path4) continue;
|
|
1124
|
+
stat.set(path4, {
|
|
1125
|
+
additions: a === "-" ? null : Number(a),
|
|
1126
|
+
deletions: d === "-" ? null : Number(d)
|
|
1127
|
+
});
|
|
1142
1128
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1129
|
+
return stat;
|
|
1130
|
+
}
|
|
1131
|
+
async function getFileHashesAtRef(cwd, ref) {
|
|
1132
|
+
const output = await git(["ls-tree", "-r", ref], cwd);
|
|
1133
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
1134
|
+
if (!output) return hashes;
|
|
1135
|
+
for (const line of output.split("\n")) {
|
|
1136
|
+
if (!line) continue;
|
|
1137
|
+
const match = line.match(/^\d+ \w+ ([a-f0-9]+)\t(.+)$/);
|
|
1138
|
+
if (match) {
|
|
1139
|
+
const [, hash, filePath] = match;
|
|
1140
|
+
hashes.set(filePath, hash);
|
|
1141
|
+
}
|
|
1151
1142
|
}
|
|
1152
|
-
|
|
1153
|
-
</head>
|
|
1154
|
-
<body>
|
|
1155
|
-
<header>
|
|
1156
|
-
<span class="brand">\u29C8 cella diff</span>
|
|
1157
|
-
<code>${escapeHtml(meta.filePath)}</code>
|
|
1158
|
-
<span class="sides">${escapeHtml(meta.srcLabel)} \u2190 \u2192 ${escapeHtml(meta.dstLabel)} ${note}</span>
|
|
1159
|
-
</header>
|
|
1160
|
-
<main>
|
|
1161
|
-
<!-- Declarative shadow DOM: the library's CSS targets :host, which only
|
|
1162
|
-
matches inside a shadow root (its browser component renders into one). -->
|
|
1163
|
-
<div class="diff-shell">
|
|
1164
|
-
<template shadowrootmode="open">
|
|
1165
|
-
${prerenderedHTML}
|
|
1166
|
-
</template>
|
|
1167
|
-
</div>
|
|
1168
|
-
</main>
|
|
1169
|
-
</body>
|
|
1170
|
-
</html>`;
|
|
1143
|
+
return hashes;
|
|
1171
1144
|
}
|
|
1172
|
-
function
|
|
1145
|
+
async function restoreToHead(cwd, filePath) {
|
|
1146
|
+
await git(["checkout", "HEAD", "--", filePath], cwd);
|
|
1147
|
+
}
|
|
1148
|
+
async function batchRestoreToHead(cwd, filePaths) {
|
|
1149
|
+
if (filePaths.length === 0) return;
|
|
1150
|
+
await git(["checkout", "HEAD", "--", ...filePaths], cwd);
|
|
1151
|
+
}
|
|
1152
|
+
async function gitRm(cwd, filePath) {
|
|
1153
|
+
await git(["rm", "-f", "--", filePath], cwd, { ignoreErrors: true });
|
|
1154
|
+
}
|
|
1155
|
+
async function batchGitRm(cwd, filePaths) {
|
|
1156
|
+
if (filePaths.length === 0) return;
|
|
1157
|
+
await git(["rm", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1158
|
+
}
|
|
1159
|
+
async function getStagedNewFiles(cwd) {
|
|
1160
|
+
const output = await git(["diff", "--cached", "--name-only", "--diff-filter=A"], cwd, { ignoreErrors: true });
|
|
1161
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
1162
|
+
}
|
|
1163
|
+
async function batchUnstageFiles(cwd, filePaths) {
|
|
1164
|
+
if (filePaths.length === 0) return;
|
|
1165
|
+
await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1166
|
+
}
|
|
1167
|
+
async function gitMv(cwd, oldPath, newPath) {
|
|
1168
|
+
const newDir = dirname2(join5(cwd, newPath));
|
|
1169
|
+
await mkdir2(newDir, { recursive: true });
|
|
1170
|
+
await git(["mv", "-f", oldPath, newPath], cwd);
|
|
1171
|
+
}
|
|
1172
|
+
async function fileExistsInWorktree(cwd, filePath) {
|
|
1173
|
+
return existsSync3(join5(cwd, filePath));
|
|
1174
|
+
}
|
|
1175
|
+
async function removeFileFromWorktree(cwd, filePath) {
|
|
1176
|
+
const fullPath = join5(cwd, filePath);
|
|
1173
1177
|
try {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
+
await unlink(fullPath);
|
|
1179
|
+
await cleanupEmptyParentDirs(cwd, dirname2(filePath));
|
|
1180
|
+
} catch {
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async function removeFileFully(cwd, filePath) {
|
|
1184
|
+
await gitRm(cwd, filePath);
|
|
1185
|
+
await removeFileFromWorktree(cwd, filePath);
|
|
1186
|
+
}
|
|
1187
|
+
async function cleanupEmptyParentDirs(cwd, relativePath) {
|
|
1188
|
+
if (!relativePath || relativePath === ".") return;
|
|
1189
|
+
const fullPath = join5(cwd, relativePath);
|
|
1190
|
+
try {
|
|
1191
|
+
const entries = await readdir(fullPath);
|
|
1192
|
+
if (entries.length === 0) {
|
|
1193
|
+
await rmdir(fullPath);
|
|
1194
|
+
await cleanupEmptyParentDirs(cwd, dirname2(relativePath));
|
|
1178
1195
|
}
|
|
1179
1196
|
} catch {
|
|
1180
1197
|
}
|
|
1181
1198
|
}
|
|
1182
|
-
async function
|
|
1183
|
-
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
openWithDefaultApp(outPath);
|
|
1189
|
-
return outPath;
|
|
1199
|
+
async function storeLastSyncRef(cwd, upstreamHash) {
|
|
1200
|
+
await git(["update-ref", "refs/cella/last-sync", upstreamHash], cwd);
|
|
1201
|
+
const head = await git(["rev-parse", "HEAD"], cwd, { ignoreErrors: true });
|
|
1202
|
+
if (head) {
|
|
1203
|
+
await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
|
|
1204
|
+
}
|
|
1190
1205
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
// src/utils/manifest.ts
|
|
1206
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1207
|
-
import { dirname, join as join7 } from "path";
|
|
1208
|
-
var MANIFEST_FILE = "cella/cella.manifest.json";
|
|
1209
|
-
async function readSyncManifest(cwd) {
|
|
1206
|
+
async function stagePath(cwd, path4) {
|
|
1207
|
+
await git(["add", "--", path4], cwd, { ignoreErrors: true });
|
|
1208
|
+
}
|
|
1209
|
+
async function getStoredSyncRef(cwd) {
|
|
1210
|
+
const ref = await git(["rev-parse", "refs/cella/last-sync"], cwd, { ignoreErrors: true });
|
|
1211
|
+
return ref || null;
|
|
1212
|
+
}
|
|
1213
|
+
async function getStoredSyncHead(cwd) {
|
|
1214
|
+
const ref = await git(["rev-parse", "refs/cella/last-sync-head"], cwd, { ignoreErrors: true });
|
|
1215
|
+
return ref || null;
|
|
1216
|
+
}
|
|
1217
|
+
async function readManifestAtRef(cwd, ref) {
|
|
1218
|
+
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
1219
|
+
if (!raw) return null;
|
|
1210
1220
|
try {
|
|
1211
|
-
const parsed = JSON.parse(
|
|
1221
|
+
const parsed = JSON.parse(raw);
|
|
1212
1222
|
const commit = parsed?.upstream?.commit;
|
|
1213
1223
|
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1214
1224
|
return parsed;
|
|
@@ -1216,588 +1226,686 @@ async function readSyncManifest(cwd) {
|
|
|
1216
1226
|
return null;
|
|
1217
1227
|
}
|
|
1218
1228
|
}
|
|
1219
|
-
async function
|
|
1220
|
-
const manifest = await
|
|
1229
|
+
async function readManifestBaseAtRef(cwd, ref) {
|
|
1230
|
+
const manifest = await readManifestAtRef(cwd, ref);
|
|
1221
1231
|
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
1222
1232
|
}
|
|
1223
|
-
async function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
1227
|
-
`, "utf8");
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// src/utils/git.ts
|
|
1231
|
-
var execFileAsync = promisify(execFile);
|
|
1232
|
-
function sanitizeCredentials(text) {
|
|
1233
|
-
return text.replace(/https?:\/\/[^@\s]+@/g, (match) => {
|
|
1234
|
-
const protocol = match.startsWith("https") ? "https" : "http";
|
|
1235
|
-
return `${protocol}://***@`;
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
async function git(args, cwd, options = {}) {
|
|
1239
|
-
const env = {
|
|
1240
|
-
...getEnvSnapshot(),
|
|
1241
|
-
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
1242
|
-
...options.env
|
|
1243
|
-
};
|
|
1244
|
-
const maxBuffer = options.maxBuffer ?? 50 * 1024 * 1024;
|
|
1233
|
+
async function readPackageVersionAtRef(cwd, ref) {
|
|
1234
|
+
const raw = await git(["show", `${ref}:package.json`], cwd, { ignoreErrors: true });
|
|
1235
|
+
if (!raw) return null;
|
|
1245
1236
|
try {
|
|
1246
|
-
const
|
|
1247
|
-
return
|
|
1248
|
-
} catch
|
|
1249
|
-
|
|
1250
|
-
if (error instanceof Error) {
|
|
1251
|
-
error.message = sanitizeCredentials(error.message);
|
|
1252
|
-
}
|
|
1253
|
-
throw error;
|
|
1237
|
+
const version = JSON.parse(raw).version;
|
|
1238
|
+
return typeof version === "string" && version ? version : null;
|
|
1239
|
+
} catch {
|
|
1240
|
+
return null;
|
|
1254
1241
|
}
|
|
1255
1242
|
}
|
|
1256
|
-
async function
|
|
1257
|
-
|
|
1243
|
+
async function commitObjectExists(cwd, sha) {
|
|
1244
|
+
try {
|
|
1245
|
+
await git(["cat-file", "-e", `${sha}^{commit}`], cwd);
|
|
1246
|
+
return true;
|
|
1247
|
+
} catch {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1258
1250
|
}
|
|
1259
|
-
async function
|
|
1260
|
-
await git(["
|
|
1251
|
+
async function getRootCommit(cwd, ref) {
|
|
1252
|
+
const out = await git(["rev-list", "--max-parents=0", ref], cwd, { ignoreErrors: true });
|
|
1253
|
+
const roots = out.split("\n").filter(Boolean);
|
|
1254
|
+
return roots.length > 0 ? roots[roots.length - 1] : null;
|
|
1261
1255
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
async function
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1256
|
+
var SCAFFOLD_CANDIDATE_LIMIT = 400;
|
|
1257
|
+
var SCAFFOLD_SCORE_CONCURRENCY = 10;
|
|
1258
|
+
var SCAFFOLD_MATCH_MAX_DIFF_RATIO = 0.3;
|
|
1259
|
+
async function inferScaffoldBase(cwd, headRef, upstreamRef) {
|
|
1260
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1261
|
+
if (!root) return null;
|
|
1262
|
+
const rootDate = await git(["show", "-s", "--format=%cI", root], cwd, { ignoreErrors: true });
|
|
1263
|
+
if (!rootDate) return null;
|
|
1264
|
+
const before = new Date(new Date(rootDate).getTime() + 24 * 60 * 60 * 1e3).toISOString();
|
|
1265
|
+
const revList = await git(
|
|
1266
|
+
["rev-list", `--max-count=${SCAFFOLD_CANDIDATE_LIMIT}`, `--before=${before}`, upstreamRef],
|
|
1267
|
+
cwd,
|
|
1268
|
+
{ ignoreErrors: true }
|
|
1269
|
+
);
|
|
1270
|
+
const candidates = revList.split("\n").filter(Boolean);
|
|
1271
|
+
if (candidates.length === 0) return null;
|
|
1272
|
+
const totalPaths = (await git(["ls-tree", "-r", "--name-only", root], cwd)).split("\n").filter(Boolean).length;
|
|
1273
|
+
if (totalPaths === 0) return null;
|
|
1274
|
+
let best = null;
|
|
1275
|
+
outer: for (let i = 0; i < candidates.length; i += SCAFFOLD_SCORE_CONCURRENCY) {
|
|
1276
|
+
const chunk = candidates.slice(i, i + SCAFFOLD_SCORE_CONCURRENCY);
|
|
1277
|
+
const scores = await Promise.all(
|
|
1278
|
+
chunk.map(async (sha) => {
|
|
1279
|
+
try {
|
|
1280
|
+
const out = await git(["diff-tree", "-r", "--name-only", sha, root], cwd);
|
|
1281
|
+
return { sha, diff: out ? out.split("\n").length : 0 };
|
|
1282
|
+
} catch {
|
|
1283
|
+
return { sha, diff: Number.POSITIVE_INFINITY };
|
|
1284
|
+
}
|
|
1285
|
+
})
|
|
1286
|
+
);
|
|
1287
|
+
for (const score of scores) {
|
|
1288
|
+
if (!best || score.diff < best.diff) best = score;
|
|
1289
|
+
if (best.diff === 0) break outer;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (!best || best.diff / totalPaths > SCAFFOLD_MATCH_MAX_DIFF_RATIO) return null;
|
|
1293
|
+
return best.sha;
|
|
1270
1294
|
}
|
|
1271
|
-
async function
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
await git(["fetch", remote], cwd, { ignoreErrors: true, skipEditor: true });
|
|
1278
|
-
const counts = await git(["rev-list", "--left-right", "--count", `HEAD...${upstream}`], cwd, { ignoreErrors: true });
|
|
1279
|
-
const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
1280
|
-
return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
1295
|
+
async function readRootTrailerBase(cwd, headRef) {
|
|
1296
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1297
|
+
if (!root) return null;
|
|
1298
|
+
const message = await git(["show", "-s", "--format=%B", root], cwd, { ignoreErrors: true });
|
|
1299
|
+
const match = message.match(/^cella-base:\s*([0-9a-f]{40})$/im);
|
|
1300
|
+
return match ? match[1].toLowerCase() : null;
|
|
1281
1301
|
}
|
|
1282
|
-
async function
|
|
1283
|
-
await git(["
|
|
1302
|
+
async function isShallowRepository(cwd) {
|
|
1303
|
+
const out = await git(["rev-parse", "--is-shallow-repository"], cwd, { ignoreErrors: true });
|
|
1304
|
+
return out === "true";
|
|
1284
1305
|
}
|
|
1285
|
-
async function
|
|
1286
|
-
|
|
1287
|
-
|
|
1306
|
+
async function unshallowRepository(cwd) {
|
|
1307
|
+
try {
|
|
1308
|
+
await git(["fetch", "--unshallow"], cwd, { skipEditor: true });
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
const detail = error instanceof Error ? `
|
|
1311
|
+
|
|
1312
|
+
${error.message}` : "";
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`this is a shallow clone and its full history could not be restored automatically.
|
|
1315
|
+
Sync needs the complete history to find the common ancestor with upstream. Restore it with:
|
|
1316
|
+
|
|
1317
|
+
git fetch --unshallow
|
|
1318
|
+
|
|
1319
|
+
then re-run sync.${detail}`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1288
1322
|
}
|
|
1289
|
-
async function
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
await git(["
|
|
1323
|
+
async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
1324
|
+
if (await isShallowRepository(cwd)) {
|
|
1325
|
+
await unshallowRepository(cwd);
|
|
1326
|
+
}
|
|
1327
|
+
const nativeBase = await git(["merge-base", headRef, upstreamRef], cwd, { ignoreErrors: true });
|
|
1328
|
+
if (nativeBase) return;
|
|
1329
|
+
const sources = [
|
|
1330
|
+
() => getStoredSyncRef(cwd),
|
|
1331
|
+
() => readManifestBaseAtRef(cwd, headRef),
|
|
1332
|
+
() => readManifestBase(cwd),
|
|
1333
|
+
() => readRootTrailerBase(cwd, headRef),
|
|
1334
|
+
() => inferScaffoldBase(cwd, headRef, upstreamRef)
|
|
1335
|
+
];
|
|
1336
|
+
let baseSha = null;
|
|
1337
|
+
for (const source of sources) {
|
|
1338
|
+
const sha = await source();
|
|
1339
|
+
if (sha && await commitObjectExists(cwd, sha)) {
|
|
1340
|
+
baseSha = sha;
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (!baseSha) {
|
|
1345
|
+
throw new Error(
|
|
1346
|
+
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1347
|
+
|
|
1348
|
+
This fork has unrelated history with upstream \u2014 typical for a create-cella scaffold, or after
|
|
1349
|
+
upstream squashed its history \u2014 and no recorded or inferable base commit exists after fetching
|
|
1350
|
+
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1351
|
+
|
|
1352
|
+
git update-ref refs/cella/last-sync <upstream-sha>
|
|
1353
|
+
`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1357
|
+
if (!root) throw new Error("could not determine the fork's root commit to bootstrap sync ancestry");
|
|
1358
|
+
await git(["replace", "-f", "--graft", root, baseSha], cwd);
|
|
1359
|
+
await git(["update-ref", "refs/cella/last-sync", baseSha], cwd);
|
|
1294
1360
|
}
|
|
1295
|
-
async function
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1361
|
+
async function isAncestor(cwd, ancestor, descendant) {
|
|
1362
|
+
try {
|
|
1363
|
+
await git(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
|
|
1364
|
+
return true;
|
|
1365
|
+
} catch {
|
|
1366
|
+
return false;
|
|
1299
1367
|
}
|
|
1300
|
-
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
1301
1368
|
}
|
|
1302
|
-
async function
|
|
1303
|
-
await
|
|
1369
|
+
async function getLegacySyncRef(cwd, headRef) {
|
|
1370
|
+
const storedRef = await getStoredSyncRef(cwd);
|
|
1371
|
+
if (!storedRef) return null;
|
|
1372
|
+
const storedHead = await getStoredSyncHead(cwd);
|
|
1373
|
+
if (storedHead) {
|
|
1374
|
+
const currentHead = await git(["rev-parse", headRef], cwd, { ignoreErrors: true });
|
|
1375
|
+
if (currentHead && currentHead === storedHead) return null;
|
|
1376
|
+
}
|
|
1377
|
+
return storedRef;
|
|
1304
1378
|
}
|
|
1305
|
-
function
|
|
1306
|
-
|
|
1379
|
+
async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
1380
|
+
const gitBase = await getMergeBase(cwd, headRef, upstreamRef);
|
|
1381
|
+
const recorded = await readManifestBaseAtRef(cwd, headRef) ?? await getLegacySyncRef(cwd, headRef);
|
|
1382
|
+
if (!recorded) {
|
|
1383
|
+
return { base: gitBase, isStale: false, storedRef: null };
|
|
1384
|
+
}
|
|
1385
|
+
const recordedIsNewer = await isAncestor(cwd, gitBase, recorded);
|
|
1386
|
+
if (recordedIsNewer && recorded !== gitBase) {
|
|
1387
|
+
return { base: recorded, isStale: true, storedRef: recorded };
|
|
1388
|
+
}
|
|
1389
|
+
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
1307
1390
|
}
|
|
1308
|
-
async function
|
|
1309
|
-
|
|
1391
|
+
async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
1392
|
+
const headSha = await git(["rev-parse", headRef], cwd, { ignoreErrors: true });
|
|
1393
|
+
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
1394
|
+
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
1395
|
+
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
1396
|
+
if (existingReplacement) return fn();
|
|
1397
|
+
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
1398
|
+
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
1399
|
+
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
1400
|
+
try {
|
|
1401
|
+
return await fn();
|
|
1402
|
+
} finally {
|
|
1403
|
+
await git(["replace", "-d", headSha], cwd, { ignoreErrors: true });
|
|
1404
|
+
}
|
|
1310
1405
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1406
|
+
|
|
1407
|
+
// src/utils/migration-notes.ts
|
|
1408
|
+
var NOTES_DIR = "cella/migrations";
|
|
1409
|
+
var PENDING_FILE = "cella/cella.migrations.json";
|
|
1410
|
+
var NOTE_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1411
|
+
function isUpstreamOnly(filePath) {
|
|
1412
|
+
return filePath === NOTES_DIR || filePath.startsWith(`${NOTES_DIR}/`);
|
|
1413
|
+
}
|
|
1414
|
+
function bodyLines(lines) {
|
|
1415
|
+
let inComment = false;
|
|
1416
|
+
return lines.filter((line) => {
|
|
1417
|
+
if (!inComment && !line.trimStart().startsWith("<!--")) return true;
|
|
1418
|
+
inComment = !line.includes("-->");
|
|
1419
|
+
return false;
|
|
1420
|
+
});
|
|
1315
1421
|
}
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1422
|
+
function parseNote(id, source, fileNames) {
|
|
1423
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
1424
|
+
const close = lines[0] === "---" ? lines.indexOf("---", 1) : -1;
|
|
1425
|
+
const meta = /* @__PURE__ */ new Map();
|
|
1426
|
+
for (const line of lines.slice(1, Math.max(close, 1))) {
|
|
1427
|
+
const match = /^(\w+):\s*(.*)$/.exec(line);
|
|
1428
|
+
if (match) meta.set(match[1], match[2].trim());
|
|
1429
|
+
}
|
|
1430
|
+
const body = bodyLines(lines.slice(close + 1)).join("\n");
|
|
1431
|
+
const blocks = body.split(/\n\s*\n/).map((block) => block.trim()).filter(Boolean);
|
|
1432
|
+
const title = /^# (.+)$/.exec(blocks[0] ?? "")?.[1].trim();
|
|
1433
|
+
const summary = title && blocks[1] && !/^(#|>|[-*|]|```)/.test(blocks[1]) ? blocks[1].replace(/\s*\n\s*/g, " ") : "";
|
|
1434
|
+
return {
|
|
1435
|
+
id,
|
|
1436
|
+
title: title || id,
|
|
1437
|
+
summary,
|
|
1438
|
+
syncBreaking: meta.get("syncBreaking") === "true",
|
|
1439
|
+
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1440
|
+
roots: (meta.get("roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean),
|
|
1441
|
+
codemod: fileNames.find((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")) ?? null
|
|
1442
|
+
};
|
|
1319
1443
|
}
|
|
1320
|
-
async function
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1444
|
+
async function listNoteIds(cwd, ref) {
|
|
1445
|
+
const out = await git(["ls-tree", "--name-only", `${ref}:${NOTES_DIR}`], cwd, { ignoreErrors: true });
|
|
1446
|
+
return out.split("\n").filter((name) => NOTE_ID.test(name)).sort();
|
|
1447
|
+
}
|
|
1448
|
+
async function readNote(cwd, ref, id) {
|
|
1449
|
+
const source = await git(["show", `${ref}:${NOTES_DIR}/${id}/README.md`], cwd, { ignoreErrors: true });
|
|
1450
|
+
if (!source) return null;
|
|
1451
|
+
const files = await git(["ls-tree", "--name-only", `${ref}:${NOTES_DIR}/${id}`], cwd, { ignoreErrors: true });
|
|
1452
|
+
return parseNote(id, source, files.split("\n").filter(Boolean));
|
|
1453
|
+
}
|
|
1454
|
+
async function arrivedNoteIds(cwd, from, to) {
|
|
1455
|
+
const [before, after] = await Promise.all([listNoteIds(cwd, from), listNoteIds(cwd, to)]);
|
|
1456
|
+
const known = new Set(before);
|
|
1457
|
+
return after.filter((id) => !known.has(id));
|
|
1458
|
+
}
|
|
1459
|
+
async function readPending(cwd, syncRef) {
|
|
1460
|
+
const path4 = join6(cwd, PENDING_FILE);
|
|
1461
|
+
if (!existsSync4(path4)) return [];
|
|
1462
|
+
const parsed = JSON.parse(await readFile2(path4, "utf8"));
|
|
1463
|
+
if (Array.isArray(parsed.pending)) return parsed.pending.filter((id) => typeof id === "string");
|
|
1464
|
+
if (!Array.isArray(parsed.applied) || !syncRef) return [];
|
|
1465
|
+
const applied = new Set(parsed.applied);
|
|
1466
|
+
return (await listNoteIds(cwd, syncRef)).filter((id) => !applied.has(id));
|
|
1467
|
+
}
|
|
1468
|
+
async function writePending(cwd, ids) {
|
|
1469
|
+
const path4 = join6(cwd, PENDING_FILE);
|
|
1470
|
+
const pending = [...new Set(ids)].sort();
|
|
1471
|
+
if (pending.length === 0) {
|
|
1472
|
+
await rm(path4, { force: true });
|
|
1473
|
+
return;
|
|
1325
1474
|
}
|
|
1475
|
+
await mkdir3(dirname3(path4), { recursive: true });
|
|
1476
|
+
await writeFile2(path4, `${JSON.stringify({ pending }, null, 2)}
|
|
1477
|
+
`, "utf8");
|
|
1326
1478
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1479
|
+
function noteUrl(upstreamGitHubUrl, commit, id) {
|
|
1480
|
+
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${NOTES_DIR}/${id}/README.md` : void 0;
|
|
1329
1481
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1482
|
+
function formatNotesLine(total, open, arrived, options = {}) {
|
|
1483
|
+
const parts = [open === 0 ? `all ${total} handled` : `${Math.max(0, total - open)} of ${total} handled`];
|
|
1484
|
+
if (arrived) parts.push(`${arrived} ${options.dryRun ? "arrive" : "arrived"} with this sync`);
|
|
1485
|
+
if (open > 0) parts.push(`${open} open: pnpm cella migrate`);
|
|
1486
|
+
return `migration notes: ${parts.join(" \xB7 ")}`;
|
|
1332
1487
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1488
|
+
|
|
1489
|
+
// src/utils/overrides.ts
|
|
1490
|
+
function isUnderAnyFolder(filePath, folders) {
|
|
1491
|
+
return folders.some((folder) => {
|
|
1492
|
+
const entry = folder.replace(/\/+$/, "");
|
|
1493
|
+
return filePath === entry || filePath.startsWith(`${entry}/`);
|
|
1337
1494
|
});
|
|
1338
|
-
if (!output) return null;
|
|
1339
|
-
for (const line of output.split("\n")) {
|
|
1340
|
-
const ref = line.trim();
|
|
1341
|
-
if (!ref) continue;
|
|
1342
|
-
const tag = ref.slice(prefix.length);
|
|
1343
|
-
if (/^v\d/.test(tag)) return { tag, ref };
|
|
1344
|
-
}
|
|
1345
|
-
return null;
|
|
1346
1495
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
const output = await git(["log", "-1", `--format=${format}`, ref], cwd);
|
|
1350
|
-
const [hash, message, date] = output.split("\n");
|
|
1351
|
-
return { hash, message, date };
|
|
1496
|
+
function isIgnored(filePath, config) {
|
|
1497
|
+
return isUpstreamOnly(filePath) || isUnderAnyFolder(filePath, config.overrides?.ignored || []);
|
|
1352
1498
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
return
|
|
1499
|
+
function isPinned(filePath, config) {
|
|
1500
|
+
if (isManagedFile(filePath)) return true;
|
|
1501
|
+
return isUnderAnyFolder(filePath, config.overrides?.pinned || []);
|
|
1356
1502
|
}
|
|
1357
|
-
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
if (!
|
|
1369
|
-
|
|
1503
|
+
function groupIgnoredUpstreamChanges(files, ignored) {
|
|
1504
|
+
const entries = [...new Set(ignored.map((entry) => entry.replace(/\/+$/, "")).filter(Boolean))];
|
|
1505
|
+
const mostSpecificFirst = [...entries].sort((a, b) => b.length - a.length);
|
|
1506
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1507
|
+
for (const file of files) {
|
|
1508
|
+
if (!file.upstreamOnly || isManagedFile(file.path) || isGeneratedFile(file.path)) continue;
|
|
1509
|
+
const entry = mostSpecificFirst.find((candidate) => isUnderAnyFolder(file.path, [candidate]));
|
|
1510
|
+
if (!entry) continue;
|
|
1511
|
+
const group = groups.get(entry) ?? { entry, paths: [], added: 0, deleted: 0 };
|
|
1512
|
+
group.paths.push(file.path);
|
|
1513
|
+
if (!file.existsInFork) group.added++;
|
|
1514
|
+
if (!file.existsInUpstream) group.deleted++;
|
|
1515
|
+
groups.set(entry, group);
|
|
1370
1516
|
}
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
if (normalizedSkip === 0 && normalizedLimit === void 0) {
|
|
1374
|
-
return commits;
|
|
1375
|
-
}
|
|
1376
|
-
const start = Math.min(normalizedSkip, commits.length);
|
|
1377
|
-
const end = normalizedLimit === void 0 ? commits.length : Math.min(start + normalizedLimit, commits.length);
|
|
1378
|
-
return commits.slice(start, end);
|
|
1379
|
-
}
|
|
1380
|
-
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
1381
|
-
const info = /* @__PURE__ */ new Map();
|
|
1382
|
-
try {
|
|
1383
|
-
const output = await git(["log", "--name-only", "--format=%h %ct %ar", `${fromRef}..${toRef}`], cwd, {
|
|
1384
|
-
ignoreErrors: true
|
|
1385
|
-
});
|
|
1386
|
-
if (!output.trim()) return info;
|
|
1387
|
-
const lines = output.split("\n");
|
|
1388
|
-
let currentHash = "";
|
|
1389
|
-
let currentDate = "";
|
|
1390
|
-
let currentTimestamp = 0;
|
|
1391
|
-
for (const line of lines) {
|
|
1392
|
-
if (!line) continue;
|
|
1393
|
-
const match = line.match(/^([a-f0-9]{7,})\s+(\d+)\s+(.+)$/);
|
|
1394
|
-
if (match) {
|
|
1395
|
-
currentHash = match[1];
|
|
1396
|
-
currentTimestamp = Number(match[2]);
|
|
1397
|
-
currentDate = match[3];
|
|
1398
|
-
} else if (currentHash && currentDate && !info.has(line)) {
|
|
1399
|
-
info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
} catch {
|
|
1403
|
-
}
|
|
1404
|
-
return info;
|
|
1405
|
-
}
|
|
1406
|
-
async function isClean(cwd) {
|
|
1407
|
-
const status = await git(["status", "--porcelain"], cwd);
|
|
1408
|
-
return !status;
|
|
1409
|
-
}
|
|
1410
|
-
async function assertClean(cwd, subject = "working directory") {
|
|
1411
|
-
if (!await isClean(cwd)) {
|
|
1412
|
-
throw new Error(`${subject} has uncommitted changes. please commit or stash before syncing.`);
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
async function createWorktree(cwd, worktreePath, commitRef) {
|
|
1416
|
-
await git(["worktree", "add", "--detach", worktreePath, commitRef], cwd);
|
|
1517
|
+
for (const group of groups.values()) group.paths.sort();
|
|
1518
|
+
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
1417
1519
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1520
|
+
function isPinnedForSync(filePath, config, unpinned) {
|
|
1521
|
+
if (isManagedFile(filePath)) return true;
|
|
1522
|
+
if (unpinned) return false;
|
|
1523
|
+
return isPinned(filePath, config);
|
|
1421
1524
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const lines = output.split("\n");
|
|
1425
|
-
const paths = [];
|
|
1426
|
-
for (const line of lines) {
|
|
1427
|
-
if (line.startsWith("worktree ")) {
|
|
1428
|
-
paths.push(line.slice(9));
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
return paths;
|
|
1525
|
+
function hasGlobChars(entry) {
|
|
1526
|
+
return entry.includes("*") || entry.includes("?");
|
|
1432
1527
|
}
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1528
|
+
function validateOverrides(config, forkPath) {
|
|
1529
|
+
const warnings = [];
|
|
1530
|
+
const pinned = config.overrides?.pinned || [];
|
|
1531
|
+
for (const entry of pinned) {
|
|
1532
|
+
if (hasGlobChars(entry)) {
|
|
1533
|
+
warnings.push({
|
|
1534
|
+
type: "pinned-glob",
|
|
1535
|
+
pattern: entry,
|
|
1536
|
+
message: `pin uses glob (not supported, use a path or folder): ${entry}`
|
|
1537
|
+
});
|
|
1538
|
+
} else if (!existsSync5(join7(forkPath, entry))) {
|
|
1539
|
+
warnings.push({
|
|
1540
|
+
type: "pinned-not-found",
|
|
1541
|
+
pattern: entry,
|
|
1542
|
+
message: `pin not found: ${entry}`
|
|
1543
|
+
});
|
|
1444
1544
|
}
|
|
1445
|
-
throw error;
|
|
1446
1545
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
await git(["merge", "--abort"], cwd, { ignoreErrors: true });
|
|
1455
|
-
}
|
|
1456
|
-
async function fileExistsAtRef(cwd, ref, filePath) {
|
|
1457
|
-
const result = await git(["ls-tree", "--name-only", ref, "--", filePath], cwd, { ignoreErrors: true });
|
|
1458
|
-
return result !== "";
|
|
1459
|
-
}
|
|
1460
|
-
async function checkoutFromRef(cwd, ref, filePath) {
|
|
1461
|
-
await git(["checkout", ref, "--", filePath], cwd);
|
|
1462
|
-
}
|
|
1463
|
-
async function restoreWorktreeFromRef(cwd, ref, filePath) {
|
|
1464
|
-
await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
|
|
1465
|
-
}
|
|
1466
|
-
async function getMergeBase(cwd, ref1, ref2) {
|
|
1467
|
-
return git(["merge-base", ref1, ref2], cwd);
|
|
1468
|
-
}
|
|
1469
|
-
async function getFileChanges(cwd, baseRef, targetRef) {
|
|
1470
|
-
const output = await git(["diff-tree", "-r", "-M90%", "--no-commit-id", baseRef, targetRef], cwd, {
|
|
1471
|
-
ignoreErrors: true
|
|
1472
|
-
});
|
|
1473
|
-
const changes = /* @__PURE__ */ new Map();
|
|
1474
|
-
if (!output) return changes;
|
|
1475
|
-
for (const line of output.split("\n")) {
|
|
1476
|
-
if (!line) continue;
|
|
1477
|
-
const renameMatch = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) R\d*\s+(.+)\t(.+)$/);
|
|
1478
|
-
if (renameMatch) {
|
|
1479
|
-
const [, baseHash, targetHash, oldPath, newPath] = renameMatch;
|
|
1480
|
-
changes.set(newPath, {
|
|
1481
|
-
status: "R",
|
|
1482
|
-
baseHash: baseHash === "0".repeat(40) ? null : baseHash,
|
|
1483
|
-
targetHash: targetHash === "0".repeat(40) ? null : targetHash,
|
|
1484
|
-
oldPath,
|
|
1485
|
-
newPath
|
|
1546
|
+
const ignored = config.overrides?.ignored || [];
|
|
1547
|
+
for (const entry of ignored) {
|
|
1548
|
+
if (hasGlobChars(entry)) {
|
|
1549
|
+
warnings.push({
|
|
1550
|
+
type: "ignored-not-found",
|
|
1551
|
+
pattern: entry,
|
|
1552
|
+
message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
|
|
1486
1553
|
});
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
changes.set(filePath, {
|
|
1493
|
-
status,
|
|
1494
|
-
baseHash: baseHash === "0".repeat(40) ? null : baseHash,
|
|
1495
|
-
targetHash: targetHash === "0".repeat(40) ? null : targetHash
|
|
1554
|
+
} else if (!existsSync5(join7(forkPath, entry.replace(/\/+$/, "")))) {
|
|
1555
|
+
warnings.push({
|
|
1556
|
+
type: "ignored-not-found",
|
|
1557
|
+
pattern: entry,
|
|
1558
|
+
message: `ignored entry not found: ${entry}`
|
|
1496
1559
|
});
|
|
1497
1560
|
}
|
|
1498
1561
|
}
|
|
1499
|
-
return
|
|
1500
|
-
}
|
|
1501
|
-
async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
1502
|
-
const args = ["diff", "--numstat", "--no-renames", fromRef, toRef];
|
|
1503
|
-
if (paths.length > 0) args.push("--", ...paths);
|
|
1504
|
-
const out = await git(args, cwd, { ignoreErrors: true });
|
|
1505
|
-
const stat = /* @__PURE__ */ new Map();
|
|
1506
|
-
for (const line of out.split("\n")) {
|
|
1507
|
-
if (!line.trim()) continue;
|
|
1508
|
-
const [a, d, ...rest] = line.split(" ");
|
|
1509
|
-
const path4 = rest.join(" ");
|
|
1510
|
-
if (!path4) continue;
|
|
1511
|
-
stat.set(path4, {
|
|
1512
|
-
additions: a === "-" ? null : Number(a),
|
|
1513
|
-
deletions: d === "-" ? null : Number(d)
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
return stat;
|
|
1562
|
+
return warnings;
|
|
1517
1563
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (!output) return hashes;
|
|
1522
|
-
for (const line of output.split("\n")) {
|
|
1523
|
-
if (!line) continue;
|
|
1524
|
-
const match = line.match(/^\d+ \w+ ([a-f0-9]+)\t(.+)$/);
|
|
1525
|
-
if (match) {
|
|
1526
|
-
const [, hash, filePath] = match;
|
|
1527
|
-
hashes.set(filePath, hash);
|
|
1528
|
-
}
|
|
1564
|
+
function printWarnings(warnings) {
|
|
1565
|
+
for (const warning of warnings) {
|
|
1566
|
+
console.info(`${warningMark} ${warning.message}`);
|
|
1529
1567
|
}
|
|
1530
|
-
return hashes;
|
|
1531
|
-
}
|
|
1532
|
-
async function restoreToHead(cwd, filePath) {
|
|
1533
|
-
await git(["checkout", "HEAD", "--", filePath], cwd);
|
|
1534
|
-
}
|
|
1535
|
-
async function batchRestoreToHead(cwd, filePaths) {
|
|
1536
|
-
if (filePaths.length === 0) return;
|
|
1537
|
-
await git(["checkout", "HEAD", "--", ...filePaths], cwd);
|
|
1538
|
-
}
|
|
1539
|
-
async function gitRm(cwd, filePath) {
|
|
1540
|
-
await git(["rm", "-f", "--", filePath], cwd, { ignoreErrors: true });
|
|
1541
|
-
}
|
|
1542
|
-
async function batchGitRm(cwd, filePaths) {
|
|
1543
|
-
if (filePaths.length === 0) return;
|
|
1544
|
-
await git(["rm", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1545
|
-
}
|
|
1546
|
-
async function getStagedNewFiles(cwd) {
|
|
1547
|
-
const output = await git(["diff", "--cached", "--name-only", "--diff-filter=A"], cwd, { ignoreErrors: true });
|
|
1548
|
-
return output ? output.split("\n").filter(Boolean) : [];
|
|
1549
|
-
}
|
|
1550
|
-
async function batchUnstageFiles(cwd, filePaths) {
|
|
1551
|
-
if (filePaths.length === 0) return;
|
|
1552
|
-
await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1553
|
-
}
|
|
1554
|
-
async function gitMv(cwd, oldPath, newPath) {
|
|
1555
|
-
const newDir = dirname2(join8(cwd, newPath));
|
|
1556
|
-
await mkdir2(newDir, { recursive: true });
|
|
1557
|
-
await git(["mv", "-f", oldPath, newPath], cwd);
|
|
1558
|
-
}
|
|
1559
|
-
async function fileExistsInWorktree(cwd, filePath) {
|
|
1560
|
-
return existsSync4(join8(cwd, filePath));
|
|
1561
1568
|
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1569
|
+
|
|
1570
|
+
// src/cli.ts
|
|
1571
|
+
function readOptions(opts) {
|
|
1572
|
+
const scope = typeof opts.scope === "string" ? opts.scope : void 0;
|
|
1573
|
+
if (scope && scope !== "all" && scope !== "risk" && scope !== "protected") {
|
|
1574
|
+
throw new Error(`invalid --scope '${scope}'. expected one of: all, risk, protected`);
|
|
1568
1575
|
}
|
|
1576
|
+
const normalizedScope = scope;
|
|
1577
|
+
return {
|
|
1578
|
+
logFile: opts.log === true,
|
|
1579
|
+
verbose: opts.verbose === true,
|
|
1580
|
+
list: opts.list === true,
|
|
1581
|
+
json: opts.json === true,
|
|
1582
|
+
diff: typeof opts.diff === "string" ? opts.diff : void 0,
|
|
1583
|
+
openDiff: typeof opts.openDiff === "string" ? opts.openDiff : void 0,
|
|
1584
|
+
scope: normalizedScope,
|
|
1585
|
+
fork: typeof opts.fork === "string" ? opts.fork : void 0,
|
|
1586
|
+
hard: opts.hard === true,
|
|
1587
|
+
unpinned: opts.unpinned === true,
|
|
1588
|
+
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
1589
|
+
force: opts.force === true,
|
|
1590
|
+
checkOverrides: opts.checkOverrides === true,
|
|
1591
|
+
coverage: opts.coverage === true,
|
|
1592
|
+
all: opts.all === true,
|
|
1593
|
+
show: typeof opts.show === "string" ? opts.show : void 0,
|
|
1594
|
+
extract: typeof opts.extract === "string" ? opts.extract : void 0,
|
|
1595
|
+
mark: Array.isArray(opts.mark) ? opts.mark.filter((id) => typeof id === "string") : void 0
|
|
1596
|
+
};
|
|
1569
1597
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1598
|
+
var serviceDefinitions = [
|
|
1599
|
+
{
|
|
1600
|
+
name: "analyze",
|
|
1601
|
+
description: "dry run to see what would change on sync",
|
|
1602
|
+
options: [
|
|
1603
|
+
{ flags: "--log", description: "write complete file list to cella-sync.log" },
|
|
1604
|
+
{ flags: "--list", description: "non-interactive output for tooling (one file per line)" },
|
|
1605
|
+
{ flags: "--json", description: "machine-readable output for tooling/agents" },
|
|
1606
|
+
{ flags: "--scope <scope>", description: "analyze scope for --list/--json: all|risk|protected" },
|
|
1607
|
+
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
|
|
1608
|
+
{ flags: "--diff <path>", description: "print unified diff for one file, then exit" },
|
|
1609
|
+
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
1610
|
+
],
|
|
1611
|
+
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
name: "sync",
|
|
1615
|
+
description: "merge upstream changes into your app",
|
|
1616
|
+
options: [
|
|
1617
|
+
{ flags: "--log", description: "write complete file list to cella-sync.log" },
|
|
1618
|
+
{ flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" },
|
|
1619
|
+
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
1620
|
+
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" }
|
|
1621
|
+
],
|
|
1622
|
+
includeInMenu: (context) => !context.isUpstreamRepo,
|
|
1623
|
+
menuDescription: () => "merge upstream changes + sync package.json"
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "migrate",
|
|
1627
|
+
description: "list the upstream migration notes this app has not handled yet",
|
|
1628
|
+
options: [
|
|
1629
|
+
{ flags: "--all", description: "list every upstream note, handled or not" },
|
|
1630
|
+
{ flags: "--json", description: "machine-readable output for tooling/agents" },
|
|
1631
|
+
{ flags: "--show <id>", description: "print one note's README" },
|
|
1632
|
+
{ flags: "--extract <id>", description: "write one note's folder under node_modules/.cache to run its codemod" },
|
|
1633
|
+
{ flags: "--mark <ids...>", description: "record notes as handled" }
|
|
1634
|
+
],
|
|
1635
|
+
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
name: "audit",
|
|
1639
|
+
description: "check for outdated packages & vulnerabilities",
|
|
1640
|
+
options: [
|
|
1641
|
+
{ flags: "--list", description: "skip interactive update prompts after printing audit results" },
|
|
1642
|
+
{ flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
|
|
1643
|
+
{ flags: "--check-overrides", description: "check which pnpm.overrides are still needed" }
|
|
1644
|
+
]
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
name: "forks",
|
|
1648
|
+
description: "sync downstream to local fork repositories",
|
|
1649
|
+
options: [
|
|
1650
|
+
{ flags: "--fork <name>", description: "pre-select fork by name (skips fork selection prompt)" },
|
|
1651
|
+
{ flags: "--log", description: "write complete file list to cella-sync.log for each synced fork" },
|
|
1652
|
+
{ flags: "-V, --verbose", description: "show detailed output during operations" },
|
|
1653
|
+
{ flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" }
|
|
1654
|
+
],
|
|
1655
|
+
includeInMenu: (context) => context.hasForks
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
name: "contributions",
|
|
1659
|
+
description: "pull and adopt changes from forks",
|
|
1660
|
+
options: [
|
|
1661
|
+
{ flags: "--fork <name>", description: "select a specific fork directly (skips fork selection)" },
|
|
1662
|
+
{ flags: "--list", description: "non-interactive output (one file per line)" },
|
|
1663
|
+
{ flags: "--json", description: "machine-readable JSON output for tooling/agents" },
|
|
1664
|
+
{ flags: "--diff <path>", description: "print the unified diff for a single contributed file, then exit" }
|
|
1665
|
+
],
|
|
1666
|
+
includeInMenu: (context) => context.hasForks
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
name: "stats",
|
|
1670
|
+
description: "count files by category and workspace package",
|
|
1671
|
+
options: [
|
|
1672
|
+
{ flags: "-V, --verbose", description: "show detailed output during operations" },
|
|
1673
|
+
{ flags: "--coverage", description: "regenerate test coverage before showing the stats summary" }
|
|
1674
|
+
]
|
|
1591
1675
|
}
|
|
1676
|
+
];
|
|
1677
|
+
async function getMenuContext(userConfig, forkPath) {
|
|
1678
|
+
return {
|
|
1679
|
+
hasForks: (userConfig.forks?.length ?? 0) > 0,
|
|
1680
|
+
isUpstreamRepo: JSON.parse(readFileSync(join8(forkPath, "package.json"), "utf8")).name === "cella"
|
|
1681
|
+
};
|
|
1592
1682
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1683
|
+
function buildServiceChoices(context) {
|
|
1684
|
+
const label = (name) => name.padEnd(14);
|
|
1685
|
+
const baseChoices = serviceDefinitions.filter((service) => service.includeInMenu?.(context) ?? true).map((service) => ({
|
|
1686
|
+
value: service.name,
|
|
1687
|
+
name: `${label(service.name)}${colors_default.dim(service.menuDescription?.(context) ?? service.description)}`
|
|
1688
|
+
}));
|
|
1689
|
+
return [
|
|
1690
|
+
...baseChoices,
|
|
1691
|
+
{ type: "separator", separator: "\u2500".repeat(40) },
|
|
1692
|
+
{ value: "exit", name: colors_default.red(`${label("exit")}${colors_default.dim("quit without doing anything")}`) }
|
|
1693
|
+
];
|
|
1603
1694
|
}
|
|
1604
|
-
|
|
1605
|
-
const
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
const parsed = JSON.parse(raw);
|
|
1609
|
-
const commit = parsed?.upstream?.commit;
|
|
1610
|
-
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1611
|
-
return parsed;
|
|
1612
|
-
} catch {
|
|
1613
|
-
return null;
|
|
1695
|
+
function addServiceCommand(program, definition, setSelection) {
|
|
1696
|
+
const command = program.command(definition.name).description(definition.description);
|
|
1697
|
+
for (const option of definition.options ?? []) {
|
|
1698
|
+
command.option(option.flags, option.description);
|
|
1614
1699
|
}
|
|
1700
|
+
command.action((opts) => {
|
|
1701
|
+
setSelection({
|
|
1702
|
+
service: definition.name,
|
|
1703
|
+
options: readOptions(opts)
|
|
1704
|
+
});
|
|
1705
|
+
});
|
|
1615
1706
|
}
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1707
|
+
function buildProgram(setSelection) {
|
|
1708
|
+
const program = new Command(NAME).name("cella").version(VERSION, "-v, --version", "output the current version").usage("[service] [options]").helpOption("-h, --help", "display help for a command").showHelpAfterError().addHelpText(
|
|
1709
|
+
"after",
|
|
1710
|
+
[
|
|
1711
|
+
"",
|
|
1712
|
+
"Examples:",
|
|
1713
|
+
" $ cella analyze",
|
|
1714
|
+
" $ cella analyze --json --scope risk",
|
|
1715
|
+
" $ cella analyze --open-diff frontend/src/routes/index.tsx",
|
|
1716
|
+
" $ cella sync --hard",
|
|
1717
|
+
" $ cella sync --unpinned",
|
|
1718
|
+
" $ cella sync --track branch",
|
|
1719
|
+
" $ cella migrate --mark 20261002T0614-config-switch",
|
|
1720
|
+
" $ cella audit --check-overrides",
|
|
1721
|
+
" $ cella contributions --fork raak --json"
|
|
1722
|
+
].join("\n")
|
|
1723
|
+
);
|
|
1724
|
+
for (const definition of serviceDefinitions) {
|
|
1725
|
+
addServiceCommand(program, definition, setSelection);
|
|
1628
1726
|
}
|
|
1727
|
+
return program;
|
|
1629
1728
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
return true;
|
|
1634
|
-
} catch {
|
|
1635
|
-
return false;
|
|
1729
|
+
function parseCommandLine(argv) {
|
|
1730
|
+
if (argv.length <= 2) {
|
|
1731
|
+
return { options: readOptions({}) };
|
|
1636
1732
|
}
|
|
1733
|
+
let selection = { options: readOptions({}) };
|
|
1734
|
+
const program = buildProgram((nextSelection) => {
|
|
1735
|
+
selection = nextSelection;
|
|
1736
|
+
});
|
|
1737
|
+
program.parse(argv);
|
|
1738
|
+
return selection;
|
|
1637
1739
|
}
|
|
1638
|
-
async function
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
if (!root) return null;
|
|
1649
|
-
const rootDate = await git(["show", "-s", "--format=%cI", root], cwd, { ignoreErrors: true });
|
|
1650
|
-
if (!rootDate) return null;
|
|
1651
|
-
const before = new Date(new Date(rootDate).getTime() + 24 * 60 * 60 * 1e3).toISOString();
|
|
1652
|
-
const revList = await git(
|
|
1653
|
-
["rev-list", `--max-count=${SCAFFOLD_CANDIDATE_LIMIT}`, `--before=${before}`, upstreamRef],
|
|
1654
|
-
cwd,
|
|
1655
|
-
{ ignoreErrors: true }
|
|
1656
|
-
);
|
|
1657
|
-
const candidates = revList.split("\n").filter(Boolean);
|
|
1658
|
-
if (candidates.length === 0) return null;
|
|
1659
|
-
const totalPaths = (await git(["ls-tree", "-r", "--name-only", root], cwd)).split("\n").filter(Boolean).length;
|
|
1660
|
-
if (totalPaths === 0) return null;
|
|
1661
|
-
let best = null;
|
|
1662
|
-
outer: for (let i = 0; i < candidates.length; i += SCAFFOLD_SCORE_CONCURRENCY) {
|
|
1663
|
-
const chunk = candidates.slice(i, i + SCAFFOLD_SCORE_CONCURRENCY);
|
|
1664
|
-
const scores = await Promise.all(
|
|
1665
|
-
chunk.map(async (sha) => {
|
|
1666
|
-
try {
|
|
1667
|
-
const out = await git(["diff-tree", "-r", "--name-only", sha, root], cwd);
|
|
1668
|
-
return { sha, diff: out ? out.split("\n").length : 0 };
|
|
1669
|
-
} catch {
|
|
1670
|
-
return { sha, diff: Number.POSITIVE_INFINITY };
|
|
1671
|
-
}
|
|
1672
|
-
})
|
|
1673
|
-
);
|
|
1674
|
-
for (const score of scores) {
|
|
1675
|
-
if (!best || score.diff < best.diff) best = score;
|
|
1676
|
-
if (best.diff === 0) break outer;
|
|
1677
|
-
}
|
|
1740
|
+
async function promptForService(userConfig, forkPath) {
|
|
1741
|
+
const selected = await select({
|
|
1742
|
+
message: "choose a service:",
|
|
1743
|
+
choices: buildServiceChoices(await getMenuContext(userConfig, forkPath)),
|
|
1744
|
+
loop: false
|
|
1745
|
+
});
|
|
1746
|
+
if (selected === "exit") {
|
|
1747
|
+
console.info(colors_default.dim("exiting..."));
|
|
1748
|
+
console.info();
|
|
1749
|
+
process5.exit(0);
|
|
1678
1750
|
}
|
|
1679
|
-
|
|
1680
|
-
return
|
|
1681
|
-
}
|
|
1682
|
-
async function readRootTrailerBase(cwd, headRef) {
|
|
1683
|
-
const root = await getRootCommit(cwd, headRef);
|
|
1684
|
-
if (!root) return null;
|
|
1685
|
-
const message = await git(["show", "-s", "--format=%B", root], cwd, { ignoreErrors: true });
|
|
1686
|
-
const match = message.match(/^cella-base:\s*([0-9a-f]{40})$/im);
|
|
1687
|
-
return match ? match[1].toLowerCase() : null;
|
|
1688
|
-
}
|
|
1689
|
-
async function isShallowRepository(cwd) {
|
|
1690
|
-
const out = await git(["rev-parse", "--is-shallow-repository"], cwd, { ignoreErrors: true });
|
|
1691
|
-
return out === "true";
|
|
1751
|
+
console.info();
|
|
1752
|
+
return selected;
|
|
1692
1753
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
Sync needs the complete history to find the common ancestor with upstream. Restore it with:
|
|
1703
|
-
|
|
1704
|
-
git fetch --unshallow
|
|
1705
|
-
|
|
1706
|
-
then re-run sync.${detail}`
|
|
1707
|
-
);
|
|
1708
|
-
}
|
|
1754
|
+
function buildRuntimeConfig(userConfig, forkPath, selection) {
|
|
1755
|
+
const { branchRef } = resolveUpstream(userConfig.settings);
|
|
1756
|
+
return {
|
|
1757
|
+
...userConfig,
|
|
1758
|
+
forkPath,
|
|
1759
|
+
upstreamRef: branchRef,
|
|
1760
|
+
service: selection.service ?? "analyze",
|
|
1761
|
+
...selection.options
|
|
1762
|
+
};
|
|
1709
1763
|
}
|
|
1710
|
-
async function
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
const
|
|
1715
|
-
if (
|
|
1716
|
-
|
|
1717
|
-
()
|
|
1718
|
-
() => readManifestBaseAtRef(cwd, headRef),
|
|
1719
|
-
() => readManifestBase(cwd),
|
|
1720
|
-
() => readRootTrailerBase(cwd, headRef),
|
|
1721
|
-
() => inferScaffoldBase(cwd, headRef, upstreamRef)
|
|
1722
|
-
];
|
|
1723
|
-
let baseSha = null;
|
|
1724
|
-
for (const source of sources) {
|
|
1725
|
-
const sha = await source();
|
|
1726
|
-
if (sha && await commitObjectExists(cwd, sha)) {
|
|
1727
|
-
baseSha = sha;
|
|
1728
|
-
break;
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
if (!baseSha) {
|
|
1732
|
-
throw new Error(
|
|
1733
|
-
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1734
|
-
|
|
1735
|
-
This fork has unrelated history with upstream \u2014 typical for a create-cella scaffold, or after
|
|
1736
|
-
upstream squashed its history \u2014 and no recorded or inferable base commit exists after fetching
|
|
1737
|
-
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1738
|
-
|
|
1739
|
-
git update-ref refs/cella/last-sync <upstream-sha>
|
|
1740
|
-
`
|
|
1741
|
-
);
|
|
1764
|
+
async function parseCli(userConfig, forkPath) {
|
|
1765
|
+
const selection = parseCommandLine(process5.argv);
|
|
1766
|
+
if (selection.options.json || selection.options.diff) setJsonMode(true);
|
|
1767
|
+
printHeader();
|
|
1768
|
+
const warnings = validateOverrides(userConfig, forkPath);
|
|
1769
|
+
if (warnings.length > 0) {
|
|
1770
|
+
printWarnings(warnings);
|
|
1771
|
+
console.info();
|
|
1742
1772
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1773
|
+
if (!selection.service) {
|
|
1774
|
+
selection.service = await promptForService(userConfig, forkPath);
|
|
1775
|
+
}
|
|
1776
|
+
return buildRuntimeConfig(userConfig, forkPath, selection);
|
|
1747
1777
|
}
|
|
1748
|
-
|
|
1778
|
+
|
|
1779
|
+
// src/services/analyze.ts
|
|
1780
|
+
import { basename as basename4 } from "path";
|
|
1781
|
+
|
|
1782
|
+
// src/utils/diff.ts
|
|
1783
|
+
import { execFileSync } from "child_process";
|
|
1784
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1785
|
+
import { tmpdir } from "os";
|
|
1786
|
+
import { basename as basename2, join as join9 } from "path";
|
|
1787
|
+
import process6 from "process";
|
|
1788
|
+
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
1789
|
+
const args = ["diff", "--no-color"];
|
|
1790
|
+
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
1791
|
+
args.push(range, "--", filePath);
|
|
1749
1792
|
try {
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1793
|
+
return execFileSync("git", args, { cwd, maxBuffer: 50 * 1024 * 1024 });
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
const stderr = error.stderr?.toString().trim();
|
|
1796
|
+
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
1754
1797
|
}
|
|
1755
1798
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
if (!storedRef) return null;
|
|
1759
|
-
const storedHead = await getStoredSyncHead(cwd);
|
|
1760
|
-
if (storedHead) {
|
|
1761
|
-
const currentHead = await git(["rev-parse", headRef], cwd, { ignoreErrors: true });
|
|
1762
|
-
if (currentHead && currentHead === storedHead) return null;
|
|
1763
|
-
}
|
|
1764
|
-
return storedRef;
|
|
1799
|
+
function escapeHtml(text) {
|
|
1800
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1765
1801
|
}
|
|
1766
|
-
async function
|
|
1767
|
-
const
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1802
|
+
async function renderDiffPage(patch, meta) {
|
|
1803
|
+
const { preloadPatchDiff } = await import("@pierre/diffs/ssr");
|
|
1804
|
+
const { prerenderedHTML } = await preloadPatchDiff({
|
|
1805
|
+
patch,
|
|
1806
|
+
options: {
|
|
1807
|
+
diffStyle: "split",
|
|
1808
|
+
themeType: "system",
|
|
1809
|
+
lineDiffType: "word-alt",
|
|
1810
|
+
hunkSeparators: "line-info",
|
|
1811
|
+
overflow: "wrap",
|
|
1812
|
+
disableFileHeader: true
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
const title = `${escapeHtml(meta.filePath)} \xB7 ${escapeHtml(meta.srcLabel)} vs ${escapeHtml(meta.dstLabel)}`;
|
|
1816
|
+
const note = meta.note ? `<span class="note">\xB7 ${escapeHtml(meta.note)}</span>` : "";
|
|
1817
|
+
return `<!doctype html>
|
|
1818
|
+
<html lang="en">
|
|
1819
|
+
<head>
|
|
1820
|
+
<meta charset="utf-8">
|
|
1821
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1822
|
+
<title>${title}</title>
|
|
1823
|
+
<style>
|
|
1824
|
+
:root { color-scheme: light dark; }
|
|
1825
|
+
* { box-sizing: border-box; }
|
|
1826
|
+
body {
|
|
1827
|
+
margin: 0;
|
|
1828
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
1829
|
+
background: light-dark(#fafafa, #111);
|
|
1830
|
+
color: light-dark(#111, #eee);
|
|
1771
1831
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1832
|
+
header {
|
|
1833
|
+
display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap;
|
|
1834
|
+
padding: 0.9rem 1.25rem;
|
|
1835
|
+
border-bottom: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
1836
|
+
position: sticky; top: 0;
|
|
1837
|
+
background: inherit;
|
|
1775
1838
|
}
|
|
1776
|
-
|
|
1839
|
+
header .brand { font-weight: 600; color: light-dark(#0891b2, #22d3ee); }
|
|
1840
|
+
header code { font-family: ui-monospace, monospace; font-size: 0.9rem; }
|
|
1841
|
+
header .sides { margin-left: auto; font-size: 0.8rem; opacity: 0.7; }
|
|
1842
|
+
main { max-width: 1400px; margin: 1rem auto; padding: 0 1rem 2rem; }
|
|
1843
|
+
.diff-shell {
|
|
1844
|
+
border: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
1845
|
+
border-radius: 8px; overflow: hidden;
|
|
1846
|
+
background: light-dark(#fff, #161616);
|
|
1847
|
+
}
|
|
1848
|
+
</style>
|
|
1849
|
+
</head>
|
|
1850
|
+
<body>
|
|
1851
|
+
<header>
|
|
1852
|
+
<span class="brand">\u29C8 cella diff</span>
|
|
1853
|
+
<code>${escapeHtml(meta.filePath)}</code>
|
|
1854
|
+
<span class="sides">${escapeHtml(meta.srcLabel)} \u2190 \u2192 ${escapeHtml(meta.dstLabel)} ${note}</span>
|
|
1855
|
+
</header>
|
|
1856
|
+
<main>
|
|
1857
|
+
<!-- Declarative shadow DOM: the library's CSS targets :host, which only
|
|
1858
|
+
matches inside a shadow root (its browser component renders into one). -->
|
|
1859
|
+
<div class="diff-shell">
|
|
1860
|
+
<template shadowrootmode="open">
|
|
1861
|
+
${prerenderedHTML}
|
|
1862
|
+
</template>
|
|
1863
|
+
</div>
|
|
1864
|
+
</main>
|
|
1865
|
+
</body>
|
|
1866
|
+
</html>`;
|
|
1777
1867
|
}
|
|
1778
|
-
|
|
1779
|
-
const headSha = await git(["rev-parse", headRef], cwd, { ignoreErrors: true });
|
|
1780
|
-
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
1781
|
-
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
1782
|
-
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
1783
|
-
if (existingReplacement) return fn();
|
|
1784
|
-
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
1785
|
-
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
1786
|
-
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
1868
|
+
function openWithDefaultApp(target) {
|
|
1787
1869
|
try {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1870
|
+
if (process6.platform === "win32") {
|
|
1871
|
+
execFileSync("rundll32", ["url.dll,FileProtocolHandler", target], { stdio: "ignore" });
|
|
1872
|
+
} else {
|
|
1873
|
+
execFileSync(process6.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
1874
|
+
}
|
|
1875
|
+
} catch {
|
|
1791
1876
|
}
|
|
1792
1877
|
}
|
|
1878
|
+
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
1879
|
+
const html = await renderDiffPage(patch, meta);
|
|
1880
|
+
const dir = join9(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1881
|
+
mkdirSync(dir, { recursive: true });
|
|
1882
|
+
const outPath = join9(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
1883
|
+
writeFileSync2(outPath, html);
|
|
1884
|
+
openWithDefaultApp(outPath);
|
|
1885
|
+
return outPath;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// src/services/merge-engine.ts
|
|
1889
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1890
|
+
import { rm as rm2 } from "fs/promises";
|
|
1891
|
+
import { join as join11 } from "path";
|
|
1793
1892
|
|
|
1794
1893
|
// src/utils/cleanup.ts
|
|
1894
|
+
import { createHash } from "crypto";
|
|
1895
|
+
import { existsSync as existsSync6, rmSync } from "fs";
|
|
1896
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1897
|
+
import { basename as basename3, join as join10, resolve as resolve3 } from "path";
|
|
1898
|
+
import process7 from "process";
|
|
1795
1899
|
var WORKTREE_PREFIXES = {
|
|
1796
1900
|
sync: "cella-sync-",
|
|
1797
1901
|
view: "cella-view-"
|
|
1798
1902
|
};
|
|
1799
1903
|
function buildWorktreePath(kind, repoPath) {
|
|
1800
|
-
|
|
1904
|
+
const key = createHash("sha256").update(resolve3(repoPath)).digest("hex").slice(0, 8);
|
|
1905
|
+
return join10(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
|
|
1906
|
+
}
|
|
1907
|
+
function legacyWorktreePath(kind, repoPath) {
|
|
1908
|
+
return join10(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
1801
1909
|
}
|
|
1802
1910
|
function getWorktreePath(repoPath) {
|
|
1803
1911
|
return buildWorktreePath("sync", repoPath);
|
|
@@ -1815,22 +1923,21 @@ function unregisterWorktree() {
|
|
|
1815
1923
|
}
|
|
1816
1924
|
async function cleanupWorktree(repoPath, worktreePath) {
|
|
1817
1925
|
await removeWorktree(repoPath, worktreePath);
|
|
1818
|
-
if (
|
|
1926
|
+
if (existsSync6(worktreePath)) {
|
|
1819
1927
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
1820
1928
|
}
|
|
1821
1929
|
unregisterWorktree();
|
|
1822
1930
|
}
|
|
1823
1931
|
async function cleanupLeftoverWorktrees(repoPath) {
|
|
1824
1932
|
for (const kind of Object.keys(WORKTREE_PREFIXES)) {
|
|
1825
|
-
const worktreePath
|
|
1826
|
-
|
|
1827
|
-
await cleanupWorktree(repoPath, worktreePath);
|
|
1933
|
+
for (const worktreePath of [buildWorktreePath(kind, repoPath), legacyWorktreePath(kind, repoPath)]) {
|
|
1934
|
+
if (existsSync6(worktreePath)) await cleanupWorktree(repoPath, worktreePath);
|
|
1828
1935
|
}
|
|
1829
1936
|
}
|
|
1830
1937
|
const prefixes = Object.values(WORKTREE_PREFIXES);
|
|
1831
1938
|
const worktrees = await listWorktrees(repoPath);
|
|
1832
1939
|
for (const wt of worktrees) {
|
|
1833
|
-
if (prefixes.some((prefix) => wt.includes(prefix)) && !
|
|
1940
|
+
if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync6(wt)) {
|
|
1834
1941
|
await removeWorktree(repoPath, wt);
|
|
1835
1942
|
}
|
|
1836
1943
|
}
|
|
@@ -1861,6 +1968,51 @@ function registerSignalHandlers() {
|
|
|
1861
1968
|
cleanupRegistered = true;
|
|
1862
1969
|
}
|
|
1863
1970
|
|
|
1971
|
+
// src/utils/versions.ts
|
|
1972
|
+
function parseComparableVersion(version) {
|
|
1973
|
+
const trimmed = version.trim();
|
|
1974
|
+
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("||")) {
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
const match = trimmed.match(
|
|
1978
|
+
/^(?:\^|~|>=|<=|>|<|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
1979
|
+
);
|
|
1980
|
+
if (!match) return null;
|
|
1981
|
+
return {
|
|
1982
|
+
major: Number(match[1]),
|
|
1983
|
+
minor: Number(match[2] ?? 0),
|
|
1984
|
+
patch: Number(match[3] ?? 0)
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function compareVersions(left, right) {
|
|
1988
|
+
if (left.major !== right.major) return left.major - right.major;
|
|
1989
|
+
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
1990
|
+
return left.patch - right.patch;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/utils/cli-version.ts
|
|
1994
|
+
var CLI_PACKAGE = "@cellajs/cli";
|
|
1995
|
+
async function readUpstreamCliRange(repoPath, ref) {
|
|
1996
|
+
const source = await git(["show", `${ref}:package.json`], repoPath, { ignoreErrors: true });
|
|
1997
|
+
if (!source) return null;
|
|
1998
|
+
try {
|
|
1999
|
+
const pkg = JSON.parse(source);
|
|
2000
|
+
return pkg.devDependencies?.[CLI_PACKAGE] ?? pkg.dependencies?.[CLI_PACKAGE] ?? null;
|
|
2001
|
+
} catch {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function cliVersionMismatch(running, range) {
|
|
2006
|
+
if (/^\s*(<|>(?!=))/.test(range)) return null;
|
|
2007
|
+
const required = parseComparableVersion(range);
|
|
2008
|
+
const current = parseComparableVersion(running);
|
|
2009
|
+
if (!required || !current || compareVersions(current, required) >= 0) return null;
|
|
2010
|
+
return [
|
|
2011
|
+
`upstream needs ${CLI_PACKAGE} ${range}, this is ${running}: an older CLI misses fixes to the sync itself.`,
|
|
2012
|
+
` update it, commit, then rerun: pnpm add -D -w '${CLI_PACKAGE}@${range}'`
|
|
2013
|
+
].join("\n");
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1864
2016
|
// src/utils/upstream-overrides.ts
|
|
1865
2017
|
import { Node, Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1866
2018
|
var LIST_KEYS = ["pinned", "ignored"];
|
|
@@ -2391,6 +2543,9 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2391
2543
|
}
|
|
2392
2544
|
config.upstreamRef = upstreamRef;
|
|
2393
2545
|
onStep?.("remote configured", `${releaseTag ?? upstreamRef} \u2192 ${config.settings.upstreamUrl}`);
|
|
2546
|
+
const cliRange = await readUpstreamCliRange(forkPath, upstreamRef);
|
|
2547
|
+
const cliMismatch = cliRange ? cliVersionMismatch(VERSION, cliRange) : null;
|
|
2548
|
+
if (cliMismatch) throw new Error(cliMismatch);
|
|
2394
2549
|
onProgress?.("checking sync base...");
|
|
2395
2550
|
await ensureSyncBase(forkPath, "HEAD", upstreamRef);
|
|
2396
2551
|
const upstreamGitHubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
@@ -2436,9 +2591,34 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2436
2591
|
return {};
|
|
2437
2592
|
}
|
|
2438
2593
|
}
|
|
2594
|
+
async function readNotesInRange(config, ctx) {
|
|
2595
|
+
const { forkPath } = config;
|
|
2596
|
+
try {
|
|
2597
|
+
const aggressive = config.hard === true || config.unpinned === true;
|
|
2598
|
+
const syncBase = aggressive ? (await getEffectiveMergeBase(forkPath, "HEAD", ctx.upstreamRef)).base : ctx.mergeBase;
|
|
2599
|
+
const [total, arrived, pending] = await Promise.all([
|
|
2600
|
+
listNoteIds(forkPath, ctx.upstreamRef),
|
|
2601
|
+
arrivedNoteIds(forkPath, syncBase, ctx.upstreamRef),
|
|
2602
|
+
readPending(forkPath, syncBase)
|
|
2603
|
+
]);
|
|
2604
|
+
return { total: total.length, arrived, pending: [.../* @__PURE__ */ new Set([...pending, ...arrived])] };
|
|
2605
|
+
} catch {
|
|
2606
|
+
return void 0;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
function notesResult(notes) {
|
|
2610
|
+
return notes ? { migrationNotes: { total: notes.total, arrived: notes.arrived, open: notes.pending.length } } : {};
|
|
2611
|
+
}
|
|
2612
|
+
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2613
|
+
if (!existsSync7(join11(forkPath, NOTES_DIR))) return;
|
|
2614
|
+
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", NOTES_DIR], forkPath);
|
|
2615
|
+
await rm2(join11(forkPath, NOTES_DIR), { recursive: true, force: true });
|
|
2616
|
+
onStep?.("migration notes", `removed ${NOTES_DIR}/: notes stay upstream, list them with pnpm cella migrate`);
|
|
2617
|
+
}
|
|
2439
2618
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2440
2619
|
const { forkPath } = config;
|
|
2441
2620
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2621
|
+
const notes = await readNotesInRange(config, ctx);
|
|
2442
2622
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2443
2623
|
forkPath,
|
|
2444
2624
|
upstreamRef,
|
|
@@ -2465,6 +2645,11 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2465
2645
|
await storeLastSyncRef(forkPath, upstreamCommit.hash);
|
|
2466
2646
|
await writeSyncManifest(forkPath, syncManifest);
|
|
2467
2647
|
await stagePath(forkPath, MANIFEST_FILE);
|
|
2648
|
+
if (notes) {
|
|
2649
|
+
await writePending(forkPath, notes.pending);
|
|
2650
|
+
await stagePath(forkPath, PENDING_FILE);
|
|
2651
|
+
}
|
|
2652
|
+
await removeUpstreamOnlyCopy(forkPath, onStep);
|
|
2468
2653
|
};
|
|
2469
2654
|
if (remainingConflicts.length > 0) {
|
|
2470
2655
|
await recordSyncPoint();
|
|
@@ -2488,12 +2673,14 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2488
2673
|
autoMergedFiles,
|
|
2489
2674
|
protectedConflicts,
|
|
2490
2675
|
...resultMeta(config, ctx),
|
|
2491
|
-
...await upstreamOnlyReports(config, ctx, analyzedFiles)
|
|
2676
|
+
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2677
|
+
...notesResult(notes)
|
|
2492
2678
|
};
|
|
2493
2679
|
}
|
|
2494
2680
|
async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
2495
2681
|
const { forkPath } = config;
|
|
2496
2682
|
const worktreePath = getWorktreePath(forkPath);
|
|
2683
|
+
const notes = await readNotesInRange(config, ctx);
|
|
2497
2684
|
onProgress?.("creating worktree in temp directory...");
|
|
2498
2685
|
await createWorktree(forkPath, worktreePath, "HEAD");
|
|
2499
2686
|
onStep?.("worktree created", worktreePath);
|
|
@@ -2527,7 +2714,8 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2527
2714
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2528
2715
|
protectedConflicts: analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path),
|
|
2529
2716
|
...resultMeta(config, ctx),
|
|
2530
|
-
...reports
|
|
2717
|
+
...reports,
|
|
2718
|
+
...notesResult(notes)
|
|
2531
2719
|
};
|
|
2532
2720
|
}
|
|
2533
2721
|
async function runMergeEngine(config, options) {
|
|
@@ -2545,6 +2733,151 @@ async function runMergeEngine(config, options) {
|
|
|
2545
2733
|
}
|
|
2546
2734
|
}
|
|
2547
2735
|
|
|
2736
|
+
// src/services/migrate.ts
|
|
2737
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2738
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2739
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
2740
|
+
var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
|
|
2741
|
+
async function resolveNotesRef(config) {
|
|
2742
|
+
const { forkPath } = config;
|
|
2743
|
+
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2744
|
+
if (!ref) return null;
|
|
2745
|
+
const present = async () => !!await git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], forkPath, { ignoreErrors: true });
|
|
2746
|
+
if (!await present()) {
|
|
2747
|
+
await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
|
|
2748
|
+
await fetch2(forkPath, DEFAULT_UPSTREAM_REMOTE);
|
|
2749
|
+
if (!await present())
|
|
2750
|
+
throw new Error(`upstream commit ${ref.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2751
|
+
}
|
|
2752
|
+
return ref;
|
|
2753
|
+
}
|
|
2754
|
+
async function readNotesStatus(config) {
|
|
2755
|
+
try {
|
|
2756
|
+
const ref = await resolveNotesRef(config);
|
|
2757
|
+
if (!ref) return null;
|
|
2758
|
+
const [ids, pending] = await Promise.all([listNoteIds(config.forkPath, ref), readPending(config.forkPath, ref)]);
|
|
2759
|
+
return ids.length > 0 || pending.length > 0 ? { total: ids.length, open: pending.length } : null;
|
|
2760
|
+
} catch {
|
|
2761
|
+
return null;
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
async function printMigrationNotesLine(config, result) {
|
|
2765
|
+
const notes = result?.migrationNotes;
|
|
2766
|
+
if (notes) {
|
|
2767
|
+
if (notes.total === 0) return;
|
|
2768
|
+
console.info();
|
|
2769
|
+
console.info(
|
|
2770
|
+
colors_default.dim(formatNotesLine(notes.total, notes.open, notes.arrived.length, { dryRun: config.service === "analyze" }))
|
|
2771
|
+
);
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
const status = await readNotesStatus(config);
|
|
2775
|
+
if (!status) return;
|
|
2776
|
+
console.info();
|
|
2777
|
+
console.info(colors_default.dim(formatNotesLine(status.total, status.open)));
|
|
2778
|
+
}
|
|
2779
|
+
function toJson(note, url, open) {
|
|
2780
|
+
return { ...note, kind: note.codemod ? "codemod" : "manual", open, url: url ?? null };
|
|
2781
|
+
}
|
|
2782
|
+
function printNote(index, note, url) {
|
|
2783
|
+
const tags = [
|
|
2784
|
+
note.codemod ? "codemod" : "manual",
|
|
2785
|
+
note.syncBreaking ? "sync-breaking" : null,
|
|
2786
|
+
note.clientCacheBump ? "cache-bump" : null
|
|
2787
|
+
].filter(Boolean).join(", ");
|
|
2788
|
+
console.info(`${index}. ${note.title} ${colors_default.dim(`[${tags}]`)}`);
|
|
2789
|
+
console.info(colors_default.dim(` id: ${note.id}`));
|
|
2790
|
+
if (note.summary) console.info(` ${note.summary}`);
|
|
2791
|
+
console.info(colors_default.dim(` read: pnpm cella migrate --show ${note.id}`));
|
|
2792
|
+
if (url) console.info(colors_default.dim(` ${url}`));
|
|
2793
|
+
if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --extract ${note.id}`));
|
|
2794
|
+
console.info();
|
|
2795
|
+
}
|
|
2796
|
+
async function extractNote(config, ref, id) {
|
|
2797
|
+
const { forkPath } = config;
|
|
2798
|
+
const note = await readNote(forkPath, ref, id);
|
|
2799
|
+
if (!note) throw new Error(`no migration note '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2800
|
+
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${NOTES_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2801
|
+
const target = join12(EXTRACT_DIR, id);
|
|
2802
|
+
for (const file of files) {
|
|
2803
|
+
const content = execFileSync2("git", ["show", `${ref}:${NOTES_DIR}/${id}/${file}`], {
|
|
2804
|
+
cwd: forkPath,
|
|
2805
|
+
maxBuffer: 50 * 1024 * 1024
|
|
2806
|
+
});
|
|
2807
|
+
const path4 = join12(forkPath, target, file);
|
|
2808
|
+
await mkdir4(dirname4(path4), { recursive: true });
|
|
2809
|
+
await writeFile3(path4, content);
|
|
2810
|
+
}
|
|
2811
|
+
console.info(`${colors_default.green("\u2713")} extracted ${files.length} file(s) to ${target}/`);
|
|
2812
|
+
console.info(colors_default.dim(` the README's ${NOTES_DIR}/${id}/ paths are this folder.`));
|
|
2813
|
+
if (note.codemod) console.info(colors_default.dim(` pnpm exec tsx ${target}/${note.codemod} inventory ${note.roots.join(" ")}`));
|
|
2814
|
+
}
|
|
2815
|
+
async function markNotes(config, ref, pending, ids) {
|
|
2816
|
+
const open = new Set(pending);
|
|
2817
|
+
const unknown = ids.filter((id) => !open.has(id));
|
|
2818
|
+
for (const id of ids) open.delete(id);
|
|
2819
|
+
await writePending(config.forkPath, [...open]);
|
|
2820
|
+
const marked = ids.length - unknown.length;
|
|
2821
|
+
if (marked > 0) console.info(`${colors_default.green("\u2713")} recorded ${marked} note(s) as handled`);
|
|
2822
|
+
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2823
|
+
console.info(colors_default.dim(formatNotesLine((await listNoteIds(config.forkPath, ref)).length, open.size)));
|
|
2824
|
+
}
|
|
2825
|
+
async function runMigrate(config) {
|
|
2826
|
+
const { forkPath } = config;
|
|
2827
|
+
const ref = await resolveNotesRef(config);
|
|
2828
|
+
if (!ref) {
|
|
2829
|
+
console.info(colors_default.dim("no sync point recorded yet: migration notes start with the first `pnpm cella sync`."));
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
const pending = await readPending(forkPath, ref);
|
|
2833
|
+
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2834
|
+
const urlOf = (id) => noteUrl(githubUrl, ref, id);
|
|
2835
|
+
if (config.mark?.length) return markNotes(config, ref, pending, config.mark);
|
|
2836
|
+
if (config.extract) return extractNote(config, ref, config.extract);
|
|
2837
|
+
if (config.show) {
|
|
2838
|
+
const source = await git(["show", `${ref}:${NOTES_DIR}/${config.show}/README.md`], forkPath, {
|
|
2839
|
+
ignoreErrors: true
|
|
2840
|
+
});
|
|
2841
|
+
if (!source) throw new Error(`no migration note '${config.show}' at upstream ${ref.slice(0, 7)}`);
|
|
2842
|
+
const url = urlOf(config.show);
|
|
2843
|
+
if (url) console.info(colors_default.dim(url));
|
|
2844
|
+
console.info(source);
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
const ids = config.all ? await listNoteIds(forkPath, ref) : pending;
|
|
2848
|
+
const notes = (await Promise.all(ids.map((id) => readNote(forkPath, ref, id)))).map(
|
|
2849
|
+
(note, i) => note ?? {
|
|
2850
|
+
id: ids[i],
|
|
2851
|
+
title: `${ids[i]} (no longer upstream)`,
|
|
2852
|
+
summary: "",
|
|
2853
|
+
syncBreaking: false,
|
|
2854
|
+
clientCacheBump: false,
|
|
2855
|
+
roots: [],
|
|
2856
|
+
codemod: null
|
|
2857
|
+
}
|
|
2858
|
+
);
|
|
2859
|
+
const total = (await listNoteIds(forkPath, ref)).length;
|
|
2860
|
+
if (config.json) {
|
|
2861
|
+
const open = new Set(pending);
|
|
2862
|
+
const out = {
|
|
2863
|
+
upstreamCommit: ref,
|
|
2864
|
+
total,
|
|
2865
|
+
open: pending.length,
|
|
2866
|
+
notes: notes.map((note) => toJson(note, urlOf(note.id), open.has(note.id)))
|
|
2867
|
+
};
|
|
2868
|
+
writeStdout(JSON.stringify(out, null, 2));
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
console.info(colors_default.dim(formatNotesLine(total, pending.length)));
|
|
2872
|
+
if (notes.length === 0) return;
|
|
2873
|
+
console.info();
|
|
2874
|
+
for (const [i, note] of notes.entries()) printNote(i + 1, note, urlOf(note.id));
|
|
2875
|
+
if (pending.length > 0) {
|
|
2876
|
+
console.info(colors_default.dim("work each note's README, check with `pnpm check`, then record it:"));
|
|
2877
|
+
console.info(colors_default.dim(` pnpm cella migrate --mark ${pending.join(" ")}`));
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2548
2881
|
// src/services/analyze.ts
|
|
2549
2882
|
var scopeStatuses = {
|
|
2550
2883
|
all: /* @__PURE__ */ new Set(["ahead", "drifted", "diverged"]),
|
|
@@ -2649,6 +2982,7 @@ async function runAnalyze(config) {
|
|
|
2649
2982
|
printIgnoredUpstreamChanges(result);
|
|
2650
2983
|
printUpstreamOverrideChanges(result);
|
|
2651
2984
|
printMaskingPinWarning(result.files);
|
|
2985
|
+
await printMigrationNotesLine(config, result);
|
|
2652
2986
|
if (config.logFile) {
|
|
2653
2987
|
const logPath = writeLogFile(config.forkPath, result.files);
|
|
2654
2988
|
console.info();
|
|
@@ -3427,7 +3761,7 @@ async function checkOverrides(forkPath) {
|
|
|
3427
3761
|
|
|
3428
3762
|
// src/services/contributions.ts
|
|
3429
3763
|
import { readFileSync as readFileSync4 } from "fs";
|
|
3430
|
-
import { join as
|
|
3764
|
+
import { join as join16 } from "path";
|
|
3431
3765
|
import {
|
|
3432
3766
|
createPrompt,
|
|
3433
3767
|
isDownKey,
|
|
@@ -3442,7 +3776,7 @@ import {
|
|
|
3442
3776
|
import { select as select2 } from "@inquirer/prompts";
|
|
3443
3777
|
|
|
3444
3778
|
// src/services/contrib-core.ts
|
|
3445
|
-
import { dirname as
|
|
3779
|
+
import { dirname as dirname5, join as join15, resolve as resolve4 } from "path";
|
|
3446
3780
|
async function listFilesAtRef(repoPath, ref) {
|
|
3447
3781
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
3448
3782
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -3458,10 +3792,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
3458
3792
|
const baseFiles = await listFilesAtRef(repoPath, baseRef);
|
|
3459
3793
|
const baseDirs = /* @__PURE__ */ new Set(["."]);
|
|
3460
3794
|
for (const f of baseFiles) {
|
|
3461
|
-
let dir =
|
|
3795
|
+
let dir = dirname5(f);
|
|
3462
3796
|
while (dir !== ".") {
|
|
3463
3797
|
baseDirs.add(dir);
|
|
3464
|
-
dir =
|
|
3798
|
+
dir = dirname5(dir);
|
|
3465
3799
|
}
|
|
3466
3800
|
}
|
|
3467
3801
|
const files = [];
|
|
@@ -3474,7 +3808,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
3474
3808
|
if (inCella && !inFork) {
|
|
3475
3809
|
kind = "deleted";
|
|
3476
3810
|
} else if (!inCella && inFork) {
|
|
3477
|
-
if (!baseDirs.has(
|
|
3811
|
+
if (!baseDirs.has(dirname5(file.path))) continue;
|
|
3478
3812
|
kind = "created";
|
|
3479
3813
|
} else {
|
|
3480
3814
|
kind = "modified";
|
|
@@ -3505,8 +3839,8 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
3505
3839
|
}
|
|
3506
3840
|
async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
|
|
3507
3841
|
const branch = `contrib/${forkName}`;
|
|
3508
|
-
const gitDir =
|
|
3509
|
-
const tmpIndex =
|
|
3842
|
+
const gitDir = resolve4(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
3843
|
+
const tmpIndex = join15(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
3510
3844
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
3511
3845
|
try {
|
|
3512
3846
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -3549,20 +3883,20 @@ ${commitBody}`;
|
|
|
3549
3883
|
}
|
|
3550
3884
|
|
|
3551
3885
|
// src/services/fork-utils.ts
|
|
3552
|
-
import { existsSync as
|
|
3553
|
-
import { dirname as
|
|
3886
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3887
|
+
import { dirname as dirname6, resolve as resolve5 } from "path";
|
|
3554
3888
|
async function resolveForkBasePath(repoPath) {
|
|
3555
3889
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
3556
|
-
return commonDir ?
|
|
3890
|
+
return commonDir ? dirname6(resolve5(repoPath, commonDir)) : repoPath;
|
|
3557
3891
|
}
|
|
3558
3892
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
3559
|
-
const resolvedPath =
|
|
3560
|
-
if (!resolvedPath.startsWith(
|
|
3893
|
+
const resolvedPath = resolve5(basePath, fork.localPath);
|
|
3894
|
+
if (!resolvedPath.startsWith(resolve5(basePath, ".."))) {
|
|
3561
3895
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
3562
3896
|
}
|
|
3563
|
-
if (!
|
|
3564
|
-
if (!
|
|
3565
|
-
if (requireConfig && !
|
|
3897
|
+
if (!existsSync9(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
3898
|
+
if (!existsSync9(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
3899
|
+
if (requireConfig && !existsSync9(`${resolvedPath}/${CONFIG_FILE}`)) {
|
|
3566
3900
|
return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
|
|
3567
3901
|
}
|
|
3568
3902
|
return { fork, resolvedPath, valid: true };
|
|
@@ -3580,7 +3914,7 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
|
3580
3914
|
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
3581
3915
|
}
|
|
3582
3916
|
function shouldApplyContributionsUnstaged(repoPath) {
|
|
3583
|
-
return JSON.parse(readFileSync4(
|
|
3917
|
+
return JSON.parse(readFileSync4(join16(repoPath, "package.json"), "utf8")).name === "cella";
|
|
3584
3918
|
}
|
|
3585
3919
|
var contribPrompt = createPrompt((config, done) => {
|
|
3586
3920
|
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
@@ -3925,7 +4259,7 @@ async function runContributions(config) {
|
|
|
3925
4259
|
}
|
|
3926
4260
|
|
|
3927
4261
|
// src/services/forks.ts
|
|
3928
|
-
import { resolve as
|
|
4262
|
+
import { resolve as resolve6 } from "path";
|
|
3929
4263
|
import process8 from "process";
|
|
3930
4264
|
import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
|
|
3931
4265
|
|
|
@@ -3968,7 +4302,6 @@ function listOpenSyncPrs(cwd, prefix) {
|
|
|
3968
4302
|
}
|
|
3969
4303
|
function buildMergeArgs(ref, options = {}) {
|
|
3970
4304
|
const args = ["pr", "merge", ref, "--squash"];
|
|
3971
|
-
if (options.auto) args.push("--auto");
|
|
3972
4305
|
if (options.deleteBranch) args.push("--delete-branch");
|
|
3973
4306
|
return args;
|
|
3974
4307
|
}
|
|
@@ -3980,12 +4313,12 @@ function closePr(cwd, ref) {
|
|
|
3980
4313
|
}
|
|
3981
4314
|
|
|
3982
4315
|
// src/services/packages.ts
|
|
3983
|
-
import { existsSync as
|
|
3984
|
-
import { join as
|
|
4316
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4317
|
+
import { join as join18 } from "path";
|
|
3985
4318
|
|
|
3986
4319
|
// src/services/package-usage.ts
|
|
3987
|
-
import { existsSync as
|
|
3988
|
-
import { join as
|
|
4320
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, statSync } from "fs";
|
|
4321
|
+
import { join as join17 } from "path";
|
|
3989
4322
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
3990
4323
|
function escapeRegExp(value) {
|
|
3991
4324
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4001,7 +4334,7 @@ function readSearchable(filePath) {
|
|
|
4001
4334
|
}
|
|
4002
4335
|
function fileSource(forkPath, path4) {
|
|
4003
4336
|
let text;
|
|
4004
|
-
return { path: path4, label: path4, text: () => text ??= readSearchable(
|
|
4337
|
+
return { path: path4, label: path4, text: () => text ??= readSearchable(join17(forkPath, path4)) };
|
|
4005
4338
|
}
|
|
4006
4339
|
async function readScriptsAtRef(forkPath, ref, path4) {
|
|
4007
4340
|
try {
|
|
@@ -4013,7 +4346,7 @@ async function readScriptsAtRef(forkPath, ref, path4) {
|
|
|
4013
4346
|
async function scriptSources(forkPath, upstreamRef, baseRef, path4) {
|
|
4014
4347
|
let forkScripts;
|
|
4015
4348
|
try {
|
|
4016
|
-
forkScripts = JSON.parse(readFileSync5(
|
|
4349
|
+
forkScripts = JSON.parse(readFileSync5(join17(forkPath, path4), "utf-8")).scripts ?? {};
|
|
4017
4350
|
} catch {
|
|
4018
4351
|
return [];
|
|
4019
4352
|
}
|
|
@@ -4023,9 +4356,9 @@ async function scriptSources(forkPath, upstreamRef, baseRef, path4) {
|
|
|
4023
4356
|
}
|
|
4024
4357
|
function binNames(forkPath, location, name) {
|
|
4025
4358
|
const bareName = name.split("/").pop();
|
|
4026
|
-
for (const dir of [
|
|
4027
|
-
const manifest =
|
|
4028
|
-
if (!
|
|
4359
|
+
for (const dir of [join17(forkPath, location), forkPath]) {
|
|
4360
|
+
const manifest = join17(dir, "node_modules", name, "package.json");
|
|
4361
|
+
if (!existsSync10(manifest)) continue;
|
|
4029
4362
|
try {
|
|
4030
4363
|
const { bin } = JSON.parse(readFileSync5(manifest, "utf-8"));
|
|
4031
4364
|
if (typeof bin === "string") return [bareName];
|
|
@@ -4067,26 +4400,6 @@ async function loadForkUsage(forkPath, upstreamRef, baseRef) {
|
|
|
4067
4400
|
}
|
|
4068
4401
|
|
|
4069
4402
|
// src/services/packages.ts
|
|
4070
|
-
function parseComparableVersion(version) {
|
|
4071
|
-
const trimmed = version.trim();
|
|
4072
|
-
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("||")) {
|
|
4073
|
-
return null;
|
|
4074
|
-
}
|
|
4075
|
-
const match = trimmed.match(
|
|
4076
|
-
/^(?:\^|~|>=|<=|>|<|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
4077
|
-
);
|
|
4078
|
-
if (!match) return null;
|
|
4079
|
-
return {
|
|
4080
|
-
major: Number(match[1]),
|
|
4081
|
-
minor: Number(match[2] ?? 0),
|
|
4082
|
-
patch: Number(match[3] ?? 0)
|
|
4083
|
-
};
|
|
4084
|
-
}
|
|
4085
|
-
function compareVersions(left, right) {
|
|
4086
|
-
if (left.major !== right.major) return left.major - right.major;
|
|
4087
|
-
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
4088
|
-
return left.patch - right.patch;
|
|
4089
|
-
}
|
|
4090
4403
|
function isHigherVersion(upstreamVersion, forkVersion) {
|
|
4091
4404
|
if (upstreamVersion === forkVersion) return false;
|
|
4092
4405
|
const upCoerced = parseComparableVersion(upstreamVersion);
|
|
@@ -4205,7 +4518,7 @@ function safeMergePnpm(forkPnpm, upstreamPnpm, basePnpm) {
|
|
|
4205
4518
|
return { merged, changed };
|
|
4206
4519
|
}
|
|
4207
4520
|
function readPackageJson(filePath) {
|
|
4208
|
-
if (!
|
|
4521
|
+
if (!existsSync11(filePath)) return null;
|
|
4209
4522
|
try {
|
|
4210
4523
|
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
4211
4524
|
} catch {
|
|
@@ -4243,13 +4556,13 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4243
4556
|
const changes = [];
|
|
4244
4557
|
const kept = [];
|
|
4245
4558
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4246
|
-
const pkgPath =
|
|
4559
|
+
const pkgPath = join18(forkPath, pkgRelPath);
|
|
4247
4560
|
const forkPkg = readPackageJson(pkgPath);
|
|
4248
4561
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4249
4562
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4250
4563
|
if (!upstream) return { updated: false, changes, kept };
|
|
4251
4564
|
if (!forkPkg) {
|
|
4252
|
-
const workspaceArrived = relativePath !== "" &&
|
|
4565
|
+
const workspaceArrived = relativePath !== "" && existsSync11(join18(forkPath, relativePath));
|
|
4253
4566
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4254
4567
|
writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4255
4568
|
`, "utf-8");
|
|
@@ -4427,6 +4740,7 @@ async function runSync(config, options) {
|
|
|
4427
4740
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
|
|
4428
4741
|
printSyncComplete(result, { stagedBranch });
|
|
4429
4742
|
printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
|
|
4743
|
+
await printMigrationNotesLine(config, result);
|
|
4430
4744
|
return result;
|
|
4431
4745
|
}
|
|
4432
4746
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
@@ -4443,7 +4757,7 @@ function qualifyPrRefs(subject, repoSlug) {
|
|
|
4443
4757
|
return subject.replace(/(^|[^\w/])#(\d+)\b/g, `$1${repoSlug}#$2`);
|
|
4444
4758
|
}
|
|
4445
4759
|
function buildSyncPrBody(input) {
|
|
4446
|
-
const { repoSlug, version, fromSha, toSha, commits, totalCount } = input;
|
|
4760
|
+
const { repoSlug, version, fromSha, toSha, commits, totalCount, notes } = input;
|
|
4447
4761
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
4448
4762
|
const short = (sha) => sha.slice(0, 7);
|
|
4449
4763
|
const commitRef = (sha) => githubUrl ? `[\`${short(sha)}\`](${githubUrl}/commit/${sha})` : `\`${short(sha)}\``;
|
|
@@ -4458,6 +4772,19 @@ function buildSyncPrBody(input) {
|
|
|
4458
4772
|
lines.push(`- ${commitRef(commit.hash)} ${qualifyPrRefs(commit.message, repoSlug)}`);
|
|
4459
4773
|
}
|
|
4460
4774
|
}
|
|
4775
|
+
if (notes && notes.total > 0) {
|
|
4776
|
+
const { total, open } = notes;
|
|
4777
|
+
if (open.length === 0) lines.push("", `**Migration notes**: all ${total} handled.`);
|
|
4778
|
+
else {
|
|
4779
|
+
lines.push(
|
|
4780
|
+
"",
|
|
4781
|
+
`**Migration notes**: ${Math.max(0, total - open.length)} of ${total} handled, open (\`pnpm cella migrate\`):`,
|
|
4782
|
+
""
|
|
4783
|
+
);
|
|
4784
|
+
for (const note of open)
|
|
4785
|
+
lines.push(`- ${note.url ? `[${note.title}](${note.url})` : note.title} (\`${note.id}\`)`);
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4461
4788
|
return `${lines.join("\n")}
|
|
4462
4789
|
`;
|
|
4463
4790
|
}
|
|
@@ -4473,7 +4800,20 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
4473
4800
|
skip: totalCount > PR_BODY_COMMIT_MAX ? totalCount - PR_BODY_COMMIT_MAX : 0,
|
|
4474
4801
|
limit: PR_BODY_COMMIT_MAX
|
|
4475
4802
|
}) : [];
|
|
4476
|
-
|
|
4803
|
+
const notes = await readPrBodyNotes(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
4804
|
+
return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount, notes });
|
|
4805
|
+
}
|
|
4806
|
+
async function readPrBodyNotes(forkPath, toSha, repoSlug) {
|
|
4807
|
+
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
4808
|
+
const [ids, pending] = await Promise.all([listNoteIds(forkPath, toSha), readPending(forkPath, toSha)]);
|
|
4809
|
+
const open = await Promise.all(
|
|
4810
|
+
pending.map(async (id) => ({
|
|
4811
|
+
id,
|
|
4812
|
+
title: (await readNote(forkPath, toSha, id))?.title ?? id,
|
|
4813
|
+
url: noteUrl(githubUrl, toSha, id)
|
|
4814
|
+
}))
|
|
4815
|
+
);
|
|
4816
|
+
return { total: ids.length, open };
|
|
4477
4817
|
}
|
|
4478
4818
|
function printPrCreateStep(branch, base, title = SYNC_PR_TITLE) {
|
|
4479
4819
|
console.info(colors_default.dim(` gh pr create --base ${base} --head ${branch} --title "${title}" --fill`));
|
|
@@ -4507,22 +4847,12 @@ async function flattenSyncBranch(forkPath, branch, base) {
|
|
|
4507
4847
|
function indentLines(text) {
|
|
4508
4848
|
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
4509
4849
|
}
|
|
4510
|
-
function enableAutoMerge(forkPath, branch) {
|
|
4511
|
-
console.info(colors_default.dim("enabling auto-merge (squash) once checks pass..."));
|
|
4512
|
-
const merge2 = mergePrSquash(forkPath, branch, { auto: true, deleteBranch: true });
|
|
4513
|
-
if (merge2.ok) return true;
|
|
4514
|
-
console.info(colors_default.yellow("could not enable auto-merge (is it enabled for the repo?). merge it manually:"));
|
|
4515
|
-
if (merge2.output) console.info(colors_default.dim(indentLines(merge2.output)));
|
|
4516
|
-
console.info(colors_default.dim(` gh pr merge ${branch} --squash --delete-branch`));
|
|
4517
|
-
return false;
|
|
4518
|
-
}
|
|
4519
4850
|
async function shipSyncBranch(config, branch) {
|
|
4520
4851
|
const { forkPath, settings } = config;
|
|
4521
4852
|
const base = resolveReleaseBase(settings);
|
|
4522
4853
|
const flattened = await flattenSyncBranch(forkPath, branch, base);
|
|
4523
4854
|
let prUrl;
|
|
4524
4855
|
let prOpened = false;
|
|
4525
|
-
let autoMergeEnabled = false;
|
|
4526
4856
|
const headSubject = (await getCommitInfo(forkPath, "HEAD").catch(() => null))?.message;
|
|
4527
4857
|
const prTitle = headSubject?.startsWith(SYNC_PR_TITLE) ? headSubject : SYNC_PR_TITLE;
|
|
4528
4858
|
console.info(colors_default.dim(`pushing '${branch}' to origin...`));
|
|
@@ -4546,9 +4876,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
4546
4876
|
prUrl = extractFirstUrl(`${pr.stdout ?? ""}
|
|
4547
4877
|
${pr.stderr ?? ""}`);
|
|
4548
4878
|
prOpened = pr.status === 0;
|
|
4549
|
-
if (prOpened) {
|
|
4550
|
-
if (config.directMerge) autoMergeEnabled = enableAutoMerge(forkPath, branch);
|
|
4551
|
-
} else {
|
|
4879
|
+
if (!prOpened) {
|
|
4552
4880
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
4553
4881
|
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
4554
4882
|
printPrCreateStep(branch, base, prTitle);
|
|
@@ -4564,12 +4892,12 @@ ${pr.stderr ?? ""}`);
|
|
|
4564
4892
|
console.info();
|
|
4565
4893
|
if (prUrl) {
|
|
4566
4894
|
console.info(`${colors_default.green("\u2713")} Sync pull request ${prOpened ? "opened" : "ready"}`);
|
|
4567
|
-
|
|
4568
|
-
console.info(colors_default.dim(` ${prUrl} \xB7 ${status}`));
|
|
4895
|
+
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, back on '${base}'`));
|
|
4569
4896
|
} else {
|
|
4570
4897
|
console.info(`${colors_default.green("\u2713")} Sync branch pushed`);
|
|
4571
4898
|
console.info(colors_default.dim(` '${branch}' is on origin, back on '${base}'`));
|
|
4572
4899
|
}
|
|
4900
|
+
await printMigrationNotesLine(config);
|
|
4573
4901
|
}
|
|
4574
4902
|
function finalizeWorkspace(forkPath) {
|
|
4575
4903
|
for (const args of [["install"], ["check"]]) {
|
|
@@ -4618,6 +4946,7 @@ async function commitSyncMerge(config, branch) {
|
|
|
4618
4946
|
console.info();
|
|
4619
4947
|
console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
|
|
4620
4948
|
printTriageSteps(branch);
|
|
4949
|
+
await printMigrationNotesLine(config);
|
|
4621
4950
|
}
|
|
4622
4951
|
function printTriageSteps(branch) {
|
|
4623
4952
|
console.info(colors_default.dim(`staying on '${branch}' without pushing.`));
|
|
@@ -4809,7 +5138,7 @@ async function runForks(config) {
|
|
|
4809
5138
|
console.error(colors_default.red(`fork '${config.fork}' not found in config`));
|
|
4810
5139
|
return;
|
|
4811
5140
|
}
|
|
4812
|
-
const resolvedPath =
|
|
5141
|
+
const resolvedPath = resolve6(await resolveForkBasePath(config.forkPath), match.localPath);
|
|
4813
5142
|
await syncFork(config, match, resolvedPath);
|
|
4814
5143
|
return;
|
|
4815
5144
|
}
|
|
@@ -4825,7 +5154,7 @@ async function runForks(config) {
|
|
|
4825
5154
|
if (selectedPath === "_exit") {
|
|
4826
5155
|
process8.exit(0);
|
|
4827
5156
|
}
|
|
4828
|
-
const resolvedForkPath =
|
|
5157
|
+
const resolvedForkPath = resolve6(forkBasePath, selectedPath);
|
|
4829
5158
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
4830
5159
|
if (!selectedFork) {
|
|
4831
5160
|
console.error(colors_default.red(`fork '${selectedPath}' not found in config`));
|
|
@@ -4837,8 +5166,8 @@ async function runForks(config) {
|
|
|
4837
5166
|
}
|
|
4838
5167
|
|
|
4839
5168
|
// src/services/stats.ts
|
|
4840
|
-
import { readFile as
|
|
4841
|
-
import { join as
|
|
5169
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5170
|
+
import { join as join20 } from "path";
|
|
4842
5171
|
|
|
4843
5172
|
// src/utils/coverage-utils.ts
|
|
4844
5173
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -4932,7 +5261,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
4932
5261
|
// src/services/stats.ts
|
|
4933
5262
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
4934
5263
|
async function getWorkspacePackages(forkPath) {
|
|
4935
|
-
const content = await
|
|
5264
|
+
const content = await readFile3(join20(forkPath, "pnpm-workspace.yaml"), "utf-8");
|
|
4936
5265
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
4937
5266
|
if (pattern.endsWith("/*")) {
|
|
4938
5267
|
const base = pattern.slice(0, -2);
|
|
@@ -5095,20 +5424,20 @@ async function runStats(forkPath, options = {}) {
|
|
|
5095
5424
|
function getForkPath() {
|
|
5096
5425
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5097
5426
|
if (envPath) {
|
|
5098
|
-
const resolved =
|
|
5099
|
-
if (!
|
|
5427
|
+
const resolved = resolve7(envPath);
|
|
5428
|
+
if (!existsSync13(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5100
5429
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5101
5430
|
}
|
|
5102
5431
|
return resolved;
|
|
5103
5432
|
}
|
|
5104
5433
|
let cwd = process9.cwd();
|
|
5105
5434
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
5106
|
-
cwd =
|
|
5435
|
+
cwd = resolve7(cwd, "../..");
|
|
5107
5436
|
}
|
|
5108
5437
|
return cwd;
|
|
5109
5438
|
}
|
|
5110
5439
|
async function preflight(forkPath) {
|
|
5111
|
-
if (!
|
|
5440
|
+
if (!existsSync13(join21(forkPath, ".git"))) {
|
|
5112
5441
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5113
5442
|
}
|
|
5114
5443
|
}
|
|
@@ -5137,6 +5466,9 @@ async function main() {
|
|
|
5137
5466
|
await runSyncCommand(config);
|
|
5138
5467
|
break;
|
|
5139
5468
|
}
|
|
5469
|
+
case "migrate":
|
|
5470
|
+
await runMigrate(config);
|
|
5471
|
+
break;
|
|
5140
5472
|
case "audit":
|
|
5141
5473
|
await runAudit(config, { force: config.force, checkOverrides: config.checkOverrides });
|
|
5142
5474
|
break;
|