@cellajs/cli 0.2.3 → 0.3.1
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 +55 -2
- package/dist/config.d.ts +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +1476 -997
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
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 resolve8 } 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.1",
|
|
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
|
-
typescript: "
|
|
291
|
-
vitest: "^
|
|
290
|
+
typescript: "6.0.3",
|
|
291
|
+
vitest: "^5.0.3"
|
|
292
292
|
},
|
|
293
293
|
publishConfig: {
|
|
294
294
|
access: "public"
|
|
@@ -800,415 +800,480 @@ 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
|
-
|
|
814
|
+
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
815
|
+
import { dirname as dirname2, join as join5, resolve as resolve3 } 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
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function readManifestBase(cwd) {
|
|
833
|
+
const manifest = await readSyncManifest(cwd);
|
|
834
|
+
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
835
|
+
}
|
|
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");
|
|
841
|
+
}
|
|
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}://***@`;
|
|
809
849
|
});
|
|
810
850
|
}
|
|
811
|
-
function
|
|
812
|
-
|
|
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;
|
|
867
|
+
}
|
|
813
868
|
}
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
return isUnderAnyFolder(filePath, config.overrides?.pinned || []);
|
|
869
|
+
async function getCurrentBranch(cwd) {
|
|
870
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
817
871
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
872
|
+
async function switchBranch(cwd, branch) {
|
|
873
|
+
await git(["switch", branch], cwd);
|
|
874
|
+
}
|
|
875
|
+
async function switchDetached(cwd, ref) {
|
|
876
|
+
await git(["switch", "--detach", ref], cwd);
|
|
877
|
+
}
|
|
878
|
+
async function createBranchFrom(cwd, branch, startPoint) {
|
|
879
|
+
await git(["switch", "-c", branch, "--no-track", startPoint], cwd);
|
|
880
|
+
}
|
|
881
|
+
async function branchExists(cwd, branch) {
|
|
882
|
+
return await git(["rev-parse", "-q", "--verify", `refs/heads/${branch}`], cwd, { ignoreErrors: true }) !== "";
|
|
883
|
+
}
|
|
884
|
+
async function getBranchWorktree(cwd, branch) {
|
|
885
|
+
const output = await git(["worktree", "list", "--porcelain"], cwd, { ignoreErrors: true });
|
|
886
|
+
let worktree = null;
|
|
887
|
+
for (const line of output.split("\n")) {
|
|
888
|
+
if (line.startsWith("worktree ")) worktree = line.slice("worktree ".length);
|
|
889
|
+
else if (line === `branch refs/heads/${branch}`) return worktree;
|
|
831
890
|
}
|
|
832
|
-
|
|
833
|
-
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
891
|
+
return null;
|
|
834
892
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
if (unpinned) return false;
|
|
838
|
-
return isPinned(filePath, config);
|
|
893
|
+
async function fastForwardBranch(cwd, branch, ref) {
|
|
894
|
+
await git(["fetch", "--quiet", ".", `${ref}:refs/heads/${branch}`], cwd);
|
|
839
895
|
}
|
|
840
|
-
function
|
|
841
|
-
|
|
896
|
+
async function deleteBranch(cwd, branch) {
|
|
897
|
+
await git(["branch", "-D", branch], cwd, { ignoreErrors: true });
|
|
842
898
|
}
|
|
843
|
-
function
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
899
|
+
async function pullFastForward(cwd) {
|
|
900
|
+
await git(["pull", "--ff-only"], cwd, { ignoreErrors: true, skipEditor: true });
|
|
901
|
+
}
|
|
902
|
+
async function getBranchUpstream(cwd, branch = "") {
|
|
903
|
+
const upstream = await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", `${branch}@{upstream}`], cwd, {
|
|
904
|
+
ignoreErrors: true
|
|
905
|
+
});
|
|
906
|
+
return upstream || null;
|
|
907
|
+
}
|
|
908
|
+
async function getUpstreamStatus(cwd, branch) {
|
|
909
|
+
const local = branch ? `refs/heads/${branch}` : "HEAD";
|
|
910
|
+
const upstream = await getBranchUpstream(cwd, branch);
|
|
911
|
+
if (!upstream) return { upstream: null, ahead: 0, behind: 0 };
|
|
912
|
+
const remote = upstream.split("/")[0];
|
|
913
|
+
await git(["fetch", remote], cwd, { ignoreErrors: true, skipEditor: true });
|
|
914
|
+
const counts = await git(["rev-list", "--left-right", "--count", `${local}...${upstream}`], cwd, {
|
|
915
|
+
ignoreErrors: true
|
|
916
|
+
});
|
|
917
|
+
const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
918
|
+
return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
919
|
+
}
|
|
920
|
+
async function pushBranch(cwd, remote, branch, options = {}) {
|
|
921
|
+
await git(["push", "-u", ...options.forceWithLease ? ["--force-with-lease"] : [], remote, branch], cwd);
|
|
922
|
+
}
|
|
923
|
+
async function listBranchMergeCommits(cwd, baseRef) {
|
|
924
|
+
const out = await git(["rev-list", "--merges", "--first-parent", `${baseRef}..HEAD`], cwd, { ignoreErrors: true });
|
|
925
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
926
|
+
}
|
|
927
|
+
async function flattenBranch(cwd, baseRef, message) {
|
|
928
|
+
const parent = await getMergeBase(cwd, baseRef, "HEAD");
|
|
929
|
+
const tree = await git(["rev-parse", "HEAD^{tree}"], cwd);
|
|
930
|
+
const commit = await git(["commit-tree", tree, "-p", parent, "-m", message], cwd);
|
|
931
|
+
await git(["reset", "--hard", commit], cwd);
|
|
932
|
+
}
|
|
933
|
+
async function commitSquash(cwd, message) {
|
|
934
|
+
for (const file of ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE"]) {
|
|
935
|
+
const path4 = await gitPath(cwd, file);
|
|
936
|
+
if (existsSync3(path4)) await unlink(path4);
|
|
860
937
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
938
|
+
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
939
|
+
}
|
|
940
|
+
async function gitPath(cwd, file) {
|
|
941
|
+
return resolve3(cwd, await git(["rev-parse", "--git-path", file], cwd));
|
|
942
|
+
}
|
|
943
|
+
async function stageAll(cwd) {
|
|
944
|
+
await git(["add", "-A"], cwd);
|
|
945
|
+
}
|
|
946
|
+
async function mergeInProgress(cwd) {
|
|
947
|
+
return await git(["rev-parse", "-q", "--verify", "MERGE_HEAD"], cwd, { ignoreErrors: true }) !== "";
|
|
948
|
+
}
|
|
949
|
+
async function getShortSha(cwd, ref) {
|
|
950
|
+
return git(["rev-parse", "--short", ref], cwd);
|
|
951
|
+
}
|
|
952
|
+
async function getWorkingTreeChangeCount(cwd) {
|
|
953
|
+
const status = await git(["status", "--porcelain"], cwd);
|
|
954
|
+
if (!status) return 0;
|
|
955
|
+
return status.split("\n").filter(Boolean).length;
|
|
956
|
+
}
|
|
957
|
+
async function remoteExists(cwd, remoteName) {
|
|
958
|
+
const remotes = await git(["remote"], cwd);
|
|
959
|
+
return remotes.split("\n").includes(remoteName);
|
|
960
|
+
}
|
|
961
|
+
async function ensureRemote(cwd, remoteName, url) {
|
|
962
|
+
if (await remoteExists(cwd, remoteName)) {
|
|
963
|
+
await git(["remote", "set-url", remoteName, url], cwd);
|
|
964
|
+
} else {
|
|
965
|
+
await git(["remote", "add", remoteName, url], cwd);
|
|
876
966
|
}
|
|
877
|
-
return warnings;
|
|
878
967
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
968
|
+
async function fetch2(cwd, remoteName) {
|
|
969
|
+
await git(["fetch", remoteName], cwd);
|
|
970
|
+
}
|
|
971
|
+
async function fetchUpstreamTags(cwd, remoteName) {
|
|
972
|
+
await git(["fetch", remoteName, "--no-tags", `+refs/tags/*:refs/${remoteName}/tags/*`], cwd);
|
|
973
|
+
}
|
|
974
|
+
async function resolveLatestReleaseTag(cwd, remoteName) {
|
|
975
|
+
const prefix = `refs/${remoteName}/tags/`;
|
|
976
|
+
const output = await git(["for-each-ref", "--sort=-version:refname", "--format=%(refname)", prefix], cwd, {
|
|
977
|
+
ignoreErrors: true
|
|
978
|
+
});
|
|
979
|
+
if (!output) return null;
|
|
980
|
+
for (const line of output.split("\n")) {
|
|
981
|
+
const ref = line.trim();
|
|
982
|
+
if (!ref) continue;
|
|
983
|
+
const tag = ref.slice(prefix.length);
|
|
984
|
+
if (/^v\d/.test(tag)) return { tag, ref };
|
|
882
985
|
}
|
|
986
|
+
return null;
|
|
883
987
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
988
|
+
async function resolveUpstreamCommit(cwd, remoteName, ref) {
|
|
989
|
+
if (ref.startsWith("-")) return null;
|
|
990
|
+
const tagRef = `refs/${remoteName}/tags/${ref}`;
|
|
991
|
+
for (const candidate of [`refs/remotes/${remoteName}/${ref}`, tagRef, ref]) {
|
|
992
|
+
const sha = await git(["rev-parse", "-q", "--verify", `${candidate}^{commit}`], cwd, { ignoreErrors: true });
|
|
993
|
+
if (!sha) continue;
|
|
994
|
+
const release = candidate === tagRef && /^v\d[\w.+-]*$/.test(ref) ? { tag: ref, ref: tagRef } : void 0;
|
|
995
|
+
return { sha, release };
|
|
890
996
|
}
|
|
891
|
-
|
|
892
|
-
return {
|
|
893
|
-
logFile: opts.log === true,
|
|
894
|
-
verbose: opts.verbose === true,
|
|
895
|
-
list: opts.list === true,
|
|
896
|
-
json: opts.json === true,
|
|
897
|
-
diff: typeof opts.diff === "string" ? opts.diff : void 0,
|
|
898
|
-
openDiff: typeof opts.openDiff === "string" ? opts.openDiff : void 0,
|
|
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
|
-
};
|
|
997
|
+
return null;
|
|
909
998
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
{
|
|
919
|
-
{
|
|
920
|
-
{ flags: "--diff <path>", description: "print unified diff for one file, then exit" },
|
|
921
|
-
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
922
|
-
],
|
|
923
|
-
includeInMenu: (context) => !context.isUpstreamRepo
|
|
924
|
-
},
|
|
925
|
-
{
|
|
926
|
-
name: "sync",
|
|
927
|
-
description: "merge upstream changes into your app",
|
|
928
|
-
options: [
|
|
929
|
-
{ flags: "--log", description: "write complete file list to cella-sync.log" },
|
|
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"
|
|
936
|
-
}
|
|
999
|
+
async function isPublishedUpstream(cwd, remoteName, branchRef, sha) {
|
|
1000
|
+
const containing = await git(
|
|
1001
|
+
[
|
|
1002
|
+
"for-each-ref",
|
|
1003
|
+
"--count=1",
|
|
1004
|
+
"--format=%(refname)",
|
|
1005
|
+
"--contains",
|
|
1006
|
+
sha,
|
|
1007
|
+
`refs/remotes/${branchRef}`,
|
|
1008
|
+
`refs/${remoteName}/tags/v*`
|
|
937
1009
|
],
|
|
938
|
-
|
|
939
|
-
|
|
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
|
-
};
|
|
1010
|
+
cwd,
|
|
1011
|
+
{ ignoreErrors: true }
|
|
1012
|
+
);
|
|
1013
|
+
return containing !== "";
|
|
986
1014
|
}
|
|
987
|
-
function
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
];
|
|
1015
|
+
async function getCommitInfo(cwd, ref) {
|
|
1016
|
+
const format = "%H%n%s%n%ar";
|
|
1017
|
+
const output = await git(["log", "-1", `--format=${format}`, ref], cwd);
|
|
1018
|
+
const [hash, message, date] = output.split("\n");
|
|
1019
|
+
return { hash, message, date };
|
|
998
1020
|
}
|
|
999
|
-
function
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
command.option(option.flags, option.description);
|
|
1003
|
-
}
|
|
1004
|
-
command.action((opts) => {
|
|
1005
|
-
setSelection({
|
|
1006
|
-
service: definition.name,
|
|
1007
|
-
options: readOptions(opts)
|
|
1008
|
-
});
|
|
1009
|
-
});
|
|
1021
|
+
async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
1022
|
+
const output = await git(["rev-list", "--count", `${fromRef}..${toRef}`], cwd);
|
|
1023
|
+
return Number.parseInt(output.trim(), 10) || 0;
|
|
1010
1024
|
}
|
|
1011
|
-
function
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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);
|
|
1025
|
+
async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
1026
|
+
const { oldestFirst = true, skip = 0, limit } = options;
|
|
1027
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar"];
|
|
1028
|
+
if (oldestFirst) args.push("--reverse");
|
|
1029
|
+
args.push(`${fromRef}..${toRef}`);
|
|
1030
|
+
const output = await git(args, cwd, { ignoreErrors: true });
|
|
1031
|
+
if (!output) return [];
|
|
1032
|
+
const commits = [];
|
|
1033
|
+
for (const line of output.split("\n")) {
|
|
1034
|
+
if (!line) continue;
|
|
1035
|
+
const [hash, message, date] = line.split("");
|
|
1036
|
+
if (!hash || !message || !date) continue;
|
|
1037
|
+
commits.push({ hash, message, date });
|
|
1030
1038
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return { options: readOptions({}) };
|
|
1039
|
+
const normalizedSkip = Math.max(0, skip);
|
|
1040
|
+
const normalizedLimit = typeof limit === "number" && limit > 0 ? limit : void 0;
|
|
1041
|
+
if (normalizedSkip === 0 && normalizedLimit === void 0) {
|
|
1042
|
+
return commits;
|
|
1036
1043
|
}
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
});
|
|
1041
|
-
program.parse(argv);
|
|
1042
|
-
return selection;
|
|
1044
|
+
const start = Math.min(normalizedSkip, commits.length);
|
|
1045
|
+
const end = normalizedLimit === void 0 ? commits.length : Math.min(start + normalizedLimit, commits.length);
|
|
1046
|
+
return commits.slice(start, end);
|
|
1043
1047
|
}
|
|
1044
|
-
async function
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1048
|
+
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
1049
|
+
const info = /* @__PURE__ */ new Map();
|
|
1050
|
+
try {
|
|
1051
|
+
const output = await git(["log", "--name-only", "--format=%h %ct %ar", `${fromRef}..${toRef}`], cwd, {
|
|
1052
|
+
ignoreErrors: true
|
|
1053
|
+
});
|
|
1054
|
+
if (!output.trim()) return info;
|
|
1055
|
+
const lines = output.split("\n");
|
|
1056
|
+
let currentHash = "";
|
|
1057
|
+
let currentDate = "";
|
|
1058
|
+
let currentTimestamp = 0;
|
|
1059
|
+
for (const line of lines) {
|
|
1060
|
+
if (!line) continue;
|
|
1061
|
+
const match = line.match(/^([a-f0-9]{7,})\s+(\d+)\s+(.+)$/);
|
|
1062
|
+
if (match) {
|
|
1063
|
+
currentHash = match[1];
|
|
1064
|
+
currentTimestamp = Number(match[2]);
|
|
1065
|
+
currentDate = match[3];
|
|
1066
|
+
} else if (currentHash && currentDate && !info.has(line)) {
|
|
1067
|
+
info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
} catch {
|
|
1054
1071
|
}
|
|
1055
|
-
|
|
1056
|
-
return selected;
|
|
1072
|
+
return info;
|
|
1057
1073
|
}
|
|
1058
|
-
function
|
|
1059
|
-
const
|
|
1060
|
-
return
|
|
1061
|
-
...userConfig,
|
|
1062
|
-
forkPath,
|
|
1063
|
-
upstreamRef: branchRef,
|
|
1064
|
-
service: selection.service ?? "analyze",
|
|
1065
|
-
...selection.options
|
|
1066
|
-
};
|
|
1074
|
+
async function isClean(cwd) {
|
|
1075
|
+
const status = await git(["status", "--porcelain"], cwd);
|
|
1076
|
+
return !status;
|
|
1067
1077
|
}
|
|
1068
|
-
async function
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
printHeader();
|
|
1072
|
-
const warnings = validateOverrides(userConfig, forkPath);
|
|
1073
|
-
if (warnings.length > 0) {
|
|
1074
|
-
printWarnings(warnings);
|
|
1075
|
-
console.info();
|
|
1078
|
+
async function assertClean(cwd, subject = "working directory") {
|
|
1079
|
+
if (!await isClean(cwd)) {
|
|
1080
|
+
throw new Error(`${subject} has uncommitted changes. please commit or stash before syncing.`);
|
|
1076
1081
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1082
|
+
}
|
|
1083
|
+
async function createWorktree(cwd, worktreePath, commitRef) {
|
|
1084
|
+
await git(["worktree", "add", "--detach", worktreePath, commitRef], cwd);
|
|
1085
|
+
}
|
|
1086
|
+
async function removeWorktree(cwd, worktreePath) {
|
|
1087
|
+
await git(["worktree", "remove", worktreePath, "--force"], cwd, { ignoreErrors: true });
|
|
1088
|
+
await git(["worktree", "prune"], cwd, { ignoreErrors: true });
|
|
1089
|
+
}
|
|
1090
|
+
async function listWorktrees(cwd) {
|
|
1091
|
+
const output = await git(["worktree", "list", "--porcelain"], cwd);
|
|
1092
|
+
const lines = output.split("\n");
|
|
1093
|
+
const paths = [];
|
|
1094
|
+
for (const line of lines) {
|
|
1095
|
+
if (line.startsWith("worktree ")) {
|
|
1096
|
+
paths.push(line.slice(9));
|
|
1097
|
+
}
|
|
1079
1098
|
}
|
|
1080
|
-
return
|
|
1099
|
+
return paths;
|
|
1081
1100
|
}
|
|
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);
|
|
1101
|
+
async function merge(cwd, ref, options = {}) {
|
|
1102
|
+
const args = ["merge", "--no-ff", ref];
|
|
1103
|
+
if (options.noCommit) args.push("--no-commit");
|
|
1104
|
+
if (options.noEdit) args.push("--no-edit");
|
|
1096
1105
|
try {
|
|
1097
|
-
|
|
1106
|
+
await git(args, cwd, { skipEditor: true });
|
|
1107
|
+
return { success: true, conflicts: [] };
|
|
1098
1108
|
} catch (error) {
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1109
|
+
const conflicts = await getConflictedFiles(cwd);
|
|
1110
|
+
if (conflicts.length > 0) {
|
|
1111
|
+
return { success: false, conflicts };
|
|
1112
|
+
}
|
|
1113
|
+
throw error;
|
|
1101
1114
|
}
|
|
1102
1115
|
}
|
|
1103
|
-
function
|
|
1104
|
-
|
|
1116
|
+
async function getConflictedFiles(cwd) {
|
|
1117
|
+
const output = await git(["diff", "--name-only", "--diff-filter=U"], cwd, { ignoreErrors: true });
|
|
1118
|
+
if (!output) return [];
|
|
1119
|
+
return output.split("\n").filter(Boolean);
|
|
1105
1120
|
}
|
|
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
|
-
|
|
1121
|
+
async function mergeAbort(cwd) {
|
|
1122
|
+
await git(["merge", "--abort"], cwd, { ignoreErrors: true });
|
|
1123
|
+
}
|
|
1124
|
+
async function fileExistsAtRef(cwd, ref, filePath) {
|
|
1125
|
+
const result = await git(["ls-tree", "--name-only", ref, "--", filePath], cwd, { ignoreErrors: true });
|
|
1126
|
+
return result !== "";
|
|
1127
|
+
}
|
|
1128
|
+
async function checkoutFromRef(cwd, ref, filePath) {
|
|
1129
|
+
await git(["checkout", ref, "--", filePath], cwd);
|
|
1130
|
+
}
|
|
1131
|
+
async function restoreWorktreeFromRef(cwd, ref, filePath) {
|
|
1132
|
+
await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
|
|
1133
|
+
}
|
|
1134
|
+
async function getMergeBase(cwd, ref1, ref2) {
|
|
1135
|
+
return git(["merge-base", ref1, ref2], cwd);
|
|
1136
|
+
}
|
|
1137
|
+
async function getFileChanges(cwd, baseRef, targetRef) {
|
|
1138
|
+
const output = await git(["diff-tree", "-r", "-M90%", "--no-commit-id", baseRef, targetRef], cwd, {
|
|
1139
|
+
ignoreErrors: true
|
|
1140
|
+
});
|
|
1141
|
+
const changes = /* @__PURE__ */ new Map();
|
|
1142
|
+
if (!output) return changes;
|
|
1143
|
+
for (const line of output.split("\n")) {
|
|
1144
|
+
if (!line) continue;
|
|
1145
|
+
const renameMatch = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) R\d*\s+(.+)\t(.+)$/);
|
|
1146
|
+
if (renameMatch) {
|
|
1147
|
+
const [, baseHash, targetHash, oldPath, newPath] = renameMatch;
|
|
1148
|
+
changes.set(newPath, {
|
|
1149
|
+
status: "R",
|
|
1150
|
+
baseHash: baseHash === "0".repeat(40) ? null : baseHash,
|
|
1151
|
+
targetHash: targetHash === "0".repeat(40) ? null : targetHash,
|
|
1152
|
+
oldPath,
|
|
1153
|
+
newPath
|
|
1154
|
+
});
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const match = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) ([ADMT])\t(.+)$/);
|
|
1158
|
+
if (match) {
|
|
1159
|
+
const [, baseHash, targetHash, status, filePath] = match;
|
|
1160
|
+
changes.set(filePath, {
|
|
1161
|
+
status,
|
|
1162
|
+
baseHash: baseHash === "0".repeat(40) ? null : baseHash,
|
|
1163
|
+
targetHash: targetHash === "0".repeat(40) ? null : targetHash
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1135
1166
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1167
|
+
return changes;
|
|
1168
|
+
}
|
|
1169
|
+
async function getDiffStat(cwd, fromRef, toRef, paths = []) {
|
|
1170
|
+
const args = ["diff", "--numstat", "--no-renames", fromRef, toRef];
|
|
1171
|
+
if (paths.length > 0) args.push("--", ...paths);
|
|
1172
|
+
const out = await git(args, cwd, { ignoreErrors: true });
|
|
1173
|
+
const stat = /* @__PURE__ */ new Map();
|
|
1174
|
+
for (const line of out.split("\n")) {
|
|
1175
|
+
if (!line.trim()) continue;
|
|
1176
|
+
const [a, d, ...rest] = line.split(" ");
|
|
1177
|
+
const path4 = rest.join(" ");
|
|
1178
|
+
if (!path4) continue;
|
|
1179
|
+
stat.set(path4, {
|
|
1180
|
+
additions: a === "-" ? null : Number(a),
|
|
1181
|
+
deletions: d === "-" ? null : Number(d)
|
|
1182
|
+
});
|
|
1142
1183
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1184
|
+
return stat;
|
|
1185
|
+
}
|
|
1186
|
+
async function getFileHashesAtRef(cwd, ref) {
|
|
1187
|
+
const output = await git(["ls-tree", "-r", ref], cwd);
|
|
1188
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
1189
|
+
if (!output) return hashes;
|
|
1190
|
+
for (const line of output.split("\n")) {
|
|
1191
|
+
if (!line) continue;
|
|
1192
|
+
const match = line.match(/^\d+ \w+ ([a-f0-9]+)\t(.+)$/);
|
|
1193
|
+
if (match) {
|
|
1194
|
+
const [, hash, filePath] = match;
|
|
1195
|
+
hashes.set(filePath, hash);
|
|
1196
|
+
}
|
|
1151
1197
|
}
|
|
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>`;
|
|
1198
|
+
return hashes;
|
|
1171
1199
|
}
|
|
1172
|
-
function
|
|
1200
|
+
async function restoreToHead(cwd, filePath) {
|
|
1201
|
+
await git(["checkout", "HEAD", "--", filePath], cwd);
|
|
1202
|
+
}
|
|
1203
|
+
async function batchRestoreToHead(cwd, filePaths) {
|
|
1204
|
+
if (filePaths.length === 0) return;
|
|
1205
|
+
await git(["checkout", "HEAD", "--", ...filePaths], cwd);
|
|
1206
|
+
}
|
|
1207
|
+
async function gitRm(cwd, filePath) {
|
|
1208
|
+
await git(["rm", "-f", "--", filePath], cwd, { ignoreErrors: true });
|
|
1209
|
+
}
|
|
1210
|
+
async function batchGitRm(cwd, filePaths) {
|
|
1211
|
+
if (filePaths.length === 0) return;
|
|
1212
|
+
await git(["rm", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1213
|
+
}
|
|
1214
|
+
async function getStagedNewFiles(cwd) {
|
|
1215
|
+
const output = await git(["diff", "--cached", "--name-only", "--diff-filter=A"], cwd, { ignoreErrors: true });
|
|
1216
|
+
return output ? output.split("\n").filter(Boolean) : [];
|
|
1217
|
+
}
|
|
1218
|
+
async function batchUnstageFiles(cwd, filePaths) {
|
|
1219
|
+
if (filePaths.length === 0) return;
|
|
1220
|
+
await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1221
|
+
}
|
|
1222
|
+
async function gitMv(cwd, oldPath, newPath) {
|
|
1223
|
+
const newDir = dirname2(join5(cwd, newPath));
|
|
1224
|
+
await mkdir2(newDir, { recursive: true });
|
|
1225
|
+
await git(["mv", "-f", oldPath, newPath], cwd);
|
|
1226
|
+
}
|
|
1227
|
+
async function fileExistsInWorktree(cwd, filePath) {
|
|
1228
|
+
return existsSync3(join5(cwd, filePath));
|
|
1229
|
+
}
|
|
1230
|
+
async function removeFileFromWorktree(cwd, filePath) {
|
|
1231
|
+
const fullPath = join5(cwd, filePath);
|
|
1173
1232
|
try {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1233
|
+
await unlink(fullPath);
|
|
1234
|
+
await cleanupEmptyParentDirs(cwd, dirname2(filePath));
|
|
1235
|
+
} catch {
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async function removeFileFully(cwd, filePath) {
|
|
1239
|
+
await gitRm(cwd, filePath);
|
|
1240
|
+
await removeFileFromWorktree(cwd, filePath);
|
|
1241
|
+
}
|
|
1242
|
+
async function cleanupEmptyParentDirs(cwd, relativePath) {
|
|
1243
|
+
if (!relativePath || relativePath === ".") return;
|
|
1244
|
+
const fullPath = join5(cwd, relativePath);
|
|
1245
|
+
try {
|
|
1246
|
+
const entries = await readdir(fullPath);
|
|
1247
|
+
if (entries.length === 0) {
|
|
1248
|
+
await rmdir(fullPath);
|
|
1249
|
+
await cleanupEmptyParentDirs(cwd, dirname2(relativePath));
|
|
1178
1250
|
}
|
|
1179
1251
|
} catch {
|
|
1180
1252
|
}
|
|
1181
1253
|
}
|
|
1182
|
-
async function
|
|
1183
|
-
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
openWithDefaultApp(outPath);
|
|
1189
|
-
return outPath;
|
|
1254
|
+
async function storeLastSyncRef(cwd, upstreamHash) {
|
|
1255
|
+
await git(["update-ref", "refs/cella/last-sync", upstreamHash], cwd);
|
|
1256
|
+
const head = await git(["rev-parse", "HEAD"], cwd, { ignoreErrors: true });
|
|
1257
|
+
if (head) {
|
|
1258
|
+
await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
|
|
1259
|
+
}
|
|
1190
1260
|
}
|
|
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) {
|
|
1261
|
+
async function stagePath(cwd, path4) {
|
|
1262
|
+
await git(["add", "--", path4], cwd, { ignoreErrors: true });
|
|
1263
|
+
}
|
|
1264
|
+
async function getStoredSyncRef(cwd) {
|
|
1265
|
+
const ref = await git(["rev-parse", "refs/cella/last-sync"], cwd, { ignoreErrors: true });
|
|
1266
|
+
return ref || null;
|
|
1267
|
+
}
|
|
1268
|
+
async function getStoredSyncHead(cwd) {
|
|
1269
|
+
const ref = await git(["rev-parse", "refs/cella/last-sync-head"], cwd, { ignoreErrors: true });
|
|
1270
|
+
return ref || null;
|
|
1271
|
+
}
|
|
1272
|
+
async function readManifestAtRef(cwd, ref) {
|
|
1273
|
+
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
1274
|
+
if (!raw) return null;
|
|
1210
1275
|
try {
|
|
1211
|
-
const parsed = JSON.parse(
|
|
1276
|
+
const parsed = JSON.parse(raw);
|
|
1212
1277
|
const commit = parsed?.upstream?.commit;
|
|
1213
1278
|
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
1214
1279
|
return parsed;
|
|
@@ -1216,588 +1281,690 @@ async function readSyncManifest(cwd) {
|
|
|
1216
1281
|
return null;
|
|
1217
1282
|
}
|
|
1218
1283
|
}
|
|
1219
|
-
async function
|
|
1220
|
-
const manifest = await
|
|
1284
|
+
async function readManifestBaseAtRef(cwd, ref) {
|
|
1285
|
+
const manifest = await readManifestAtRef(cwd, ref);
|
|
1221
1286
|
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
1222
1287
|
}
|
|
1223
|
-
async function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
function sanitizeCredentials(text) {
|
|
1233
|
-
return text.replace(/https?:\/\/[^@\s]+@/g, (match) => {
|
|
1234
|
-
const protocol = match.startsWith("https") ? "https" : "http";
|
|
1235
|
-
return `${protocol}://***@`;
|
|
1236
|
-
});
|
|
1288
|
+
async function readPackageVersionAtRef(cwd, ref) {
|
|
1289
|
+
const raw = await git(["show", `${ref}:package.json`], cwd, { ignoreErrors: true });
|
|
1290
|
+
if (!raw) return null;
|
|
1291
|
+
try {
|
|
1292
|
+
const version = JSON.parse(raw).version;
|
|
1293
|
+
return typeof version === "string" && version ? version : null;
|
|
1294
|
+
} catch {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1237
1297
|
}
|
|
1238
|
-
async function
|
|
1239
|
-
const env = {
|
|
1240
|
-
...getEnvSnapshot(),
|
|
1241
|
-
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
1242
|
-
...options.env
|
|
1243
|
-
};
|
|
1244
|
-
const maxBuffer = options.maxBuffer ?? 50 * 1024 * 1024;
|
|
1298
|
+
async function commitObjectExists(cwd, sha) {
|
|
1245
1299
|
try {
|
|
1246
|
-
|
|
1247
|
-
return
|
|
1248
|
-
} catch
|
|
1249
|
-
|
|
1250
|
-
if (error instanceof Error) {
|
|
1251
|
-
error.message = sanitizeCredentials(error.message);
|
|
1252
|
-
}
|
|
1253
|
-
throw error;
|
|
1300
|
+
await git(["cat-file", "-e", `${sha}^{commit}`], cwd);
|
|
1301
|
+
return true;
|
|
1302
|
+
} catch {
|
|
1303
|
+
return false;
|
|
1254
1304
|
}
|
|
1255
1305
|
}
|
|
1256
|
-
async function
|
|
1257
|
-
|
|
1306
|
+
async function getRootCommit(cwd, ref) {
|
|
1307
|
+
const out = await git(["rev-list", "--max-parents=0", ref], cwd, { ignoreErrors: true });
|
|
1308
|
+
const roots = out.split("\n").filter(Boolean);
|
|
1309
|
+
return roots.length > 0 ? roots[roots.length - 1] : null;
|
|
1258
1310
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1311
|
+
var SCAFFOLD_CANDIDATE_LIMIT = 400;
|
|
1312
|
+
var SCAFFOLD_SCORE_CONCURRENCY = 10;
|
|
1313
|
+
var SCAFFOLD_MATCH_MAX_DIFF_RATIO = 0.3;
|
|
1314
|
+
async function inferScaffoldBase(cwd, headRef, upstreamRef) {
|
|
1315
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1316
|
+
if (!root) return null;
|
|
1317
|
+
const rootDate = await git(["show", "-s", "--format=%cI", root], cwd, { ignoreErrors: true });
|
|
1318
|
+
if (!rootDate) return null;
|
|
1319
|
+
const before = new Date(new Date(rootDate).getTime() + 24 * 60 * 60 * 1e3).toISOString();
|
|
1320
|
+
const revList = await git(
|
|
1321
|
+
["rev-list", `--max-count=${SCAFFOLD_CANDIDATE_LIMIT}`, `--before=${before}`, upstreamRef],
|
|
1322
|
+
cwd,
|
|
1323
|
+
{ ignoreErrors: true }
|
|
1324
|
+
);
|
|
1325
|
+
const candidates = revList.split("\n").filter(Boolean);
|
|
1326
|
+
if (candidates.length === 0) return null;
|
|
1327
|
+
const totalPaths = (await git(["ls-tree", "-r", "--name-only", root], cwd)).split("\n").filter(Boolean).length;
|
|
1328
|
+
if (totalPaths === 0) return null;
|
|
1329
|
+
let best = null;
|
|
1330
|
+
outer: for (let i = 0; i < candidates.length; i += SCAFFOLD_SCORE_CONCURRENCY) {
|
|
1331
|
+
const chunk = candidates.slice(i, i + SCAFFOLD_SCORE_CONCURRENCY);
|
|
1332
|
+
const scores = await Promise.all(
|
|
1333
|
+
chunk.map(async (sha) => {
|
|
1334
|
+
try {
|
|
1335
|
+
const out = await git(["diff-tree", "-r", "--name-only", sha, root], cwd);
|
|
1336
|
+
return { sha, diff: out ? out.split("\n").length : 0 };
|
|
1337
|
+
} catch {
|
|
1338
|
+
return { sha, diff: Number.POSITIVE_INFINITY };
|
|
1339
|
+
}
|
|
1340
|
+
})
|
|
1341
|
+
);
|
|
1342
|
+
for (const score of scores) {
|
|
1343
|
+
if (!best || score.diff < best.diff) best = score;
|
|
1344
|
+
if (best.diff === 0) break outer;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (!best || best.diff / totalPaths > SCAFFOLD_MATCH_MAX_DIFF_RATIO) return null;
|
|
1348
|
+
return best.sha;
|
|
1261
1349
|
}
|
|
1262
|
-
async function
|
|
1263
|
-
|
|
1350
|
+
async function readRootTrailerBase(cwd, headRef) {
|
|
1351
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1352
|
+
if (!root) return null;
|
|
1353
|
+
const message = await git(["show", "-s", "--format=%B", root], cwd, { ignoreErrors: true });
|
|
1354
|
+
const match = message.match(/^cella-base:\s*([0-9a-f]{40})$/im);
|
|
1355
|
+
return match ? match[1].toLowerCase() : null;
|
|
1264
1356
|
}
|
|
1265
|
-
async function
|
|
1266
|
-
await git(["
|
|
1357
|
+
async function isShallowRepository(cwd) {
|
|
1358
|
+
const out = await git(["rev-parse", "--is-shallow-repository"], cwd, { ignoreErrors: true });
|
|
1359
|
+
return out === "true";
|
|
1267
1360
|
}
|
|
1268
|
-
async function
|
|
1269
|
-
|
|
1361
|
+
async function unshallowRepository(cwd) {
|
|
1362
|
+
try {
|
|
1363
|
+
await git(["fetch", "--unshallow"], cwd, { skipEditor: true });
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
const detail = error instanceof Error ? `
|
|
1366
|
+
|
|
1367
|
+
${error.message}` : "";
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
`this is a shallow clone and its full history could not be restored automatically.
|
|
1370
|
+
Sync needs the complete history to find the common ancestor with upstream. Restore it with:
|
|
1371
|
+
|
|
1372
|
+
git fetch --unshallow
|
|
1373
|
+
|
|
1374
|
+
then re-run sync.${detail}`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1270
1377
|
}
|
|
1271
|
-
async function
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1378
|
+
async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
1379
|
+
if (await isShallowRepository(cwd)) {
|
|
1380
|
+
await unshallowRepository(cwd);
|
|
1381
|
+
}
|
|
1382
|
+
const nativeBase = await git(["merge-base", headRef, upstreamRef], cwd, { ignoreErrors: true });
|
|
1383
|
+
if (nativeBase) return;
|
|
1384
|
+
const sources = [
|
|
1385
|
+
() => getStoredSyncRef(cwd),
|
|
1386
|
+
() => readManifestBaseAtRef(cwd, headRef),
|
|
1387
|
+
() => readManifestBase(cwd),
|
|
1388
|
+
() => readRootTrailerBase(cwd, headRef),
|
|
1389
|
+
() => inferScaffoldBase(cwd, headRef, upstreamRef)
|
|
1390
|
+
];
|
|
1391
|
+
let baseSha = null;
|
|
1392
|
+
for (const source of sources) {
|
|
1393
|
+
const sha = await source();
|
|
1394
|
+
if (sha && await commitObjectExists(cwd, sha)) {
|
|
1395
|
+
baseSha = sha;
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
if (!baseSha) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1402
|
+
|
|
1403
|
+
This fork has unrelated history with upstream \u2014 typical for a create-cella scaffold, or after
|
|
1404
|
+
upstream squashed its history \u2014 and no recorded or inferable base commit exists after fetching
|
|
1405
|
+
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1406
|
+
|
|
1407
|
+
git update-ref refs/cella/last-sync <upstream-sha>
|
|
1408
|
+
`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
const root = await getRootCommit(cwd, headRef);
|
|
1412
|
+
if (!root) throw new Error("could not determine the fork's root commit to bootstrap sync ancestry");
|
|
1413
|
+
await git(["replace", "-f", "--graft", root, baseSha], cwd);
|
|
1414
|
+
await git(["update-ref", "refs/cella/last-sync", baseSha], cwd);
|
|
1281
1415
|
}
|
|
1282
|
-
async function
|
|
1283
|
-
|
|
1416
|
+
async function isAncestor(cwd, ancestor, descendant) {
|
|
1417
|
+
try {
|
|
1418
|
+
await git(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
|
|
1419
|
+
return true;
|
|
1420
|
+
} catch {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1284
1423
|
}
|
|
1285
|
-
async function
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1424
|
+
async function getLegacySyncRef(cwd, headRef) {
|
|
1425
|
+
const storedRef = await getStoredSyncRef(cwd);
|
|
1426
|
+
if (!storedRef) return null;
|
|
1427
|
+
const storedHead = await getStoredSyncHead(cwd);
|
|
1428
|
+
if (storedHead) {
|
|
1429
|
+
const currentHead = await git(["rev-parse", headRef], cwd, { ignoreErrors: true });
|
|
1430
|
+
if (currentHead && currentHead === storedHead) return null;
|
|
1431
|
+
}
|
|
1432
|
+
return storedRef;
|
|
1288
1433
|
}
|
|
1289
|
-
async function
|
|
1290
|
-
const
|
|
1291
|
-
const
|
|
1292
|
-
|
|
1293
|
-
|
|
1434
|
+
async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
1435
|
+
const gitBase = await getMergeBase(cwd, headRef, upstreamRef);
|
|
1436
|
+
const recorded = await readManifestBaseAtRef(cwd, headRef) ?? await getLegacySyncRef(cwd, headRef);
|
|
1437
|
+
if (!recorded) {
|
|
1438
|
+
return { base: gitBase, isStale: false, storedRef: null };
|
|
1439
|
+
}
|
|
1440
|
+
const recordedIsNewer = await isAncestor(cwd, gitBase, recorded);
|
|
1441
|
+
if (recordedIsNewer && recorded !== gitBase) {
|
|
1442
|
+
return { base: recorded, isStale: true, storedRef: recorded };
|
|
1443
|
+
}
|
|
1444
|
+
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
1294
1445
|
}
|
|
1295
|
-
async function
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1446
|
+
async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
1447
|
+
const headSha = await git(["rev-parse", headRef], cwd, { ignoreErrors: true });
|
|
1448
|
+
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
1449
|
+
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
1450
|
+
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
1451
|
+
if (existingReplacement) return fn();
|
|
1452
|
+
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
1453
|
+
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
1454
|
+
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
1455
|
+
try {
|
|
1456
|
+
return await fn();
|
|
1457
|
+
} finally {
|
|
1458
|
+
await git(["replace", "-d", headSha], cwd, { ignoreErrors: true });
|
|
1299
1459
|
}
|
|
1300
|
-
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
1301
1460
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1461
|
+
|
|
1462
|
+
// src/utils/migration-notes.ts
|
|
1463
|
+
var NOTES_DIR = "cella/migrations";
|
|
1464
|
+
var PENDING_FILE = "cella/cella.migrations.json";
|
|
1465
|
+
var NOTE_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1466
|
+
function isUpstreamOnly(filePath) {
|
|
1467
|
+
return filePath === NOTES_DIR || filePath.startsWith(`${NOTES_DIR}/`);
|
|
1468
|
+
}
|
|
1469
|
+
function bodyLines(lines) {
|
|
1470
|
+
let inComment = false;
|
|
1471
|
+
return lines.filter((line) => {
|
|
1472
|
+
if (!inComment && !line.trimStart().startsWith("<!--")) return true;
|
|
1473
|
+
inComment = !line.includes("-->");
|
|
1474
|
+
return false;
|
|
1475
|
+
});
|
|
1304
1476
|
}
|
|
1305
|
-
function
|
|
1306
|
-
|
|
1477
|
+
function parseNote(id, source, fileNames) {
|
|
1478
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
1479
|
+
const close = lines[0] === "---" ? lines.indexOf("---", 1) : -1;
|
|
1480
|
+
const meta = /* @__PURE__ */ new Map();
|
|
1481
|
+
for (const line of lines.slice(1, Math.max(close, 1))) {
|
|
1482
|
+
const match = /^(\w+):\s*(.*)$/.exec(line);
|
|
1483
|
+
if (match) meta.set(match[1], match[2].trim());
|
|
1484
|
+
}
|
|
1485
|
+
const body = bodyLines(lines.slice(close + 1)).join("\n");
|
|
1486
|
+
const blocks = body.split(/\n\s*\n/).map((block) => block.trim()).filter(Boolean);
|
|
1487
|
+
const title = /^# (.+)$/.exec(blocks[0] ?? "")?.[1].trim();
|
|
1488
|
+
const summary = title && blocks[1] && !/^(#|>|[-*|]|```)/.test(blocks[1]) ? blocks[1].replace(/\s*\n\s*/g, " ") : "";
|
|
1489
|
+
return {
|
|
1490
|
+
id,
|
|
1491
|
+
title: title || id,
|
|
1492
|
+
summary,
|
|
1493
|
+
syncBreaking: meta.get("syncBreaking") === "true",
|
|
1494
|
+
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1495
|
+
roots: (meta.get("roots") ?? "").split(",").map((root) => root.trim()).filter(Boolean),
|
|
1496
|
+
codemod: fileNames.find((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")) ?? null
|
|
1497
|
+
};
|
|
1307
1498
|
}
|
|
1308
|
-
async function
|
|
1309
|
-
|
|
1499
|
+
async function listNoteIds(cwd, ref) {
|
|
1500
|
+
const out = await git(["ls-tree", "--name-only", `${ref}:${NOTES_DIR}`], cwd, { ignoreErrors: true });
|
|
1501
|
+
return out.split("\n").filter((name) => NOTE_ID.test(name)).sort();
|
|
1502
|
+
}
|
|
1503
|
+
async function readNote(cwd, ref, id) {
|
|
1504
|
+
const source = await git(["show", `${ref}:${NOTES_DIR}/${id}/README.md`], cwd, { ignoreErrors: true });
|
|
1505
|
+
if (!source) return null;
|
|
1506
|
+
const files = await git(["ls-tree", "--name-only", `${ref}:${NOTES_DIR}/${id}`], cwd, { ignoreErrors: true });
|
|
1507
|
+
return parseNote(id, source, files.split("\n").filter(Boolean));
|
|
1508
|
+
}
|
|
1509
|
+
async function arrivedNoteIds(cwd, from, to) {
|
|
1510
|
+
const [before, after] = await Promise.all([listNoteIds(cwd, from), listNoteIds(cwd, to)]);
|
|
1511
|
+
const known = new Set(before);
|
|
1512
|
+
return after.filter((id) => !known.has(id));
|
|
1513
|
+
}
|
|
1514
|
+
async function readPending(cwd, syncRef) {
|
|
1515
|
+
const path4 = join6(cwd, PENDING_FILE);
|
|
1516
|
+
if (!existsSync4(path4)) return [];
|
|
1517
|
+
const parsed = JSON.parse(await readFile2(path4, "utf8"));
|
|
1518
|
+
if (Array.isArray(parsed.pending)) return parsed.pending.filter((id) => typeof id === "string");
|
|
1519
|
+
if (!Array.isArray(parsed.applied) || !syncRef) return [];
|
|
1520
|
+
const applied = new Set(parsed.applied);
|
|
1521
|
+
return (await listNoteIds(cwd, syncRef)).filter((id) => !applied.has(id));
|
|
1522
|
+
}
|
|
1523
|
+
async function writePending(cwd, ids) {
|
|
1524
|
+
const path4 = join6(cwd, PENDING_FILE);
|
|
1525
|
+
const pending = [...new Set(ids)].sort();
|
|
1526
|
+
if (pending.length === 0) {
|
|
1527
|
+
await rm(path4, { force: true });
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
await mkdir3(dirname3(path4), { recursive: true });
|
|
1531
|
+
await writeFile2(path4, `${JSON.stringify({ pending }, null, 2)}
|
|
1532
|
+
`, "utf8");
|
|
1310
1533
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if (!status) return 0;
|
|
1314
|
-
return status.split("\n").filter(Boolean).length;
|
|
1534
|
+
function noteUrl(upstreamGitHubUrl, commit, id) {
|
|
1535
|
+
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${NOTES_DIR}/${id}/README.md` : void 0;
|
|
1315
1536
|
}
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1537
|
+
function formatNotesLine(total, open, arrived, options = {}) {
|
|
1538
|
+
const parts = [open === 0 ? `all ${total} handled` : `${Math.max(0, total - open)} of ${total} handled`];
|
|
1539
|
+
if (arrived) parts.push(`${arrived} ${options.dryRun ? "arrive" : "arrived"} with this sync`);
|
|
1540
|
+
if (open > 0) parts.push(`${open} open: pnpm cella migrate`);
|
|
1541
|
+
return `migration notes: ${parts.join(" \xB7 ")}`;
|
|
1319
1542
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1543
|
+
|
|
1544
|
+
// src/utils/overrides.ts
|
|
1545
|
+
function isUnderAnyFolder(filePath, folders) {
|
|
1546
|
+
return folders.some((folder) => {
|
|
1547
|
+
const entry = folder.replace(/\/+$/, "");
|
|
1548
|
+
return filePath === entry || filePath.startsWith(`${entry}/`);
|
|
1549
|
+
});
|
|
1326
1550
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1551
|
+
function isIgnored(filePath, config) {
|
|
1552
|
+
return isUpstreamOnly(filePath) || isUnderAnyFolder(filePath, config.overrides?.ignored || []);
|
|
1329
1553
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1554
|
+
function isPinned(filePath, config) {
|
|
1555
|
+
if (isManagedFile(filePath)) return true;
|
|
1556
|
+
return isUnderAnyFolder(filePath, config.overrides?.pinned || []);
|
|
1332
1557
|
}
|
|
1333
|
-
|
|
1334
|
-
const
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
if (
|
|
1558
|
+
function groupIgnoredUpstreamChanges(files, ignored) {
|
|
1559
|
+
const entries = [...new Set(ignored.map((entry) => entry.replace(/\/+$/, "")).filter(Boolean))];
|
|
1560
|
+
const mostSpecificFirst = [...entries].sort((a, b) => b.length - a.length);
|
|
1561
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1562
|
+
for (const file of files) {
|
|
1563
|
+
if (!file.upstreamOnly || isManagedFile(file.path) || isGeneratedFile(file.path)) continue;
|
|
1564
|
+
const entry = mostSpecificFirst.find((candidate) => isUnderAnyFolder(file.path, [candidate]));
|
|
1565
|
+
if (!entry) continue;
|
|
1566
|
+
const group = groups.get(entry) ?? { entry, paths: [], added: 0, deleted: 0 };
|
|
1567
|
+
group.paths.push(file.path);
|
|
1568
|
+
if (!file.existsInFork) group.added++;
|
|
1569
|
+
if (!file.existsInUpstream) group.deleted++;
|
|
1570
|
+
groups.set(entry, group);
|
|
1344
1571
|
}
|
|
1345
|
-
|
|
1572
|
+
for (const group of groups.values()) group.paths.sort();
|
|
1573
|
+
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
1346
1574
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
return { hash, message, date };
|
|
1575
|
+
function isPinnedForSync(filePath, config, unpinned) {
|
|
1576
|
+
if (isManagedFile(filePath)) return true;
|
|
1577
|
+
if (unpinned) return false;
|
|
1578
|
+
return isPinned(filePath, config);
|
|
1352
1579
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
return Number.parseInt(output.trim(), 10) || 0;
|
|
1580
|
+
function hasGlobChars(entry) {
|
|
1581
|
+
return entry.includes("*") || entry.includes("?");
|
|
1356
1582
|
}
|
|
1357
|
-
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
if (!line) continue;
|
|
1367
|
-
const [hash, message, date] = line.split("");
|
|
1368
|
-
if (!hash || !message || !date) continue;
|
|
1369
|
-
commits.push({ hash, message, date });
|
|
1370
|
-
}
|
|
1371
|
-
const normalizedSkip = Math.max(0, skip);
|
|
1372
|
-
const normalizedLimit = typeof limit === "number" && limit > 0 ? limit : void 0;
|
|
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);
|
|
1417
|
-
}
|
|
1418
|
-
async function removeWorktree(cwd, worktreePath) {
|
|
1419
|
-
await git(["worktree", "remove", worktreePath, "--force"], cwd, { ignoreErrors: true });
|
|
1420
|
-
await git(["worktree", "prune"], cwd, { ignoreErrors: true });
|
|
1421
|
-
}
|
|
1422
|
-
async function listWorktrees(cwd) {
|
|
1423
|
-
const output = await git(["worktree", "list", "--porcelain"], cwd);
|
|
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;
|
|
1432
|
-
}
|
|
1433
|
-
async function merge(cwd, ref, options = {}) {
|
|
1434
|
-
const args = ["merge", "--no-ff", ref];
|
|
1435
|
-
if (options.noCommit) args.push("--no-commit");
|
|
1436
|
-
if (options.noEdit) args.push("--no-edit");
|
|
1437
|
-
try {
|
|
1438
|
-
await git(args, cwd, { skipEditor: true });
|
|
1439
|
-
return { success: true, conflicts: [] };
|
|
1440
|
-
} catch (error) {
|
|
1441
|
-
const conflicts = await getConflictedFiles(cwd);
|
|
1442
|
-
if (conflicts.length > 0) {
|
|
1443
|
-
return { success: false, conflicts };
|
|
1444
|
-
}
|
|
1445
|
-
throw error;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
async function getConflictedFiles(cwd) {
|
|
1449
|
-
const output = await git(["diff", "--name-only", "--diff-filter=U"], cwd, { ignoreErrors: true });
|
|
1450
|
-
if (!output) return [];
|
|
1451
|
-
return output.split("\n").filter(Boolean);
|
|
1452
|
-
}
|
|
1453
|
-
async function mergeAbort(cwd) {
|
|
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
|
|
1583
|
+
function validateOverrides(config, forkPath) {
|
|
1584
|
+
const warnings = [];
|
|
1585
|
+
const pinned = config.overrides?.pinned || [];
|
|
1586
|
+
for (const entry of pinned) {
|
|
1587
|
+
if (hasGlobChars(entry)) {
|
|
1588
|
+
warnings.push({
|
|
1589
|
+
type: "pinned-glob",
|
|
1590
|
+
pattern: entry,
|
|
1591
|
+
message: `pin uses glob (not supported, use a path or folder): ${entry}`
|
|
1486
1592
|
});
|
|
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
|
|
1593
|
+
} else if (!existsSync5(join7(forkPath, entry))) {
|
|
1594
|
+
warnings.push({
|
|
1595
|
+
type: "pinned-not-found",
|
|
1596
|
+
pattern: entry,
|
|
1597
|
+
message: `pin not found: ${entry}`
|
|
1496
1598
|
});
|
|
1497
1599
|
}
|
|
1498
1600
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
if (!
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
deletions: d === "-" ? null : Number(d)
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
return stat;
|
|
1517
|
-
}
|
|
1518
|
-
async function getFileHashesAtRef(cwd, ref) {
|
|
1519
|
-
const output = await git(["ls-tree", "-r", ref], cwd);
|
|
1520
|
-
const hashes = /* @__PURE__ */ new Map();
|
|
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);
|
|
1601
|
+
const ignored = config.overrides?.ignored || [];
|
|
1602
|
+
for (const entry of ignored) {
|
|
1603
|
+
if (hasGlobChars(entry)) {
|
|
1604
|
+
warnings.push({
|
|
1605
|
+
type: "ignored-not-found",
|
|
1606
|
+
pattern: entry,
|
|
1607
|
+
message: `ignored entry uses glob (not supported, use a path or folder): ${entry}`
|
|
1608
|
+
});
|
|
1609
|
+
} else if (!existsSync5(join7(forkPath, entry.replace(/\/+$/, "")))) {
|
|
1610
|
+
warnings.push({
|
|
1611
|
+
type: "ignored-not-found",
|
|
1612
|
+
pattern: entry,
|
|
1613
|
+
message: `ignored entry not found: ${entry}`
|
|
1614
|
+
});
|
|
1528
1615
|
}
|
|
1529
1616
|
}
|
|
1530
|
-
return
|
|
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));
|
|
1617
|
+
return warnings;
|
|
1561
1618
|
}
|
|
1562
|
-
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
await unlink(fullPath);
|
|
1566
|
-
await cleanupEmptyParentDirs(cwd, dirname2(filePath));
|
|
1567
|
-
} catch {
|
|
1619
|
+
function printWarnings(warnings) {
|
|
1620
|
+
for (const warning of warnings) {
|
|
1621
|
+
console.info(`${warningMark} ${warning.message}`);
|
|
1568
1622
|
}
|
|
1569
1623
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
const fullPath = join8(cwd, relativePath);
|
|
1577
|
-
try {
|
|
1578
|
-
const entries = await readdir(fullPath);
|
|
1579
|
-
if (entries.length === 0) {
|
|
1580
|
-
await rmdir(fullPath);
|
|
1581
|
-
await cleanupEmptyParentDirs(cwd, dirname2(relativePath));
|
|
1582
|
-
}
|
|
1583
|
-
} catch {
|
|
1624
|
+
|
|
1625
|
+
// src/cli.ts
|
|
1626
|
+
function readOptions(opts) {
|
|
1627
|
+
const scope = typeof opts.scope === "string" ? opts.scope : void 0;
|
|
1628
|
+
if (scope && scope !== "all" && scope !== "risk" && scope !== "protected") {
|
|
1629
|
+
throw new Error(`invalid --scope '${scope}'. expected one of: all, risk, protected`);
|
|
1584
1630
|
}
|
|
1631
|
+
const normalizedScope = scope;
|
|
1632
|
+
return {
|
|
1633
|
+
logFile: opts.log === true,
|
|
1634
|
+
verbose: opts.verbose === true,
|
|
1635
|
+
list: opts.list === true,
|
|
1636
|
+
json: opts.json === true,
|
|
1637
|
+
diff: typeof opts.diff === "string" ? opts.diff : void 0,
|
|
1638
|
+
openDiff: typeof opts.openDiff === "string" ? opts.openDiff : void 0,
|
|
1639
|
+
scope: normalizedScope,
|
|
1640
|
+
fork: typeof opts.fork === "string" ? opts.fork : void 0,
|
|
1641
|
+
hard: opts.hard === true,
|
|
1642
|
+
unpinned: opts.unpinned === true,
|
|
1643
|
+
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
1644
|
+
ref: typeof opts.ref === "string" && opts.ref ? opts.ref : void 0,
|
|
1645
|
+
force: opts.force === true,
|
|
1646
|
+
checkOverrides: opts.checkOverrides === true,
|
|
1647
|
+
coverage: opts.coverage === true,
|
|
1648
|
+
all: opts.all === true,
|
|
1649
|
+
show: typeof opts.show === "string" ? opts.show : void 0,
|
|
1650
|
+
extract: typeof opts.extract === "string" ? opts.extract : void 0,
|
|
1651
|
+
mark: Array.isArray(opts.mark) ? opts.mark.filter((id) => typeof id === "string") : void 0
|
|
1652
|
+
};
|
|
1585
1653
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1654
|
+
var serviceDefinitions = [
|
|
1655
|
+
{
|
|
1656
|
+
name: "analyze",
|
|
1657
|
+
description: "dry run to see what would change on sync",
|
|
1658
|
+
options: [
|
|
1659
|
+
{ flags: "--log", description: "write complete file list to cella-sync.log" },
|
|
1660
|
+
{ flags: "--list", description: "non-interactive output for tooling (one file per line)" },
|
|
1661
|
+
{ flags: "--json", description: "machine-readable output for tooling/agents" },
|
|
1662
|
+
{ flags: "--scope <scope>", description: "analyze scope for --list/--json: all|risk|protected" },
|
|
1663
|
+
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
|
|
1664
|
+
{ flags: "--ref <ref>", description: "pin the upstream commit for this run: sha, release tag or branch" },
|
|
1665
|
+
{ flags: "--diff <path>", description: "print unified diff for one file, then exit" },
|
|
1666
|
+
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
1667
|
+
],
|
|
1668
|
+
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
name: "sync",
|
|
1672
|
+
description: "merge upstream changes into your app",
|
|
1673
|
+
options: [
|
|
1674
|
+
{ flags: "--log", description: "write complete file list to cella-sync.log" },
|
|
1675
|
+
{ flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" },
|
|
1676
|
+
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
1677
|
+
{ flags: "--track <mode>", description: "override upstream tracking for this run: release|branch" },
|
|
1678
|
+
{ flags: "--ref <ref>", description: "pin the upstream commit for this run: sha, release tag or branch" }
|
|
1679
|
+
],
|
|
1680
|
+
includeInMenu: (context) => !context.isUpstreamRepo,
|
|
1681
|
+
menuDescription: () => "merge upstream changes + sync package.json"
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
name: "migrate",
|
|
1685
|
+
description: "list the upstream migration notes this app has not handled yet",
|
|
1686
|
+
options: [
|
|
1687
|
+
{ flags: "--all", description: "list every upstream note, handled or not" },
|
|
1688
|
+
{ flags: "--json", description: "machine-readable output for tooling/agents" },
|
|
1689
|
+
{ flags: "--show <id>", description: "print one note's README" },
|
|
1690
|
+
{ flags: "--extract <id>", description: "write one note's folder under node_modules/.cache to run its codemod" },
|
|
1691
|
+
{ flags: "--mark <ids...>", description: "record notes as handled" }
|
|
1692
|
+
],
|
|
1693
|
+
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
name: "audit",
|
|
1697
|
+
description: "check for outdated packages & vulnerabilities",
|
|
1698
|
+
options: [
|
|
1699
|
+
{ flags: "--list", description: "skip interactive update prompts after printing audit results" },
|
|
1700
|
+
{ flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
|
|
1701
|
+
{ flags: "--check-overrides", description: "check which pnpm.overrides are still needed" }
|
|
1702
|
+
]
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
name: "forks",
|
|
1706
|
+
description: "sync downstream to local fork repositories",
|
|
1707
|
+
options: [
|
|
1708
|
+
{ flags: "--fork <name>", description: "pre-select fork by name (skips fork selection prompt)" },
|
|
1709
|
+
{ flags: "--log", description: "write complete file list to cella-sync.log for each synced fork" },
|
|
1710
|
+
{ flags: "-V, --verbose", description: "show detailed output during operations" },
|
|
1711
|
+
{ flags: "--hard", description: "overwrite drifted files with upstream version (aggressive realignment)" }
|
|
1712
|
+
],
|
|
1713
|
+
includeInMenu: (context) => context.hasForks
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
name: "contributions",
|
|
1717
|
+
description: "pull and adopt changes from forks",
|
|
1718
|
+
options: [
|
|
1719
|
+
{ flags: "--fork <name>", description: "select a specific fork directly (skips fork selection)" },
|
|
1720
|
+
{ flags: "--list", description: "non-interactive output (one file per line)" },
|
|
1721
|
+
{ flags: "--json", description: "machine-readable JSON output for tooling/agents" },
|
|
1722
|
+
{ flags: "--diff <path>", description: "print the unified diff for a single contributed file, then exit" }
|
|
1723
|
+
],
|
|
1724
|
+
includeInMenu: (context) => context.hasForks
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
name: "stats",
|
|
1728
|
+
description: "count files by category and workspace package",
|
|
1729
|
+
options: [
|
|
1730
|
+
{ flags: "-V, --verbose", description: "show detailed output during operations" },
|
|
1731
|
+
{ flags: "--coverage", description: "regenerate test coverage before showing the stats summary" }
|
|
1732
|
+
]
|
|
1591
1733
|
}
|
|
1734
|
+
];
|
|
1735
|
+
async function getMenuContext(userConfig, forkPath) {
|
|
1736
|
+
return {
|
|
1737
|
+
hasForks: (userConfig.forks?.length ?? 0) > 0,
|
|
1738
|
+
isUpstreamRepo: JSON.parse(readFileSync(join8(forkPath, "package.json"), "utf8")).name === "cella"
|
|
1739
|
+
};
|
|
1592
1740
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1741
|
+
function buildServiceChoices(context) {
|
|
1742
|
+
const label = (name) => name.padEnd(14);
|
|
1743
|
+
const baseChoices = serviceDefinitions.filter((service) => service.includeInMenu?.(context) ?? true).map((service) => ({
|
|
1744
|
+
value: service.name,
|
|
1745
|
+
name: `${label(service.name)}${colors_default.dim(service.menuDescription?.(context) ?? service.description)}`
|
|
1746
|
+
}));
|
|
1747
|
+
return [
|
|
1748
|
+
...baseChoices,
|
|
1749
|
+
{ type: "separator", separator: "\u2500".repeat(40) },
|
|
1750
|
+
{ value: "exit", name: colors_default.red(`${label("exit")}${colors_default.dim("quit without doing anything")}`) }
|
|
1751
|
+
];
|
|
1603
1752
|
}
|
|
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;
|
|
1753
|
+
function addServiceCommand(program, definition, setSelection) {
|
|
1754
|
+
const command = program.command(definition.name).description(definition.description);
|
|
1755
|
+
for (const option of definition.options ?? []) {
|
|
1756
|
+
command.option(option.flags, option.description);
|
|
1614
1757
|
}
|
|
1758
|
+
command.action((opts) => {
|
|
1759
|
+
setSelection({
|
|
1760
|
+
service: definition.name,
|
|
1761
|
+
options: readOptions(opts)
|
|
1762
|
+
});
|
|
1763
|
+
});
|
|
1615
1764
|
}
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1765
|
+
function buildProgram(setSelection) {
|
|
1766
|
+
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(
|
|
1767
|
+
"after",
|
|
1768
|
+
[
|
|
1769
|
+
"",
|
|
1770
|
+
"Examples:",
|
|
1771
|
+
" $ cella analyze",
|
|
1772
|
+
" $ cella analyze --json --scope risk",
|
|
1773
|
+
" $ cella analyze --open-diff frontend/src/routes/index.tsx",
|
|
1774
|
+
" $ cella sync --hard",
|
|
1775
|
+
" $ cella sync --unpinned",
|
|
1776
|
+
" $ cella sync --track branch",
|
|
1777
|
+
" $ cella sync --ref 4f7d87c",
|
|
1778
|
+
" $ cella migrate --mark 20261002T0614-config-switch",
|
|
1779
|
+
" $ cella audit --check-overrides",
|
|
1780
|
+
" $ cella contributions --fork raak --json"
|
|
1781
|
+
].join("\n")
|
|
1782
|
+
);
|
|
1783
|
+
for (const definition of serviceDefinitions) {
|
|
1784
|
+
addServiceCommand(program, definition, setSelection);
|
|
1628
1785
|
}
|
|
1786
|
+
return program;
|
|
1629
1787
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
return true;
|
|
1634
|
-
} catch {
|
|
1635
|
-
return false;
|
|
1788
|
+
function parseCommandLine(argv) {
|
|
1789
|
+
if (argv.length <= 2) {
|
|
1790
|
+
return { options: readOptions({}) };
|
|
1636
1791
|
}
|
|
1792
|
+
let selection = { options: readOptions({}) };
|
|
1793
|
+
const program = buildProgram((nextSelection) => {
|
|
1794
|
+
selection = nextSelection;
|
|
1795
|
+
});
|
|
1796
|
+
program.parse(argv);
|
|
1797
|
+
return selection;
|
|
1637
1798
|
}
|
|
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
|
-
}
|
|
1799
|
+
async function promptForService(userConfig, forkPath) {
|
|
1800
|
+
const selected = await select({
|
|
1801
|
+
message: "choose a service:",
|
|
1802
|
+
choices: buildServiceChoices(await getMenuContext(userConfig, forkPath)),
|
|
1803
|
+
loop: false
|
|
1804
|
+
});
|
|
1805
|
+
if (selected === "exit") {
|
|
1806
|
+
console.info(colors_default.dim("exiting..."));
|
|
1807
|
+
console.info();
|
|
1808
|
+
process5.exit(0);
|
|
1678
1809
|
}
|
|
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";
|
|
1810
|
+
console.info();
|
|
1811
|
+
return selected;
|
|
1692
1812
|
}
|
|
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
|
-
}
|
|
1813
|
+
function buildRuntimeConfig(userConfig, forkPath, selection) {
|
|
1814
|
+
const { branchRef } = resolveUpstream(userConfig.settings);
|
|
1815
|
+
return {
|
|
1816
|
+
...userConfig,
|
|
1817
|
+
forkPath,
|
|
1818
|
+
upstreamRef: branchRef,
|
|
1819
|
+
service: selection.service ?? "analyze",
|
|
1820
|
+
...selection.options
|
|
1821
|
+
};
|
|
1709
1822
|
}
|
|
1710
|
-
async function
|
|
1711
|
-
|
|
1712
|
-
|
|
1823
|
+
async function parseCli(userConfig, forkPath) {
|
|
1824
|
+
const selection = parseCommandLine(process5.argv);
|
|
1825
|
+
if (selection.options.json || selection.options.diff) setJsonMode(true);
|
|
1826
|
+
printHeader();
|
|
1827
|
+
const warnings = validateOverrides(userConfig, forkPath);
|
|
1828
|
+
if (warnings.length > 0) {
|
|
1829
|
+
printWarnings(warnings);
|
|
1830
|
+
console.info();
|
|
1713
1831
|
}
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
const sources = [
|
|
1717
|
-
() => getStoredSyncRef(cwd),
|
|
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
|
-
}
|
|
1832
|
+
if (!selection.service) {
|
|
1833
|
+
selection.service = await promptForService(userConfig, forkPath);
|
|
1730
1834
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1835
|
+
return buildRuntimeConfig(userConfig, forkPath, selection);
|
|
1836
|
+
}
|
|
1734
1837
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1838
|
+
// src/services/analyze.ts
|
|
1839
|
+
import { basename as basename4 } from "path";
|
|
1738
1840
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
try {
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
async function getLegacySyncRef(cwd, headRef) {
|
|
1757
|
-
const storedRef = await getStoredSyncRef(cwd);
|
|
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;
|
|
1841
|
+
// src/utils/diff.ts
|
|
1842
|
+
import { execFileSync } from "child_process";
|
|
1843
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1844
|
+
import { tmpdir } from "os";
|
|
1845
|
+
import { basename as basename2, join as join9 } from "path";
|
|
1846
|
+
import process6 from "process";
|
|
1847
|
+
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
1848
|
+
const args = ["diff", "--no-color"];
|
|
1849
|
+
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
1850
|
+
args.push(range, "--", filePath);
|
|
1851
|
+
try {
|
|
1852
|
+
return execFileSync("git", args, { cwd, maxBuffer: 50 * 1024 * 1024 });
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
const stderr = error.stderr?.toString().trim();
|
|
1855
|
+
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
1763
1856
|
}
|
|
1764
|
-
return storedRef;
|
|
1765
1857
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1858
|
+
function escapeHtml(text) {
|
|
1859
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1860
|
+
}
|
|
1861
|
+
async function renderDiffPage(patch, meta) {
|
|
1862
|
+
const { preloadPatchDiff } = await import("@pierre/diffs/ssr");
|
|
1863
|
+
const { prerenderedHTML } = await preloadPatchDiff({
|
|
1864
|
+
patch,
|
|
1865
|
+
options: {
|
|
1866
|
+
diffStyle: "split",
|
|
1867
|
+
themeType: "system",
|
|
1868
|
+
lineDiffType: "word-alt",
|
|
1869
|
+
hunkSeparators: "line-info",
|
|
1870
|
+
overflow: "wrap",
|
|
1871
|
+
disableFileHeader: true
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
const title = `${escapeHtml(meta.filePath)} \xB7 ${escapeHtml(meta.srcLabel)} vs ${escapeHtml(meta.dstLabel)}`;
|
|
1875
|
+
const note = meta.note ? `<span class="note">\xB7 ${escapeHtml(meta.note)}</span>` : "";
|
|
1876
|
+
return `<!doctype html>
|
|
1877
|
+
<html lang="en">
|
|
1878
|
+
<head>
|
|
1879
|
+
<meta charset="utf-8">
|
|
1880
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1881
|
+
<title>${title}</title>
|
|
1882
|
+
<style>
|
|
1883
|
+
:root { color-scheme: light dark; }
|
|
1884
|
+
* { box-sizing: border-box; }
|
|
1885
|
+
body {
|
|
1886
|
+
margin: 0;
|
|
1887
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
1888
|
+
background: light-dark(#fafafa, #111);
|
|
1889
|
+
color: light-dark(#111, #eee);
|
|
1771
1890
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1891
|
+
header {
|
|
1892
|
+
display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap;
|
|
1893
|
+
padding: 0.9rem 1.25rem;
|
|
1894
|
+
border-bottom: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
1895
|
+
position: sticky; top: 0;
|
|
1896
|
+
background: inherit;
|
|
1775
1897
|
}
|
|
1776
|
-
|
|
1898
|
+
header .brand { font-weight: 600; color: light-dark(#0891b2, #22d3ee); }
|
|
1899
|
+
header code { font-family: ui-monospace, monospace; font-size: 0.9rem; }
|
|
1900
|
+
header .sides { margin-left: auto; font-size: 0.8rem; opacity: 0.7; }
|
|
1901
|
+
main { max-width: 1400px; margin: 1rem auto; padding: 0 1rem 2rem; }
|
|
1902
|
+
.diff-shell {
|
|
1903
|
+
border: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
1904
|
+
border-radius: 8px; overflow: hidden;
|
|
1905
|
+
background: light-dark(#fff, #161616);
|
|
1906
|
+
}
|
|
1907
|
+
</style>
|
|
1908
|
+
</head>
|
|
1909
|
+
<body>
|
|
1910
|
+
<header>
|
|
1911
|
+
<span class="brand">\u29C8 cella diff</span>
|
|
1912
|
+
<code>${escapeHtml(meta.filePath)}</code>
|
|
1913
|
+
<span class="sides">${escapeHtml(meta.srcLabel)} \u2190 \u2192 ${escapeHtml(meta.dstLabel)} ${note}</span>
|
|
1914
|
+
</header>
|
|
1915
|
+
<main>
|
|
1916
|
+
<!-- Declarative shadow DOM: the library's CSS targets :host, which only
|
|
1917
|
+
matches inside a shadow root (its browser component renders into one). -->
|
|
1918
|
+
<div class="diff-shell">
|
|
1919
|
+
<template shadowrootmode="open">
|
|
1920
|
+
${prerenderedHTML}
|
|
1921
|
+
</template>
|
|
1922
|
+
</div>
|
|
1923
|
+
</main>
|
|
1924
|
+
</body>
|
|
1925
|
+
</html>`;
|
|
1777
1926
|
}
|
|
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);
|
|
1927
|
+
function openWithDefaultApp(target) {
|
|
1787
1928
|
try {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1929
|
+
if (process6.platform === "win32") {
|
|
1930
|
+
execFileSync("rundll32", ["url.dll,FileProtocolHandler", target], { stdio: "ignore" });
|
|
1931
|
+
} else {
|
|
1932
|
+
execFileSync(process6.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
1933
|
+
}
|
|
1934
|
+
} catch {
|
|
1791
1935
|
}
|
|
1792
1936
|
}
|
|
1937
|
+
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
1938
|
+
const html = await renderDiffPage(patch, meta);
|
|
1939
|
+
const dir = join9(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
1940
|
+
mkdirSync(dir, { recursive: true });
|
|
1941
|
+
const outPath = join9(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
1942
|
+
writeFileSync2(outPath, html);
|
|
1943
|
+
openWithDefaultApp(outPath);
|
|
1944
|
+
return outPath;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// src/services/merge-engine.ts
|
|
1948
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1949
|
+
import { rm as rm2 } from "fs/promises";
|
|
1950
|
+
import { join as join11 } from "path";
|
|
1793
1951
|
|
|
1794
1952
|
// src/utils/cleanup.ts
|
|
1953
|
+
import { createHash } from "crypto";
|
|
1954
|
+
import { existsSync as existsSync6, rmSync } from "fs";
|
|
1955
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1956
|
+
import { basename as basename3, join as join10, resolve as resolve4 } from "path";
|
|
1957
|
+
import process7 from "process";
|
|
1795
1958
|
var WORKTREE_PREFIXES = {
|
|
1796
1959
|
sync: "cella-sync-",
|
|
1797
1960
|
view: "cella-view-"
|
|
1798
1961
|
};
|
|
1799
1962
|
function buildWorktreePath(kind, repoPath) {
|
|
1800
|
-
|
|
1963
|
+
const key = createHash("sha256").update(resolve4(repoPath)).digest("hex").slice(0, 8);
|
|
1964
|
+
return join10(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
|
|
1965
|
+
}
|
|
1966
|
+
function legacyWorktreePath(kind, repoPath) {
|
|
1967
|
+
return join10(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
1801
1968
|
}
|
|
1802
1969
|
function getWorktreePath(repoPath) {
|
|
1803
1970
|
return buildWorktreePath("sync", repoPath);
|
|
@@ -1815,22 +1982,21 @@ function unregisterWorktree() {
|
|
|
1815
1982
|
}
|
|
1816
1983
|
async function cleanupWorktree(repoPath, worktreePath) {
|
|
1817
1984
|
await removeWorktree(repoPath, worktreePath);
|
|
1818
|
-
if (
|
|
1985
|
+
if (existsSync6(worktreePath)) {
|
|
1819
1986
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
1820
1987
|
}
|
|
1821
1988
|
unregisterWorktree();
|
|
1822
1989
|
}
|
|
1823
1990
|
async function cleanupLeftoverWorktrees(repoPath) {
|
|
1824
1991
|
for (const kind of Object.keys(WORKTREE_PREFIXES)) {
|
|
1825
|
-
const worktreePath
|
|
1826
|
-
|
|
1827
|
-
await cleanupWorktree(repoPath, worktreePath);
|
|
1992
|
+
for (const worktreePath of [buildWorktreePath(kind, repoPath), legacyWorktreePath(kind, repoPath)]) {
|
|
1993
|
+
if (existsSync6(worktreePath)) await cleanupWorktree(repoPath, worktreePath);
|
|
1828
1994
|
}
|
|
1829
1995
|
}
|
|
1830
1996
|
const prefixes = Object.values(WORKTREE_PREFIXES);
|
|
1831
1997
|
const worktrees = await listWorktrees(repoPath);
|
|
1832
1998
|
for (const wt of worktrees) {
|
|
1833
|
-
if (prefixes.some((prefix) => wt.includes(prefix)) && !
|
|
1999
|
+
if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync6(wt)) {
|
|
1834
2000
|
await removeWorktree(repoPath, wt);
|
|
1835
2001
|
}
|
|
1836
2002
|
}
|
|
@@ -1861,6 +2027,51 @@ function registerSignalHandlers() {
|
|
|
1861
2027
|
cleanupRegistered = true;
|
|
1862
2028
|
}
|
|
1863
2029
|
|
|
2030
|
+
// src/utils/versions.ts
|
|
2031
|
+
function parseComparableVersion(version) {
|
|
2032
|
+
const trimmed = version.trim();
|
|
2033
|
+
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("||")) {
|
|
2034
|
+
return null;
|
|
2035
|
+
}
|
|
2036
|
+
const match = trimmed.match(
|
|
2037
|
+
/^(?:\^|~|>=|<=|>|<|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
2038
|
+
);
|
|
2039
|
+
if (!match) return null;
|
|
2040
|
+
return {
|
|
2041
|
+
major: Number(match[1]),
|
|
2042
|
+
minor: Number(match[2] ?? 0),
|
|
2043
|
+
patch: Number(match[3] ?? 0)
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
function compareVersions(left, right) {
|
|
2047
|
+
if (left.major !== right.major) return left.major - right.major;
|
|
2048
|
+
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
2049
|
+
return left.patch - right.patch;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// src/utils/cli-version.ts
|
|
2053
|
+
var CLI_PACKAGE = "@cellajs/cli";
|
|
2054
|
+
async function readUpstreamCliRange(repoPath, ref) {
|
|
2055
|
+
const source = await git(["show", `${ref}:package.json`], repoPath, { ignoreErrors: true });
|
|
2056
|
+
if (!source) return null;
|
|
2057
|
+
try {
|
|
2058
|
+
const pkg = JSON.parse(source);
|
|
2059
|
+
return pkg.devDependencies?.[CLI_PACKAGE] ?? pkg.dependencies?.[CLI_PACKAGE] ?? null;
|
|
2060
|
+
} catch {
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
function cliVersionMismatch(running, range) {
|
|
2065
|
+
if (/^\s*(<|>(?!=))/.test(range)) return null;
|
|
2066
|
+
const required = parseComparableVersion(range);
|
|
2067
|
+
const current = parseComparableVersion(running);
|
|
2068
|
+
if (!required || !current || compareVersions(current, required) >= 0) return null;
|
|
2069
|
+
return [
|
|
2070
|
+
`upstream needs ${CLI_PACKAGE} ${range}, this is ${running}: an older CLI misses fixes to the sync itself.`,
|
|
2071
|
+
` update it, commit, then rerun: pnpm add -D -w '${CLI_PACKAGE}@${range}'`
|
|
2072
|
+
].join("\n");
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1864
2075
|
// src/utils/upstream-overrides.ts
|
|
1865
2076
|
import { Node, Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1866
2077
|
var LIST_KEYS = ["pinned", "ignored"];
|
|
@@ -2359,6 +2570,20 @@ function formatLocalCheckoutDetail(forkPath, branch, headSha, changeCount, upstr
|
|
|
2359
2570
|
const workingTree = changeCount === 0 ? " working tree clean" : ` ${changeCount} uncommitted change${changeCount === 1 ? "" : "s"} in working tree, not included in analysis`;
|
|
2360
2571
|
return [`${forkPath}`, ` ${branch} @ ${headSha}${tracking}`, workingTree].join("\n");
|
|
2361
2572
|
}
|
|
2573
|
+
async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
|
|
2574
|
+
const resolved = await resolveUpstreamCommit(forkPath, remoteName, ref);
|
|
2575
|
+
if (!resolved) {
|
|
2576
|
+
throw new Error(
|
|
2577
|
+
`--ref '${ref}' does not resolve to a commit. Pass a commit sha, a release tag (v*) or a branch on ${config.settings.upstreamUrl}.`
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
if (!await isPublishedUpstream(forkPath, remoteName, branchRef, resolved.sha)) {
|
|
2581
|
+
throw new Error(
|
|
2582
|
+
`--ref '${ref}' (${resolved.sha.slice(0, 7)}) is not on ${branchRef} or in an upstream release (v*). Sync only to commits upstream has published there.`
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
return resolved.release ? { ref: resolved.release.ref, releaseTag: resolved.release.tag } : { ref: resolved.sha };
|
|
2586
|
+
}
|
|
2362
2587
|
async function prepareUpstream(config, onProgress, onStep) {
|
|
2363
2588
|
const { forkPath } = config;
|
|
2364
2589
|
onProgress?.("checking local checkout...");
|
|
@@ -2378,7 +2603,12 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2378
2603
|
await fetch2(forkPath, remoteName);
|
|
2379
2604
|
let upstreamRef = branchRef;
|
|
2380
2605
|
let releaseTag;
|
|
2381
|
-
if (
|
|
2606
|
+
if (config.ref) {
|
|
2607
|
+
await fetchUpstreamTags(forkPath, remoteName);
|
|
2608
|
+
const pinned = await resolvePinnedRef(forkPath, config, remoteName, branchRef, config.ref);
|
|
2609
|
+
upstreamRef = pinned.ref;
|
|
2610
|
+
releaseTag = pinned.releaseTag;
|
|
2611
|
+
} else if (track === "release") {
|
|
2382
2612
|
await fetchUpstreamTags(forkPath, remoteName);
|
|
2383
2613
|
const latest = await resolveLatestReleaseTag(forkPath, remoteName);
|
|
2384
2614
|
if (!latest) {
|
|
@@ -2389,14 +2619,30 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2389
2619
|
upstreamRef = latest.ref;
|
|
2390
2620
|
releaseTag = latest.tag;
|
|
2391
2621
|
}
|
|
2622
|
+
const upstreamLabel = releaseTag ?? (config.ref ? `--ref ${config.ref}` : upstreamRef);
|
|
2392
2623
|
config.upstreamRef = upstreamRef;
|
|
2393
|
-
onStep?.("remote configured", `${
|
|
2624
|
+
onStep?.("remote configured", `${upstreamLabel} \u2192 ${config.settings.upstreamUrl}`);
|
|
2625
|
+
const cliRange = await readUpstreamCliRange(forkPath, upstreamRef);
|
|
2626
|
+
const cliMismatch = cliRange ? cliVersionMismatch(VERSION, cliRange) : null;
|
|
2627
|
+
if (cliMismatch) throw new Error(cliMismatch);
|
|
2394
2628
|
onProgress?.("checking sync base...");
|
|
2395
2629
|
await ensureSyncBase(forkPath, "HEAD", upstreamRef);
|
|
2396
2630
|
const upstreamGitHubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2397
2631
|
const aggressive = config.hard === true || config.unpinned === true;
|
|
2398
|
-
const
|
|
2632
|
+
const syncPoint = (await getEffectiveMergeBase(forkPath, "HEAD", upstreamRef)).base;
|
|
2633
|
+
const mergeBase = aggressive ? await getMergeBase(forkPath, "HEAD", upstreamRef) : syncPoint;
|
|
2399
2634
|
const upstreamCommit = await getCommitInfo(forkPath, upstreamRef);
|
|
2635
|
+
if (upstreamCommit.hash !== syncPoint && await isAncestor(forkPath, upstreamCommit.hash, syncPoint)) {
|
|
2636
|
+
const [upstreamShort, syncShort] = await Promise.all([
|
|
2637
|
+
getShortSha(forkPath, upstreamCommit.hash),
|
|
2638
|
+
getShortSha(forkPath, syncPoint)
|
|
2639
|
+
]);
|
|
2640
|
+
const hint = config.ref ? "Pick a newer --ref." : releaseTag ? `Nothing to sync until a release past ${syncShort}; to follow the tip instead, run with --track branch.` : `Nothing to sync until '${branchRef}' moves past ${syncShort}.`;
|
|
2641
|
+
throw new Error(
|
|
2642
|
+
`upstream ${upstreamLabel} (${upstreamShort}) is behind the last sync point ${syncShort}: the fork already has it, and syncing it would revert the upstream changes after it.
|
|
2643
|
+
${hint}`
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2400
2646
|
const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
|
|
2401
2647
|
const commitListMax = 50;
|
|
2402
2648
|
const commitSkip = commitCount > commitListMax ? commitCount - commitListMax : 0;
|
|
@@ -2436,9 +2682,34 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2436
2682
|
return {};
|
|
2437
2683
|
}
|
|
2438
2684
|
}
|
|
2685
|
+
async function readNotesInRange(config, ctx) {
|
|
2686
|
+
const { forkPath } = config;
|
|
2687
|
+
try {
|
|
2688
|
+
const aggressive = config.hard === true || config.unpinned === true;
|
|
2689
|
+
const syncBase = aggressive ? (await getEffectiveMergeBase(forkPath, "HEAD", ctx.upstreamRef)).base : ctx.mergeBase;
|
|
2690
|
+
const [total, arrived, pending] = await Promise.all([
|
|
2691
|
+
listNoteIds(forkPath, ctx.upstreamRef),
|
|
2692
|
+
arrivedNoteIds(forkPath, syncBase, ctx.upstreamRef),
|
|
2693
|
+
readPending(forkPath, syncBase)
|
|
2694
|
+
]);
|
|
2695
|
+
return { total: total.length, arrived, pending: [.../* @__PURE__ */ new Set([...pending, ...arrived])] };
|
|
2696
|
+
} catch {
|
|
2697
|
+
return void 0;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function notesResult(notes) {
|
|
2701
|
+
return notes ? { migrationNotes: { total: notes.total, arrived: notes.arrived, open: notes.pending.length } } : {};
|
|
2702
|
+
}
|
|
2703
|
+
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2704
|
+
if (!existsSync7(join11(forkPath, NOTES_DIR))) return;
|
|
2705
|
+
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", NOTES_DIR], forkPath);
|
|
2706
|
+
await rm2(join11(forkPath, NOTES_DIR), { recursive: true, force: true });
|
|
2707
|
+
onStep?.("migration notes", `removed ${NOTES_DIR}/: notes stay upstream, list them with pnpm cella migrate`);
|
|
2708
|
+
}
|
|
2439
2709
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2440
2710
|
const { forkPath } = config;
|
|
2441
2711
|
const { upstreamRef, releaseTag, mergeBase, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2712
|
+
const notes = await readNotesInRange(config, ctx);
|
|
2442
2713
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2443
2714
|
forkPath,
|
|
2444
2715
|
upstreamRef,
|
|
@@ -2465,6 +2736,11 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2465
2736
|
await storeLastSyncRef(forkPath, upstreamCommit.hash);
|
|
2466
2737
|
await writeSyncManifest(forkPath, syncManifest);
|
|
2467
2738
|
await stagePath(forkPath, MANIFEST_FILE);
|
|
2739
|
+
if (notes) {
|
|
2740
|
+
await writePending(forkPath, notes.pending);
|
|
2741
|
+
await stagePath(forkPath, PENDING_FILE);
|
|
2742
|
+
}
|
|
2743
|
+
await removeUpstreamOnlyCopy(forkPath, onStep);
|
|
2468
2744
|
};
|
|
2469
2745
|
if (remainingConflicts.length > 0) {
|
|
2470
2746
|
await recordSyncPoint();
|
|
@@ -2488,12 +2764,14 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2488
2764
|
autoMergedFiles,
|
|
2489
2765
|
protectedConflicts,
|
|
2490
2766
|
...resultMeta(config, ctx),
|
|
2491
|
-
...await upstreamOnlyReports(config, ctx, analyzedFiles)
|
|
2767
|
+
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2768
|
+
...notesResult(notes)
|
|
2492
2769
|
};
|
|
2493
2770
|
}
|
|
2494
2771
|
async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
2495
2772
|
const { forkPath } = config;
|
|
2496
2773
|
const worktreePath = getWorktreePath(forkPath);
|
|
2774
|
+
const notes = await readNotesInRange(config, ctx);
|
|
2497
2775
|
onProgress?.("creating worktree in temp directory...");
|
|
2498
2776
|
await createWorktree(forkPath, worktreePath, "HEAD");
|
|
2499
2777
|
onStep?.("worktree created", worktreePath);
|
|
@@ -2527,7 +2805,8 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2527
2805
|
conflicts: analyzedFiles.filter((f) => f.status === "diverged").map((f) => f.path),
|
|
2528
2806
|
protectedConflicts: analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path),
|
|
2529
2807
|
...resultMeta(config, ctx),
|
|
2530
|
-
...reports
|
|
2808
|
+
...reports,
|
|
2809
|
+
...notesResult(notes)
|
|
2531
2810
|
};
|
|
2532
2811
|
}
|
|
2533
2812
|
async function runMergeEngine(config, options) {
|
|
@@ -2545,6 +2824,151 @@ async function runMergeEngine(config, options) {
|
|
|
2545
2824
|
}
|
|
2546
2825
|
}
|
|
2547
2826
|
|
|
2827
|
+
// src/services/migrate.ts
|
|
2828
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2829
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2830
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
2831
|
+
var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
|
|
2832
|
+
async function resolveNotesRef(config) {
|
|
2833
|
+
const { forkPath } = config;
|
|
2834
|
+
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2835
|
+
if (!ref) return null;
|
|
2836
|
+
const present = async () => !!await git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], forkPath, { ignoreErrors: true });
|
|
2837
|
+
if (!await present()) {
|
|
2838
|
+
await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
|
|
2839
|
+
await fetch2(forkPath, DEFAULT_UPSTREAM_REMOTE);
|
|
2840
|
+
if (!await present())
|
|
2841
|
+
throw new Error(`upstream commit ${ref.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2842
|
+
}
|
|
2843
|
+
return ref;
|
|
2844
|
+
}
|
|
2845
|
+
async function readNotesStatus(config) {
|
|
2846
|
+
try {
|
|
2847
|
+
const ref = await resolveNotesRef(config);
|
|
2848
|
+
if (!ref) return null;
|
|
2849
|
+
const [ids, pending] = await Promise.all([listNoteIds(config.forkPath, ref), readPending(config.forkPath, ref)]);
|
|
2850
|
+
return ids.length > 0 || pending.length > 0 ? { total: ids.length, open: pending.length } : null;
|
|
2851
|
+
} catch {
|
|
2852
|
+
return null;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
async function printMigrationNotesLine(config, result) {
|
|
2856
|
+
const notes = result?.migrationNotes;
|
|
2857
|
+
if (notes) {
|
|
2858
|
+
if (notes.total === 0) return;
|
|
2859
|
+
console.info();
|
|
2860
|
+
console.info(
|
|
2861
|
+
colors_default.dim(formatNotesLine(notes.total, notes.open, notes.arrived.length, { dryRun: config.service === "analyze" }))
|
|
2862
|
+
);
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
const status = await readNotesStatus(config);
|
|
2866
|
+
if (!status) return;
|
|
2867
|
+
console.info();
|
|
2868
|
+
console.info(colors_default.dim(formatNotesLine(status.total, status.open)));
|
|
2869
|
+
}
|
|
2870
|
+
function toJson(note, url, open) {
|
|
2871
|
+
return { ...note, kind: note.codemod ? "codemod" : "manual", open, url: url ?? null };
|
|
2872
|
+
}
|
|
2873
|
+
function printNote(index, note, url) {
|
|
2874
|
+
const tags = [
|
|
2875
|
+
note.codemod ? "codemod" : "manual",
|
|
2876
|
+
note.syncBreaking ? "sync-breaking" : null,
|
|
2877
|
+
note.clientCacheBump ? "cache-bump" : null
|
|
2878
|
+
].filter(Boolean).join(", ");
|
|
2879
|
+
console.info(`${index}. ${note.title} ${colors_default.dim(`[${tags}]`)}`);
|
|
2880
|
+
console.info(colors_default.dim(` id: ${note.id}`));
|
|
2881
|
+
if (note.summary) console.info(` ${note.summary}`);
|
|
2882
|
+
console.info(colors_default.dim(` read: pnpm cella migrate --show ${note.id}`));
|
|
2883
|
+
if (url) console.info(colors_default.dim(` ${url}`));
|
|
2884
|
+
if (note.codemod) console.info(colors_default.dim(` codemod: pnpm cella migrate --extract ${note.id}`));
|
|
2885
|
+
console.info();
|
|
2886
|
+
}
|
|
2887
|
+
async function extractNote(config, ref, id) {
|
|
2888
|
+
const { forkPath } = config;
|
|
2889
|
+
const note = await readNote(forkPath, ref, id);
|
|
2890
|
+
if (!note) throw new Error(`no migration note '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2891
|
+
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${NOTES_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2892
|
+
const target = join12(EXTRACT_DIR, id);
|
|
2893
|
+
for (const file of files) {
|
|
2894
|
+
const content = execFileSync2("git", ["show", `${ref}:${NOTES_DIR}/${id}/${file}`], {
|
|
2895
|
+
cwd: forkPath,
|
|
2896
|
+
maxBuffer: 50 * 1024 * 1024
|
|
2897
|
+
});
|
|
2898
|
+
const path4 = join12(forkPath, target, file);
|
|
2899
|
+
await mkdir4(dirname4(path4), { recursive: true });
|
|
2900
|
+
await writeFile3(path4, content);
|
|
2901
|
+
}
|
|
2902
|
+
console.info(`${colors_default.green("\u2713")} extracted ${files.length} file(s) to ${target}/`);
|
|
2903
|
+
console.info(colors_default.dim(` the README's ${NOTES_DIR}/${id}/ paths are this folder.`));
|
|
2904
|
+
if (note.codemod) console.info(colors_default.dim(` pnpm exec tsx ${target}/${note.codemod} inventory ${note.roots.join(" ")}`));
|
|
2905
|
+
}
|
|
2906
|
+
async function markNotes(config, ref, pending, ids) {
|
|
2907
|
+
const open = new Set(pending);
|
|
2908
|
+
const unknown = ids.filter((id) => !open.has(id));
|
|
2909
|
+
for (const id of ids) open.delete(id);
|
|
2910
|
+
await writePending(config.forkPath, [...open]);
|
|
2911
|
+
const marked = ids.length - unknown.length;
|
|
2912
|
+
if (marked > 0) console.info(`${colors_default.green("\u2713")} recorded ${marked} note(s) as handled`);
|
|
2913
|
+
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2914
|
+
console.info(colors_default.dim(formatNotesLine((await listNoteIds(config.forkPath, ref)).length, open.size)));
|
|
2915
|
+
}
|
|
2916
|
+
async function runMigrate(config) {
|
|
2917
|
+
const { forkPath } = config;
|
|
2918
|
+
const ref = await resolveNotesRef(config);
|
|
2919
|
+
if (!ref) {
|
|
2920
|
+
console.info(colors_default.dim("no sync point recorded yet: migration notes start with the first `pnpm cella sync`."));
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const pending = await readPending(forkPath, ref);
|
|
2924
|
+
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2925
|
+
const urlOf = (id) => noteUrl(githubUrl, ref, id);
|
|
2926
|
+
if (config.mark?.length) return markNotes(config, ref, pending, config.mark);
|
|
2927
|
+
if (config.extract) return extractNote(config, ref, config.extract);
|
|
2928
|
+
if (config.show) {
|
|
2929
|
+
const source = await git(["show", `${ref}:${NOTES_DIR}/${config.show}/README.md`], forkPath, {
|
|
2930
|
+
ignoreErrors: true
|
|
2931
|
+
});
|
|
2932
|
+
if (!source) throw new Error(`no migration note '${config.show}' at upstream ${ref.slice(0, 7)}`);
|
|
2933
|
+
const url = urlOf(config.show);
|
|
2934
|
+
if (url) console.info(colors_default.dim(url));
|
|
2935
|
+
console.info(source);
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
const ids = config.all ? await listNoteIds(forkPath, ref) : pending;
|
|
2939
|
+
const notes = (await Promise.all(ids.map((id) => readNote(forkPath, ref, id)))).map(
|
|
2940
|
+
(note, i) => note ?? {
|
|
2941
|
+
id: ids[i],
|
|
2942
|
+
title: `${ids[i]} (no longer upstream)`,
|
|
2943
|
+
summary: "",
|
|
2944
|
+
syncBreaking: false,
|
|
2945
|
+
clientCacheBump: false,
|
|
2946
|
+
roots: [],
|
|
2947
|
+
codemod: null
|
|
2948
|
+
}
|
|
2949
|
+
);
|
|
2950
|
+
const total = (await listNoteIds(forkPath, ref)).length;
|
|
2951
|
+
if (config.json) {
|
|
2952
|
+
const open = new Set(pending);
|
|
2953
|
+
const out = {
|
|
2954
|
+
upstreamCommit: ref,
|
|
2955
|
+
total,
|
|
2956
|
+
open: pending.length,
|
|
2957
|
+
notes: notes.map((note) => toJson(note, urlOf(note.id), open.has(note.id)))
|
|
2958
|
+
};
|
|
2959
|
+
writeStdout(JSON.stringify(out, null, 2));
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
console.info(colors_default.dim(formatNotesLine(total, pending.length)));
|
|
2963
|
+
if (notes.length === 0) return;
|
|
2964
|
+
console.info();
|
|
2965
|
+
for (const [i, note] of notes.entries()) printNote(i + 1, note, urlOf(note.id));
|
|
2966
|
+
if (pending.length > 0) {
|
|
2967
|
+
console.info(colors_default.dim("work each note's README, check with `pnpm check`, then record it:"));
|
|
2968
|
+
console.info(colors_default.dim(` pnpm cella migrate --mark ${pending.join(" ")}`));
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2548
2972
|
// src/services/analyze.ts
|
|
2549
2973
|
var scopeStatuses = {
|
|
2550
2974
|
all: /* @__PURE__ */ new Set(["ahead", "drifted", "diverged"]),
|
|
@@ -2649,6 +3073,7 @@ async function runAnalyze(config) {
|
|
|
2649
3073
|
printIgnoredUpstreamChanges(result);
|
|
2650
3074
|
printUpstreamOverrideChanges(result);
|
|
2651
3075
|
printMaskingPinWarning(result.files);
|
|
3076
|
+
await printMigrationNotesLine(config, result);
|
|
2652
3077
|
if (config.logFile) {
|
|
2653
3078
|
const logPath = writeLogFile(config.forkPath, result.files);
|
|
2654
3079
|
console.info();
|
|
@@ -3427,7 +3852,7 @@ async function checkOverrides(forkPath) {
|
|
|
3427
3852
|
|
|
3428
3853
|
// src/services/contributions.ts
|
|
3429
3854
|
import { readFileSync as readFileSync4 } from "fs";
|
|
3430
|
-
import { join as
|
|
3855
|
+
import { join as join16 } from "path";
|
|
3431
3856
|
import {
|
|
3432
3857
|
createPrompt,
|
|
3433
3858
|
isDownKey,
|
|
@@ -3442,7 +3867,7 @@ import {
|
|
|
3442
3867
|
import { select as select2 } from "@inquirer/prompts";
|
|
3443
3868
|
|
|
3444
3869
|
// src/services/contrib-core.ts
|
|
3445
|
-
import { dirname as
|
|
3870
|
+
import { dirname as dirname5, join as join15, resolve as resolve5 } from "path";
|
|
3446
3871
|
async function listFilesAtRef(repoPath, ref) {
|
|
3447
3872
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
3448
3873
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -3458,10 +3883,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
3458
3883
|
const baseFiles = await listFilesAtRef(repoPath, baseRef);
|
|
3459
3884
|
const baseDirs = /* @__PURE__ */ new Set(["."]);
|
|
3460
3885
|
for (const f of baseFiles) {
|
|
3461
|
-
let dir =
|
|
3886
|
+
let dir = dirname5(f);
|
|
3462
3887
|
while (dir !== ".") {
|
|
3463
3888
|
baseDirs.add(dir);
|
|
3464
|
-
dir =
|
|
3889
|
+
dir = dirname5(dir);
|
|
3465
3890
|
}
|
|
3466
3891
|
}
|
|
3467
3892
|
const files = [];
|
|
@@ -3474,7 +3899,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
3474
3899
|
if (inCella && !inFork) {
|
|
3475
3900
|
kind = "deleted";
|
|
3476
3901
|
} else if (!inCella && inFork) {
|
|
3477
|
-
if (!baseDirs.has(
|
|
3902
|
+
if (!baseDirs.has(dirname5(file.path))) continue;
|
|
3478
3903
|
kind = "created";
|
|
3479
3904
|
} else {
|
|
3480
3905
|
kind = "modified";
|
|
@@ -3505,8 +3930,8 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
3505
3930
|
}
|
|
3506
3931
|
async function buildContribBranch(repoPath, baseRef, forkRef, detection, forkName) {
|
|
3507
3932
|
const branch = `contrib/${forkName}`;
|
|
3508
|
-
const gitDir =
|
|
3509
|
-
const tmpIndex =
|
|
3933
|
+
const gitDir = resolve5(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
3934
|
+
const tmpIndex = join15(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
3510
3935
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
3511
3936
|
try {
|
|
3512
3937
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -3549,20 +3974,20 @@ ${commitBody}`;
|
|
|
3549
3974
|
}
|
|
3550
3975
|
|
|
3551
3976
|
// src/services/fork-utils.ts
|
|
3552
|
-
import { existsSync as
|
|
3553
|
-
import { dirname as
|
|
3977
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3978
|
+
import { dirname as dirname6, resolve as resolve6 } from "path";
|
|
3554
3979
|
async function resolveForkBasePath(repoPath) {
|
|
3555
3980
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
3556
|
-
return commonDir ?
|
|
3981
|
+
return commonDir ? dirname6(resolve6(repoPath, commonDir)) : repoPath;
|
|
3557
3982
|
}
|
|
3558
3983
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
3559
|
-
const resolvedPath =
|
|
3560
|
-
if (!resolvedPath.startsWith(
|
|
3984
|
+
const resolvedPath = resolve6(basePath, fork.localPath);
|
|
3985
|
+
if (!resolvedPath.startsWith(resolve6(basePath, ".."))) {
|
|
3561
3986
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
3562
3987
|
}
|
|
3563
|
-
if (!
|
|
3564
|
-
if (!
|
|
3565
|
-
if (requireConfig && !
|
|
3988
|
+
if (!existsSync9(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
3989
|
+
if (!existsSync9(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
3990
|
+
if (requireConfig && !existsSync9(`${resolvedPath}/${CONFIG_FILE}`)) {
|
|
3566
3991
|
return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
|
|
3567
3992
|
}
|
|
3568
3993
|
return { fork, resolvedPath, valid: true };
|
|
@@ -3580,7 +4005,7 @@ async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
|
3580
4005
|
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
3581
4006
|
}
|
|
3582
4007
|
function shouldApplyContributionsUnstaged(repoPath) {
|
|
3583
|
-
return JSON.parse(readFileSync4(
|
|
4008
|
+
return JSON.parse(readFileSync4(join16(repoPath, "package.json"), "utf8")).name === "cella";
|
|
3584
4009
|
}
|
|
3585
4010
|
var contribPrompt = createPrompt((config, done) => {
|
|
3586
4011
|
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
@@ -3925,7 +4350,7 @@ async function runContributions(config) {
|
|
|
3925
4350
|
}
|
|
3926
4351
|
|
|
3927
4352
|
// src/services/forks.ts
|
|
3928
|
-
import { resolve as
|
|
4353
|
+
import { resolve as resolve7 } from "path";
|
|
3929
4354
|
import process8 from "process";
|
|
3930
4355
|
import { Separator as Separator2, select as select4 } from "@inquirer/prompts";
|
|
3931
4356
|
|
|
@@ -3968,7 +4393,6 @@ function listOpenSyncPrs(cwd, prefix) {
|
|
|
3968
4393
|
}
|
|
3969
4394
|
function buildMergeArgs(ref, options = {}) {
|
|
3970
4395
|
const args = ["pr", "merge", ref, "--squash"];
|
|
3971
|
-
if (options.auto) args.push("--auto");
|
|
3972
4396
|
if (options.deleteBranch) args.push("--delete-branch");
|
|
3973
4397
|
return args;
|
|
3974
4398
|
}
|
|
@@ -3980,12 +4404,12 @@ function closePr(cwd, ref) {
|
|
|
3980
4404
|
}
|
|
3981
4405
|
|
|
3982
4406
|
// src/services/packages.ts
|
|
3983
|
-
import { existsSync as
|
|
3984
|
-
import { join as
|
|
4407
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4408
|
+
import { join as join18 } from "path";
|
|
3985
4409
|
|
|
3986
4410
|
// src/services/package-usage.ts
|
|
3987
|
-
import { existsSync as
|
|
3988
|
-
import { join as
|
|
4411
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, statSync } from "fs";
|
|
4412
|
+
import { join as join17 } from "path";
|
|
3989
4413
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
3990
4414
|
function escapeRegExp(value) {
|
|
3991
4415
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4001,7 +4425,7 @@ function readSearchable(filePath) {
|
|
|
4001
4425
|
}
|
|
4002
4426
|
function fileSource(forkPath, path4) {
|
|
4003
4427
|
let text;
|
|
4004
|
-
return { path: path4, label: path4, text: () => text ??= readSearchable(
|
|
4428
|
+
return { path: path4, label: path4, text: () => text ??= readSearchable(join17(forkPath, path4)) };
|
|
4005
4429
|
}
|
|
4006
4430
|
async function readScriptsAtRef(forkPath, ref, path4) {
|
|
4007
4431
|
try {
|
|
@@ -4013,7 +4437,7 @@ async function readScriptsAtRef(forkPath, ref, path4) {
|
|
|
4013
4437
|
async function scriptSources(forkPath, upstreamRef, baseRef, path4) {
|
|
4014
4438
|
let forkScripts;
|
|
4015
4439
|
try {
|
|
4016
|
-
forkScripts = JSON.parse(readFileSync5(
|
|
4440
|
+
forkScripts = JSON.parse(readFileSync5(join17(forkPath, path4), "utf-8")).scripts ?? {};
|
|
4017
4441
|
} catch {
|
|
4018
4442
|
return [];
|
|
4019
4443
|
}
|
|
@@ -4023,9 +4447,9 @@ async function scriptSources(forkPath, upstreamRef, baseRef, path4) {
|
|
|
4023
4447
|
}
|
|
4024
4448
|
function binNames(forkPath, location, name) {
|
|
4025
4449
|
const bareName = name.split("/").pop();
|
|
4026
|
-
for (const dir of [
|
|
4027
|
-
const manifest =
|
|
4028
|
-
if (!
|
|
4450
|
+
for (const dir of [join17(forkPath, location), forkPath]) {
|
|
4451
|
+
const manifest = join17(dir, "node_modules", name, "package.json");
|
|
4452
|
+
if (!existsSync10(manifest)) continue;
|
|
4029
4453
|
try {
|
|
4030
4454
|
const { bin } = JSON.parse(readFileSync5(manifest, "utf-8"));
|
|
4031
4455
|
if (typeof bin === "string") return [bareName];
|
|
@@ -4067,26 +4491,6 @@ async function loadForkUsage(forkPath, upstreamRef, baseRef) {
|
|
|
4067
4491
|
}
|
|
4068
4492
|
|
|
4069
4493
|
// 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
4494
|
function isHigherVersion(upstreamVersion, forkVersion) {
|
|
4091
4495
|
if (upstreamVersion === forkVersion) return false;
|
|
4092
4496
|
const upCoerced = parseComparableVersion(upstreamVersion);
|
|
@@ -4204,8 +4608,28 @@ function safeMergePnpm(forkPnpm, upstreamPnpm, basePnpm) {
|
|
|
4204
4608
|
}
|
|
4205
4609
|
return { merged, changed };
|
|
4206
4610
|
}
|
|
4611
|
+
function mergeModuleType(forkType, upstreamType, baseType) {
|
|
4612
|
+
const asString = (value) => typeof value === "string" ? value : void 0;
|
|
4613
|
+
const fork = asString(forkType);
|
|
4614
|
+
const upstream = asString(upstreamType);
|
|
4615
|
+
if (fork === upstream) return null;
|
|
4616
|
+
if (fork === void 0) return { value: upstream, change: "added" };
|
|
4617
|
+
if (fork !== asString(baseType)) return null;
|
|
4618
|
+
return { value: upstream, change: upstream === void 0 ? "removed" : "updated" };
|
|
4619
|
+
}
|
|
4620
|
+
function setTopLevelKey(pkg, key, value) {
|
|
4621
|
+
if (key in pkg) {
|
|
4622
|
+
pkg[key] = value;
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
const entries = Object.entries(pkg);
|
|
4626
|
+
const after = Math.max(...["name", "version", "private"].map((name) => entries.findIndex(([k]) => k === name)));
|
|
4627
|
+
entries.splice(after + 1, 0, [key, value]);
|
|
4628
|
+
for (const name of Object.keys(pkg)) delete pkg[name];
|
|
4629
|
+
Object.assign(pkg, Object.fromEntries(entries));
|
|
4630
|
+
}
|
|
4207
4631
|
function readPackageJson(filePath) {
|
|
4208
|
-
if (!
|
|
4632
|
+
if (!existsSync11(filePath)) return null;
|
|
4209
4633
|
try {
|
|
4210
4634
|
return JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
4211
4635
|
} catch {
|
|
@@ -4243,13 +4667,13 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4243
4667
|
const changes = [];
|
|
4244
4668
|
const kept = [];
|
|
4245
4669
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4246
|
-
const pkgPath =
|
|
4670
|
+
const pkgPath = join18(forkPath, pkgRelPath);
|
|
4247
4671
|
const forkPkg = readPackageJson(pkgPath);
|
|
4248
4672
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4249
4673
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4250
4674
|
if (!upstream) return { updated: false, changes, kept };
|
|
4251
4675
|
if (!forkPkg) {
|
|
4252
|
-
const workspaceArrived = relativePath !== "" &&
|
|
4676
|
+
const workspaceArrived = relativePath !== "" && existsSync11(join18(forkPath, relativePath));
|
|
4253
4677
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4254
4678
|
writeFileSync4(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4255
4679
|
`, "utf-8");
|
|
@@ -4257,6 +4681,13 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4257
4681
|
}
|
|
4258
4682
|
const upstreamPkg = upstream.data;
|
|
4259
4683
|
let updated = false;
|
|
4684
|
+
const type = mergeModuleType(forkPkg.type, upstreamPkg.type, basePkg?.type);
|
|
4685
|
+
if (type) {
|
|
4686
|
+
if (type.value === void 0) delete forkPkg.type;
|
|
4687
|
+
else setTopLevelKey(forkPkg, "type", type.value);
|
|
4688
|
+
updated = true;
|
|
4689
|
+
changes.push(`type: ${type.change}`);
|
|
4690
|
+
}
|
|
4260
4691
|
for (const key of keysToSync) {
|
|
4261
4692
|
if (key === "pnpm") {
|
|
4262
4693
|
const result2 = safeMergePnpm(forkPkg.pnpm, upstreamPkg.pnpm, basePkg?.pnpm);
|
|
@@ -4364,11 +4795,17 @@ async function runPackages(config, options = {}) {
|
|
|
4364
4795
|
}
|
|
4365
4796
|
|
|
4366
4797
|
// src/services/sync.ts
|
|
4367
|
-
async function
|
|
4368
|
-
|
|
4798
|
+
async function resolveCycleStart(forkPath, base, currentBranch) {
|
|
4799
|
+
if (!await branchExists(forkPath, base)) {
|
|
4800
|
+
await fetch2(forkPath, "origin").catch(() => {
|
|
4801
|
+
});
|
|
4802
|
+
console.info(colors_default.dim(`'${base}' has no local branch \u2014 cutting from 'origin/${base}'.`));
|
|
4803
|
+
return `origin/${base}`;
|
|
4804
|
+
}
|
|
4805
|
+
const { upstream, ahead, behind } = await getUpstreamStatus(forkPath, base);
|
|
4369
4806
|
if (!upstream) {
|
|
4370
4807
|
console.info(colors_default.dim(`'${base}' has no upstream \u2014 skipping the up-to-date check.`));
|
|
4371
|
-
return;
|
|
4808
|
+
return base;
|
|
4372
4809
|
}
|
|
4373
4810
|
if (ahead > 0 && behind > 0) {
|
|
4374
4811
|
throw new Error(
|
|
@@ -4377,26 +4814,53 @@ Reconcile '${base}' with '${upstream}' first (e.g. rebase or reset it), then re-
|
|
|
4377
4814
|
);
|
|
4378
4815
|
}
|
|
4379
4816
|
if (behind > 0) {
|
|
4817
|
+
const worktree = currentBranch === base ? null : await getBranchWorktree(forkPath, base);
|
|
4818
|
+
if (worktree) {
|
|
4819
|
+
console.info(
|
|
4820
|
+
colors_default.dim(`'${base}' is ${behind} behind '${upstream}' but checked out at ${worktree} \u2014 leaving it as it is.`)
|
|
4821
|
+
);
|
|
4822
|
+
return upstream;
|
|
4823
|
+
}
|
|
4380
4824
|
console.info(colors_default.dim(`fast-forwarding '${base}' to '${upstream}' (${behind} behind)...`));
|
|
4381
|
-
await pullFastForward(forkPath);
|
|
4382
|
-
|
|
4825
|
+
if (currentBranch === base) await pullFastForward(forkPath);
|
|
4826
|
+
else await fastForwardBranch(forkPath, base, upstream);
|
|
4827
|
+
return base;
|
|
4383
4828
|
}
|
|
4384
4829
|
if (ahead > 0) {
|
|
4385
4830
|
console.info(colors_default.dim(`'${base}' is ${ahead} commit(s) ahead of '${upstream}' (unpushed) \u2014 continuing.`));
|
|
4386
4831
|
}
|
|
4832
|
+
return base;
|
|
4387
4833
|
}
|
|
4388
4834
|
async function setupTemporarySyncBranch(config) {
|
|
4389
4835
|
const { forkPath, settings } = config;
|
|
4390
4836
|
const base = resolveReleaseBase(settings);
|
|
4391
4837
|
const startBranch = await getCurrentBranch(forkPath);
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
await
|
|
4838
|
+
const startCommit = (await getCommitInfo(forkPath, "HEAD")).hash;
|
|
4839
|
+
console.info(colors_default.dim(`updating '${base}'...`));
|
|
4840
|
+
const startPoint = await resolveCycleStart(forkPath, base, startBranch);
|
|
4395
4841
|
const temporaryBranch = buildTemporarySyncBranch();
|
|
4396
|
-
console.info(colors_default.dim(`creating temporary sync branch '${temporaryBranch}' from '${
|
|
4842
|
+
console.info(colors_default.dim(`creating temporary sync branch '${temporaryBranch}' from '${startPoint}'...`));
|
|
4397
4843
|
console.info();
|
|
4398
|
-
await createBranchFrom(forkPath, temporaryBranch,
|
|
4399
|
-
return { temporaryBranch, base, startBranch };
|
|
4844
|
+
await createBranchFrom(forkPath, temporaryBranch, startPoint);
|
|
4845
|
+
return { temporaryBranch, base, startBranch, startCommit };
|
|
4846
|
+
}
|
|
4847
|
+
async function resolveCutBase(forkPath, base) {
|
|
4848
|
+
if (!await branchExists(forkPath, base)) return `origin/${base}`;
|
|
4849
|
+
const upstream = await getBranchUpstream(forkPath, base);
|
|
4850
|
+
return upstream && await isAncestor(forkPath, base, upstream) ? upstream : base;
|
|
4851
|
+
}
|
|
4852
|
+
async function returnToBase(forkPath, base) {
|
|
4853
|
+
const worktree = await getBranchWorktree(forkPath, base);
|
|
4854
|
+
if (worktree) {
|
|
4855
|
+
console.info(colors_default.dim(`'${base}' is checked out at ${worktree} \u2014 detaching at '${base}' here instead...`));
|
|
4856
|
+
await switchDetached(forkPath, base);
|
|
4857
|
+
return `detached at '${base}'`;
|
|
4858
|
+
}
|
|
4859
|
+
console.info(colors_default.dim(`switching back to '${base}'...`));
|
|
4860
|
+
await switchBranch(forkPath, base);
|
|
4861
|
+
await pullFastForward(forkPath).catch(() => {
|
|
4862
|
+
});
|
|
4863
|
+
return `back on '${base}'`;
|
|
4400
4864
|
}
|
|
4401
4865
|
async function runSync(config, options) {
|
|
4402
4866
|
createSpinner("starting sync...");
|
|
@@ -4427,6 +4891,7 @@ async function runSync(config, options) {
|
|
|
4427
4891
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && hasStagedSyncChanges(result) ? options.stagedBranch : void 0;
|
|
4428
4892
|
printSyncComplete(result, { stagedBranch });
|
|
4429
4893
|
printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
|
|
4894
|
+
await printMigrationNotesLine(config, result);
|
|
4430
4895
|
return result;
|
|
4431
4896
|
}
|
|
4432
4897
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
@@ -4443,7 +4908,7 @@ function qualifyPrRefs(subject, repoSlug) {
|
|
|
4443
4908
|
return subject.replace(/(^|[^\w/])#(\d+)\b/g, `$1${repoSlug}#$2`);
|
|
4444
4909
|
}
|
|
4445
4910
|
function buildSyncPrBody(input) {
|
|
4446
|
-
const { repoSlug, version, fromSha, toSha, commits, totalCount } = input;
|
|
4911
|
+
const { repoSlug, version, fromSha, toSha, commits, totalCount, notes } = input;
|
|
4447
4912
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
4448
4913
|
const short = (sha) => sha.slice(0, 7);
|
|
4449
4914
|
const commitRef = (sha) => githubUrl ? `[\`${short(sha)}\`](${githubUrl}/commit/${sha})` : `\`${short(sha)}\``;
|
|
@@ -4458,6 +4923,19 @@ function buildSyncPrBody(input) {
|
|
|
4458
4923
|
lines.push(`- ${commitRef(commit.hash)} ${qualifyPrRefs(commit.message, repoSlug)}`);
|
|
4459
4924
|
}
|
|
4460
4925
|
}
|
|
4926
|
+
if (notes && notes.total > 0) {
|
|
4927
|
+
const { total, open } = notes;
|
|
4928
|
+
if (open.length === 0) lines.push("", `**Migration notes**: all ${total} handled.`);
|
|
4929
|
+
else {
|
|
4930
|
+
lines.push(
|
|
4931
|
+
"",
|
|
4932
|
+
`**Migration notes**: ${Math.max(0, total - open.length)} of ${total} handled, open (\`pnpm cella migrate\`):`,
|
|
4933
|
+
""
|
|
4934
|
+
);
|
|
4935
|
+
for (const note of open)
|
|
4936
|
+
lines.push(`- ${note.url ? `[${note.title}](${note.url})` : note.title} (\`${note.id}\`)`);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4461
4939
|
return `${lines.join("\n")}
|
|
4462
4940
|
`;
|
|
4463
4941
|
}
|
|
@@ -4473,7 +4951,20 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
4473
4951
|
skip: totalCount > PR_BODY_COMMIT_MAX ? totalCount - PR_BODY_COMMIT_MAX : 0,
|
|
4474
4952
|
limit: PR_BODY_COMMIT_MAX
|
|
4475
4953
|
}) : [];
|
|
4476
|
-
|
|
4954
|
+
const notes = await readPrBodyNotes(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
4955
|
+
return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount, notes });
|
|
4956
|
+
}
|
|
4957
|
+
async function readPrBodyNotes(forkPath, toSha, repoSlug) {
|
|
4958
|
+
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
4959
|
+
const [ids, pending] = await Promise.all([listNoteIds(forkPath, toSha), readPending(forkPath, toSha)]);
|
|
4960
|
+
const open = await Promise.all(
|
|
4961
|
+
pending.map(async (id) => ({
|
|
4962
|
+
id,
|
|
4963
|
+
title: (await readNote(forkPath, toSha, id))?.title ?? id,
|
|
4964
|
+
url: noteUrl(githubUrl, toSha, id)
|
|
4965
|
+
}))
|
|
4966
|
+
);
|
|
4967
|
+
return { total: ids.length, open };
|
|
4477
4968
|
}
|
|
4478
4969
|
function printPrCreateStep(branch, base, title = SYNC_PR_TITLE) {
|
|
4479
4970
|
console.info(colors_default.dim(` gh pr create --base ${base} --head ${branch} --title "${title}" --fill`));
|
|
@@ -4507,22 +4998,13 @@ async function flattenSyncBranch(forkPath, branch, base) {
|
|
|
4507
4998
|
function indentLines(text) {
|
|
4508
4999
|
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
4509
5000
|
}
|
|
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
5001
|
async function shipSyncBranch(config, branch) {
|
|
4520
5002
|
const { forkPath, settings } = config;
|
|
4521
5003
|
const base = resolveReleaseBase(settings);
|
|
4522
|
-
const
|
|
5004
|
+
const cutBase = await resolveCutBase(forkPath, base);
|
|
5005
|
+
const flattened = await flattenSyncBranch(forkPath, branch, cutBase);
|
|
4523
5006
|
let prUrl;
|
|
4524
5007
|
let prOpened = false;
|
|
4525
|
-
let autoMergeEnabled = false;
|
|
4526
5008
|
const headSubject = (await getCommitInfo(forkPath, "HEAD").catch(() => null))?.message;
|
|
4527
5009
|
const prTitle = headSubject?.startsWith(SYNC_PR_TITLE) ? headSubject : SYNC_PR_TITLE;
|
|
4528
5010
|
console.info(colors_default.dim(`pushing '${branch}' to origin...`));
|
|
@@ -4536,7 +5018,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
4536
5018
|
}
|
|
4537
5019
|
if (ghAvailable()) {
|
|
4538
5020
|
console.info(colors_default.dim("opening a pull request..."));
|
|
4539
|
-
const prBody = await buildSyncPrBodyForBranch(forkPath,
|
|
5021
|
+
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
4540
5022
|
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
4541
5023
|
const pr = spawnSync3("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
4542
5024
|
cwd: forkPath,
|
|
@@ -4546,9 +5028,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
4546
5028
|
prUrl = extractFirstUrl(`${pr.stdout ?? ""}
|
|
4547
5029
|
${pr.stderr ?? ""}`);
|
|
4548
5030
|
prOpened = pr.status === 0;
|
|
4549
|
-
if (prOpened) {
|
|
4550
|
-
if (config.directMerge) autoMergeEnabled = enableAutoMerge(forkPath, branch);
|
|
4551
|
-
} else {
|
|
5031
|
+
if (!prOpened) {
|
|
4552
5032
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
4553
5033
|
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
4554
5034
|
printPrCreateStep(branch, base, prTitle);
|
|
@@ -4557,19 +5037,16 @@ ${pr.stderr ?? ""}`);
|
|
|
4557
5037
|
console.info(colors_default.yellow("`gh` not found \u2014 open the PR manually:"));
|
|
4558
5038
|
printPrCreateStep(branch, base, prTitle);
|
|
4559
5039
|
}
|
|
4560
|
-
|
|
4561
|
-
await switchBranch(forkPath, base);
|
|
4562
|
-
await pullFastForward(forkPath).catch(() => {
|
|
4563
|
-
});
|
|
5040
|
+
const position = await returnToBase(forkPath, base);
|
|
4564
5041
|
console.info();
|
|
4565
5042
|
if (prUrl) {
|
|
4566
5043
|
console.info(`${colors_default.green("\u2713")} Sync pull request ${prOpened ? "opened" : "ready"}`);
|
|
4567
|
-
|
|
4568
|
-
console.info(colors_default.dim(` ${prUrl} \xB7 ${status}`));
|
|
5044
|
+
console.info(colors_default.dim(` ${prUrl} \xB7 branch pushed, ${position}`));
|
|
4569
5045
|
} else {
|
|
4570
5046
|
console.info(`${colors_default.green("\u2713")} Sync branch pushed`);
|
|
4571
|
-
console.info(colors_default.dim(` '${branch}' is on origin,
|
|
5047
|
+
console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
|
|
4572
5048
|
}
|
|
5049
|
+
await printMigrationNotesLine(config);
|
|
4573
5050
|
}
|
|
4574
5051
|
function finalizeWorkspace(forkPath) {
|
|
4575
5052
|
for (const args of [["install"], ["check"]]) {
|
|
@@ -4587,8 +5064,9 @@ async function runSyncCycle(config) {
|
|
|
4587
5064
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
4588
5065
|
}
|
|
4589
5066
|
if (result.conflicts.length > 0) return { status: "conflicts", branch };
|
|
4590
|
-
if (!mergeInProgress(forkPath)) {
|
|
4591
|
-
|
|
5067
|
+
if (!await mergeInProgress(forkPath)) {
|
|
5068
|
+
if (branch.startBranch === "HEAD") await switchDetached(forkPath, branch.startCommit);
|
|
5069
|
+
else await switchBranch(forkPath, branch.startBranch);
|
|
4592
5070
|
await deleteBranch(forkPath, branch.temporaryBranch);
|
|
4593
5071
|
return { status: "noop" };
|
|
4594
5072
|
}
|
|
@@ -4618,6 +5096,7 @@ async function commitSyncMerge(config, branch) {
|
|
|
4618
5096
|
console.info();
|
|
4619
5097
|
console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
|
|
4620
5098
|
printTriageSteps(branch);
|
|
5099
|
+
await printMigrationNotesLine(config);
|
|
4621
5100
|
}
|
|
4622
5101
|
function printTriageSteps(branch) {
|
|
4623
5102
|
console.info(colors_default.dim(`staying on '${branch}' without pushing.`));
|
|
@@ -4645,9 +5124,6 @@ async function mergeOpenSyncPrs(config, open) {
|
|
|
4645
5124
|
closePr(forkPath, pr.number);
|
|
4646
5125
|
await deleteBranch(forkPath, pr.headRefName);
|
|
4647
5126
|
}
|
|
4648
|
-
console.info(colors_default.dim(`switching to '${base}' and fast-forwarding...`));
|
|
4649
|
-
await switchBranch(forkPath, base);
|
|
4650
|
-
await pullFastForward(forkPath);
|
|
4651
5127
|
return "continue";
|
|
4652
5128
|
}
|
|
4653
5129
|
async function guardAgainstOpenSyncPr(config) {
|
|
@@ -4686,7 +5162,7 @@ async function runSyncCommand(config) {
|
|
|
4686
5162
|
const { forkPath } = config;
|
|
4687
5163
|
const currentBranch = await getCurrentBranch(forkPath);
|
|
4688
5164
|
const onSyncBranch = isTemporarySyncBranch(currentBranch);
|
|
4689
|
-
if (onSyncBranch && mergeInProgress(forkPath)) {
|
|
5165
|
+
if (onSyncBranch && await mergeInProgress(forkPath)) {
|
|
4690
5166
|
await commitSyncMerge(config, currentBranch);
|
|
4691
5167
|
return;
|
|
4692
5168
|
}
|
|
@@ -4809,7 +5285,7 @@ async function runForks(config) {
|
|
|
4809
5285
|
console.error(colors_default.red(`fork '${config.fork}' not found in config`));
|
|
4810
5286
|
return;
|
|
4811
5287
|
}
|
|
4812
|
-
const resolvedPath =
|
|
5288
|
+
const resolvedPath = resolve7(await resolveForkBasePath(config.forkPath), match.localPath);
|
|
4813
5289
|
await syncFork(config, match, resolvedPath);
|
|
4814
5290
|
return;
|
|
4815
5291
|
}
|
|
@@ -4825,7 +5301,7 @@ async function runForks(config) {
|
|
|
4825
5301
|
if (selectedPath === "_exit") {
|
|
4826
5302
|
process8.exit(0);
|
|
4827
5303
|
}
|
|
4828
|
-
const resolvedForkPath =
|
|
5304
|
+
const resolvedForkPath = resolve7(forkBasePath, selectedPath);
|
|
4829
5305
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
4830
5306
|
if (!selectedFork) {
|
|
4831
5307
|
console.error(colors_default.red(`fork '${selectedPath}' not found in config`));
|
|
@@ -4837,8 +5313,8 @@ async function runForks(config) {
|
|
|
4837
5313
|
}
|
|
4838
5314
|
|
|
4839
5315
|
// src/services/stats.ts
|
|
4840
|
-
import { readFile as
|
|
4841
|
-
import { join as
|
|
5316
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5317
|
+
import { join as join20 } from "path";
|
|
4842
5318
|
|
|
4843
5319
|
// src/utils/coverage-utils.ts
|
|
4844
5320
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -4932,7 +5408,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
4932
5408
|
// src/services/stats.ts
|
|
4933
5409
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
4934
5410
|
async function getWorkspacePackages(forkPath) {
|
|
4935
|
-
const content = await
|
|
5411
|
+
const content = await readFile3(join20(forkPath, "pnpm-workspace.yaml"), "utf-8");
|
|
4936
5412
|
return parseYamlBlockList(content, "packages").map((pattern) => {
|
|
4937
5413
|
if (pattern.endsWith("/*")) {
|
|
4938
5414
|
const base = pattern.slice(0, -2);
|
|
@@ -5095,20 +5571,20 @@ async function runStats(forkPath, options = {}) {
|
|
|
5095
5571
|
function getForkPath() {
|
|
5096
5572
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5097
5573
|
if (envPath) {
|
|
5098
|
-
const resolved =
|
|
5099
|
-
if (!
|
|
5574
|
+
const resolved = resolve8(envPath);
|
|
5575
|
+
if (!existsSync13(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5100
5576
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5101
5577
|
}
|
|
5102
5578
|
return resolved;
|
|
5103
5579
|
}
|
|
5104
5580
|
let cwd = process9.cwd();
|
|
5105
5581
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
5106
|
-
cwd =
|
|
5582
|
+
cwd = resolve8(cwd, "../..");
|
|
5107
5583
|
}
|
|
5108
5584
|
return cwd;
|
|
5109
5585
|
}
|
|
5110
5586
|
async function preflight(forkPath) {
|
|
5111
|
-
if (!
|
|
5587
|
+
if (!existsSync13(join21(forkPath, ".git"))) {
|
|
5112
5588
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5113
5589
|
}
|
|
5114
5590
|
}
|
|
@@ -5137,6 +5613,9 @@ async function main() {
|
|
|
5137
5613
|
await runSyncCommand(config);
|
|
5138
5614
|
break;
|
|
5139
5615
|
}
|
|
5616
|
+
case "migrate":
|
|
5617
|
+
await runMigrate(config);
|
|
5618
|
+
break;
|
|
5140
5619
|
case "audit":
|
|
5141
5620
|
await runAudit(config, { force: config.force, checkOverrides: config.checkOverrides });
|
|
5142
5621
|
break;
|