@launchsecure/launch-kit 0.0.35 → 0.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chart-client/assets/index-DJrjyXbN.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-8eSXr3Ez.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/council-client/assets/index-4K0t2WrZ.css +1 -0
- package/dist/council-client/index.html +2 -2
- package/dist/deck-client/assets/{_baseUniq-BiVx0WO_.js → _baseUniq-Cn5TyL9s.js} +1 -1
- package/dist/deck-client/assets/{arc-DGMkiEzS.js → arc-D61amKYu.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-Y2WRmHtk.js → architectureDiagram-Q4EWVU46-CpKrvC2W.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-_Lbfu5BQ.js → blockDiagram-DXYQGD6D-Yj5OjxvG.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CTqpYTBX.js → c4Diagram-AHTNJAMY-BIR810Tv.js} +1 -1
- package/dist/deck-client/assets/channel-DrJz2x-n.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-liEIbPHs.js → chunk-4BX2VUAB-BeSHwGvx.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-CCc6lYvL.js → chunk-4TB4RGXK-CCqzsLpg.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-D02jJUR2.js → chunk-55IACEB6-CuW_aq4-.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-BFmGMbLD.js → chunk-EDXVE4YY-Dl35ixYh.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-6wFLOVcJ.js → chunk-FMBD7UC4-TwreZQTv.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-Bnr8RiBf.js → chunk-OYMX7WX6-Ahfw8EUo.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-Ct82MksJ.js → chunk-QZHKN3VN-DlE_zlU-.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-BXmN1diQ.js → chunk-YZCP3GAM-Dj6QWzSg.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-a3tg9w7z.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-a3tg9w7z.js +1 -0
- package/dist/deck-client/assets/clone-Dd7JBCL5.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-CmQCT-mH.js → cose-bilkent-S5V4N54A-BO1z5aOM.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-DDdSa9EX.js → dagre-KV5264BT-DVsw17fE.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD-Bccks2xJ.js → diagram-5BDNPKRD-6jYs7oZk.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-CPPNgxmQ.js → diagram-G4DWMVQ6-6DbggeGE.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-KrD300pS.js → diagram-MMDJMWI5-CQtk1cSU.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-DefnLuQf.js → diagram-TYMM5635-BR-gt75b.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-DI9FfnFP.js → erDiagram-SMLLAGMA-C9qMtjdY.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-twKyd3Fx.js → flowDiagram-DWJPFMVM-CdaPhPYb.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-Wau3jhBr.js → ganttDiagram-T4ZO3ILL-BRsZWUy4.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-D9GgYXwb.js → gitGraphDiagram-UUTBAWPF-B8Z90jCj.js} +1 -1
- package/dist/deck-client/assets/{graph-BhNLzyXS.js → graph-my2Zphm4.js} +1 -1
- package/dist/deck-client/assets/index-ByqxPEgU.css +1 -0
- package/dist/deck-client/assets/{index-BtQBaQ7s.js → index-DqAoYZwV.js} +43 -42
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-TylGlSG-.js → infoDiagram-42DDH7IO-Csr9loin.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DAT8icpg.js → ishikawaDiagram-UXIWVN3A-HWdvUNFi.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-D3v_XL72.js → journeyDiagram-VCZTEJTY-CjYHG6EM.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-DNUOBiNr.js → kanban-definition-6JOO6SKY-CX3JdUu7.js} +1 -1
- package/dist/deck-client/assets/{layout-COfodgwF.js → layout-Bcucv5Gi.js} +1 -1
- package/dist/deck-client/assets/{linear-DmTsuIvK.js → linear-CUGM5FJZ.js} +1 -1
- package/dist/deck-client/assets/{min-BW1F7i1D.js → min-Dw4g5w9z.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-CErFzKWl.js → mindmap-definition-QFDTVHPH-C8oo61fg.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DW5F757o.js → pieDiagram-DEJITSTG-D2WYGkq8.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-B1S2-TfI.js → quadrantDiagram-34T5L4WZ-Vh00GISt.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BY5BAR-5.js → requirementDiagram-MS252O5E-DxI-DFrN.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-CE1Cp9HS.js → sankeyDiagram-XADWPNL6-QgwyjasI.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-IaHnbKye.js → sequenceDiagram-FGHM5R23-DmOmD5Ni.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-CwPJm9hU.js → stateDiagram-FHFEXIEX-CRwglGg_.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BvZLEWAA.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DVFGGSgN.js → timeline-definition-GMOUNBTQ-Dj9YGKOh.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-C1194MJi.js → vennDiagram-DHZGUBPP-xzIaOzEU.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-CEAay09T.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-hpwdFfGj.js → wardleyDiagram-NUSXRM2D-BIYYh-JZ.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-DYkotwy8.js → xychartDiagram-5P7HB3ND-Cy9EoJCh.js} +1 -1
- package/dist/deck-client/index.html +2 -2
- package/dist/server/cli.js +261 -26
- package/dist/server/council-entry.js +86 -2
- package/dist/server/council-serve.js +81 -2
- package/dist/server/deck-mcp-entry.js +449 -68
- package/dist/server/deck-serve.js +411 -42
- package/dist/server/init-entry.js +732 -237
- package/dist/server/orbit-entry.js +880 -144
- package/dist/server/radar-docker-init-entry.js +371 -37
- package/dist/server/rover-entry.js +108 -20
- package/package.json +1 -1
- package/scaffolds/ls-marketplace/plugins/kit/skills/deploy-check/SKILL.md +5 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/kickoff/SKILL.md +20 -4
- package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +27 -7
- package/dist/chart-client/assets/index-DpKO9p0s.css +0 -1
- package/dist/client/assets/index-Dv6dD2zY.css +0 -32
- package/dist/council-client/assets/index-AqQ9Sei6.css +0 -1
- package/dist/deck-client/assets/channel-DB6LxW_l.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +0 -1
- package/dist/deck-client/assets/clone-DiIRH1pI.js +0 -1
- package/dist/deck-client/assets/index-B-YQq5b5.css +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +0 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-CHZiUbBa.js +0 -162
- /package/dist/chart-client/assets/{index-DFu2xIrM.js → index-BgUxHxwE.js} +0 -0
- /package/dist/client/assets/{index-Cbw6bVdx.js → index-CUivaQnN.js} +0 -0
- /package/dist/council-client/assets/{index-CAsmGTzg.js → index-DN8HN_5K.js} +0 -0
|
@@ -361,9 +361,15 @@ function resolvePgClient(versionHint) {
|
|
|
361
361
|
const dumpOnPath = which("pg_dump");
|
|
362
362
|
const psqlOnPath = which("psql");
|
|
363
363
|
if (dumpOnPath && psqlOnPath) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
364
|
+
const pathVer = probeVersion(dumpOnPath);
|
|
365
|
+
const pathMajor = pathVer ? parseInt(pathVer, 10) : void 0;
|
|
366
|
+
if (versionHint === void 0 || pathMajor === void 0 || pathMajor >= versionHint) {
|
|
367
|
+
return { kind: "binary", pgDump: dumpOnPath, psql: psqlOnPath, version: pathVer };
|
|
368
|
+
}
|
|
369
|
+
reasons.push(
|
|
370
|
+
`PATH pg_dump is v${pathMajor} but the server is v${versionHint} \u2014 pg_dump must be \u2265 the server; trying version-matched installs`
|
|
371
|
+
);
|
|
372
|
+
} else if (dumpOnPath || psqlOnPath) {
|
|
367
373
|
reasons.push(`PATH has only ${dumpOnPath ? "pg_dump" : "psql"} \u2014 need both`);
|
|
368
374
|
} else {
|
|
369
375
|
reasons.push("pg_dump + psql not on PATH");
|
|
@@ -428,6 +434,7 @@ function findPostgresApp(versionHint) {
|
|
|
428
434
|
}
|
|
429
435
|
versions.sort((a, b) => parseFloat(b) - parseFloat(a));
|
|
430
436
|
for (const v of versions) {
|
|
437
|
+
if (versionHint && parseInt(v, 10) < versionHint) continue;
|
|
431
438
|
const r = tryVersion(v);
|
|
432
439
|
if (r) return r;
|
|
433
440
|
}
|
|
@@ -803,18 +810,575 @@ var init_adapter_registry = __esm({
|
|
|
803
810
|
}
|
|
804
811
|
});
|
|
805
812
|
|
|
813
|
+
// src/server/orbit/cleanliness.ts
|
|
814
|
+
async function runCleanlinessReport(opts) {
|
|
815
|
+
const start = Date.now();
|
|
816
|
+
const { entry, manifest, base, deep } = opts;
|
|
817
|
+
const checks = [];
|
|
818
|
+
const pgState = findPgState(entry);
|
|
819
|
+
const portState = findPortState(entry);
|
|
820
|
+
checks.push(checkGitClean(entry));
|
|
821
|
+
checks.push(checkGitMerged(entry, base));
|
|
822
|
+
let orbitClient = null;
|
|
823
|
+
let orbitConnError = null;
|
|
824
|
+
if (pgState) {
|
|
825
|
+
try {
|
|
826
|
+
orbitClient = new Client2({ connectionString: orbitDbUrl(pgState) });
|
|
827
|
+
await orbitClient.connect();
|
|
828
|
+
} catch (e) {
|
|
829
|
+
orbitConnError = errMsg(e);
|
|
830
|
+
orbitClient = null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
checks.push(await checkOrbitOnlyRows(pgState, orbitClient, orbitConnError));
|
|
834
|
+
checks.push(await checkUncommittedMigrations(entry, pgState, orbitClient, orbitConnError));
|
|
835
|
+
checks.push(checkSnapshot(pgState));
|
|
836
|
+
checks.push(checkRegistry(entry));
|
|
837
|
+
checks.push(checkEnvParity(entry, manifest, pgState));
|
|
838
|
+
checks.push(await checkDbReachable(pgState, orbitClient, orbitConnError));
|
|
839
|
+
checks.push(deep ? await checkSchemaDrift(entry, pgState) : skip("db.schema-drift", "Schema \u2194 migration drift", "working-health", "not run (pass --deep to enable)"));
|
|
840
|
+
checks.push(checkPorts(portState));
|
|
841
|
+
checks.push(await checkBackupResidue(orbitClient, orbitConnError, pgState));
|
|
842
|
+
if (orbitClient) {
|
|
843
|
+
try {
|
|
844
|
+
await orbitClient.end();
|
|
845
|
+
} catch {
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const dropChecks = checks.filter((c) => c.section === "drop-safety");
|
|
849
|
+
const healthChecks = checks.filter((c) => c.section === "working-health");
|
|
850
|
+
const dropSafe = dropChecks.every((c) => c.status === "pass" || c.status === "skip");
|
|
851
|
+
const healthy = healthChecks.every((c) => c.status !== "fail");
|
|
852
|
+
const { recommendation, actions } = deriveActions(entry, base, checks, dropSafe, healthy);
|
|
853
|
+
return {
|
|
854
|
+
slug: entry.slug,
|
|
855
|
+
branch: entry.branch,
|
|
856
|
+
path: entry.path,
|
|
857
|
+
projectRoot: entry.projectRoot,
|
|
858
|
+
base,
|
|
859
|
+
dropSafe,
|
|
860
|
+
healthy,
|
|
861
|
+
recommendation,
|
|
862
|
+
actions,
|
|
863
|
+
checks,
|
|
864
|
+
durationMs: Date.now() - start
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function deriveActions(entry, base, checks, dropSafe, healthy) {
|
|
868
|
+
const by = (id) => checks.find((c) => c.id === id);
|
|
869
|
+
const preserve = [];
|
|
870
|
+
if (by("git.clean")?.status === "fail") {
|
|
871
|
+
preserve.push("Commit or stash the uncommitted changes \u2014 they are NOT carried by a merge and are lost on drop.");
|
|
872
|
+
}
|
|
873
|
+
const merged = by("git.merged");
|
|
874
|
+
if (merged?.status === "fail") preserve.push(`Merge ${entry.branch} into ${base} to preserve its commits \u2014 ${merged.summary}.`);
|
|
875
|
+
const mig = by("db.uncommitted-migrations");
|
|
876
|
+
if (mig?.status === "fail") preserve.push(`Commit the applied-but-uncommitted migration(s) \u2014 ${mig.summary}.`);
|
|
877
|
+
if (by("db.orbit-only-rows")?.status === "warn") {
|
|
878
|
+
preserve.push("Review possible orbit-only DB rows before dropping (heuristic \u2014 verify against source).");
|
|
879
|
+
}
|
|
880
|
+
const health = [];
|
|
881
|
+
const env = by("env.parity");
|
|
882
|
+
if (env?.status === "fail") health.push(`Fix the orbit env file \u2014 ${env.summary}${envDetail(env)}.`);
|
|
883
|
+
else if (env?.status === "warn") health.push(`Reconcile env drift with the main repo${envDetail(env)}.`);
|
|
884
|
+
if (by("db.reachable")?.status === "fail") {
|
|
885
|
+
health.push(`Start the database cluster so the forked DB is reachable, then re-run \`launch-orbit check ${entry.branch} --deep\` to validate the DB checks.`);
|
|
886
|
+
} else if (by("db.reachable")?.status === "warn") {
|
|
887
|
+
health.push("Forked DB server version differs from the recorded one \u2014 re-fork if you need parity.");
|
|
888
|
+
}
|
|
889
|
+
const ports = by("ports.liveness");
|
|
890
|
+
if (ports?.status === "warn") health.push(`${ports.summary} \u2014 stop it if it's stale.`);
|
|
891
|
+
if (by("db.schema-drift")?.status === "warn") health.push("Resolve schema drift vs schema.prisma (run prisma migrate in the orbit).");
|
|
892
|
+
if (by("db.backup-residue")?.status === "warn") health.push("Drop the leftover _backup_ tables in a follow-up migration.");
|
|
893
|
+
const actions = [];
|
|
894
|
+
let recommendation;
|
|
895
|
+
if (dropSafe) {
|
|
896
|
+
recommendation = healthy ? "DROP \u2014 merged, clean, healthy; nothing would be lost." : "DROP-SAFE \u2014 merged & clean. Remove it, or fix the health items below if you mean to keep working in it.";
|
|
897
|
+
actions.push(`Remove it: launch-orbit drop ${entry.branch} (a pg_dump backup is taken by default)`);
|
|
898
|
+
for (const h of health) actions.push(`If keeping \u2014 ${h}`);
|
|
899
|
+
} else {
|
|
900
|
+
recommendation = "KEEP \u2014 unmerged or uncommitted work would be lost on drop. Address the actions below.";
|
|
901
|
+
actions.push(...preserve, ...health);
|
|
902
|
+
}
|
|
903
|
+
return { recommendation, actions };
|
|
904
|
+
}
|
|
905
|
+
function envDetail(check2) {
|
|
906
|
+
const d = (check2.detail ?? []).map((l) => l.trim()).filter(Boolean);
|
|
907
|
+
return d.length > 0 ? ` (${d.join("; ")})` : "";
|
|
908
|
+
}
|
|
909
|
+
function checkGitClean(entry) {
|
|
910
|
+
if (!(0, import_node_fs6.existsSync)(entry.path)) {
|
|
911
|
+
return mk("git.clean", "Working tree clean", "drop-safety", "fail", `worktree path missing: ${entry.path}`);
|
|
912
|
+
}
|
|
913
|
+
const r = gitTry(entry.path, ["status", "--porcelain"]);
|
|
914
|
+
if (!r.ok) {
|
|
915
|
+
return mk("git.clean", "Working tree clean", "drop-safety", "skip", `git status failed: ${r.err}`);
|
|
916
|
+
}
|
|
917
|
+
const lines = r.out.split("\n").filter((l) => l.length > 0);
|
|
918
|
+
if (lines.length === 0) {
|
|
919
|
+
return mk("git.clean", "Working tree clean", "drop-safety", "pass", "no uncommitted changes");
|
|
920
|
+
}
|
|
921
|
+
return mk(
|
|
922
|
+
"git.clean",
|
|
923
|
+
"Working tree clean",
|
|
924
|
+
"drop-safety",
|
|
925
|
+
"fail",
|
|
926
|
+
`${lines.length} uncommitted/untracked ${lines.length === 1 ? "entry" : "entries"} \u2014 dropping would lose them`,
|
|
927
|
+
lines.map((l) => ` ${l}`)
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
function checkGitMerged(entry, base) {
|
|
931
|
+
const label = `Merged into ${base}`;
|
|
932
|
+
if (!refExists(entry.projectRoot, base)) {
|
|
933
|
+
return mk("git.merged", label, "drop-safety", "skip", `base ref "${base}" not found \u2014 pass --base <ref>`);
|
|
934
|
+
}
|
|
935
|
+
if (!refExists(entry.projectRoot, entry.branch)) {
|
|
936
|
+
return mk("git.merged", label, "drop-safety", "skip", `orbit branch "${entry.branch}" not found`);
|
|
937
|
+
}
|
|
938
|
+
const r = gitTry(entry.projectRoot, ["rev-list", "--count", `${base}..${entry.branch}`]);
|
|
939
|
+
if (!r.ok) {
|
|
940
|
+
return mk("git.merged", label, "drop-safety", "skip", `rev-list failed: ${r.err}`);
|
|
941
|
+
}
|
|
942
|
+
const ahead = Number(r.out.trim());
|
|
943
|
+
if (ahead === 0) {
|
|
944
|
+
return mk("git.merged", label, "drop-safety", "pass", `fully merged (0 commits ahead of ${base})`);
|
|
945
|
+
}
|
|
946
|
+
return mk(
|
|
947
|
+
"git.merged",
|
|
948
|
+
label,
|
|
949
|
+
"drop-safety",
|
|
950
|
+
"fail",
|
|
951
|
+
`${ahead} commit${ahead === 1 ? "" : "s"} ahead of ${base} \u2014 unmerged work would be lost on drop`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
async function checkOrbitOnlyRows(pgState, orbit, connError) {
|
|
955
|
+
const id = "db.orbit-only-rows";
|
|
956
|
+
const label = "No orbit-only DB rows";
|
|
957
|
+
if (!pgState) return skip(id, label, "drop-safety", "no orbit-pg resource");
|
|
958
|
+
if (!orbit) return skip(id, label, "drop-safety", `orbit DB unreachable: ${connError ?? "unknown"}`);
|
|
959
|
+
let source;
|
|
960
|
+
try {
|
|
961
|
+
source = new Client2({ connectionString: pgState.sourceUrl });
|
|
962
|
+
await source.connect();
|
|
963
|
+
} catch (e) {
|
|
964
|
+
return skip(id, label, "drop-safety", `source DB unreachable: ${errMsg(e)}`);
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
const tables = await userTables(orbit);
|
|
968
|
+
const grown = [];
|
|
969
|
+
for (const t of tables) {
|
|
970
|
+
const oc = await rowCount(orbit, t);
|
|
971
|
+
const sc = await rowCount(source, t);
|
|
972
|
+
if (oc !== null && sc !== null && oc > sc) grown.push(` ${t}: ${oc} (orbit) vs ${sc} (source) \u2014 +${oc - sc}`);
|
|
973
|
+
}
|
|
974
|
+
if (grown.length === 0) {
|
|
975
|
+
return mk(id, label, "drop-safety", "pass", `no table grew vs source (${tables.length} tables checked)`);
|
|
976
|
+
}
|
|
977
|
+
return mk(
|
|
978
|
+
id,
|
|
979
|
+
label,
|
|
980
|
+
"drop-safety",
|
|
981
|
+
"warn",
|
|
982
|
+
`${grown.length} table${grown.length === 1 ? "" : "s"} grew vs source \u2014 possible orbit-only data (heuristic)`,
|
|
983
|
+
grown
|
|
984
|
+
);
|
|
985
|
+
} catch (e) {
|
|
986
|
+
return skip(id, label, "drop-safety", `count comparison failed: ${e.message}`);
|
|
987
|
+
} finally {
|
|
988
|
+
try {
|
|
989
|
+
await source.end();
|
|
990
|
+
} catch {
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async function checkUncommittedMigrations(entry, pgState, orbit, connError) {
|
|
995
|
+
const id = "db.uncommitted-migrations";
|
|
996
|
+
const label = "No applied-but-uncommitted migrations";
|
|
997
|
+
if (!pgState) return skip(id, label, "drop-safety", "no orbit-pg resource");
|
|
998
|
+
if (!orbit) return skip(id, label, "drop-safety", `orbit DB unreachable: ${connError ?? "unknown"}`);
|
|
999
|
+
let applied;
|
|
1000
|
+
try {
|
|
1001
|
+
const res = await orbit.query(
|
|
1002
|
+
`SELECT migration_name FROM _prisma_migrations WHERE finished_at IS NOT NULL`
|
|
1003
|
+
);
|
|
1004
|
+
applied = res.rows.map((r) => r.migration_name);
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
return skip(id, label, "drop-safety", `_prisma_migrations not readable: ${e.message}`);
|
|
1007
|
+
}
|
|
1008
|
+
const committed = committedMigrations(entry.path);
|
|
1009
|
+
const orphan = applied.filter((m) => !committed.has(m));
|
|
1010
|
+
if (orphan.length === 0) {
|
|
1011
|
+
return mk(id, label, "drop-safety", "pass", `all ${applied.length} applied migrations are committed`);
|
|
1012
|
+
}
|
|
1013
|
+
return mk(
|
|
1014
|
+
id,
|
|
1015
|
+
label,
|
|
1016
|
+
"drop-safety",
|
|
1017
|
+
"fail",
|
|
1018
|
+
`${orphan.length} migration${orphan.length === 1 ? "" : "s"} applied to the orbit DB but not committed \u2014 SQL would be lost on drop`,
|
|
1019
|
+
orphan.map((m) => ` ${m}`)
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
function checkSnapshot(pgState) {
|
|
1023
|
+
const id = "db.snapshot";
|
|
1024
|
+
const label = "Fork snapshot present";
|
|
1025
|
+
if (!pgState) return skip(id, label, "drop-safety", "no orbit-pg resource");
|
|
1026
|
+
const matches = listSnapshots(pgState.dbName);
|
|
1027
|
+
if (matches.length > 0) {
|
|
1028
|
+
return mk(id, label, "drop-safety", "pass", `${matches.length} fork snapshot(s) under ${SNAPSHOT_DIR2}`);
|
|
1029
|
+
}
|
|
1030
|
+
return mk(
|
|
1031
|
+
id,
|
|
1032
|
+
label,
|
|
1033
|
+
"drop-safety",
|
|
1034
|
+
"warn",
|
|
1035
|
+
"no fork snapshot found \u2014 drop still takes a pg_dump backup, but the create-time baseline is gone"
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
function checkRegistry(entry) {
|
|
1039
|
+
const id = "registry.consistency";
|
|
1040
|
+
const label = "Registry consistency";
|
|
1041
|
+
const issues = doctor().filter((i) => i.slug === entry.slug);
|
|
1042
|
+
if (issues.length === 0) {
|
|
1043
|
+
return mk(id, label, "working-health", "pass", "registry matches disk + git");
|
|
1044
|
+
}
|
|
1045
|
+
return mk(
|
|
1046
|
+
id,
|
|
1047
|
+
label,
|
|
1048
|
+
"working-health",
|
|
1049
|
+
"fail",
|
|
1050
|
+
`${issues.length} registry issue(s)`,
|
|
1051
|
+
issues.map((i) => ` [${i.kind}] ${i.detail}`)
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
function checkEnvParity(entry, manifest, pgState) {
|
|
1055
|
+
const id = "env.parity";
|
|
1056
|
+
const label = "Env parity with main repo";
|
|
1057
|
+
const envName = manifest.envFile ?? ".env.local";
|
|
1058
|
+
const mainPath = (0, import_node_path5.join)(entry.projectRoot, envName);
|
|
1059
|
+
const orbitPath = (0, import_node_path5.join)(entry.path, envName);
|
|
1060
|
+
if (!(0, import_node_fs6.existsSync)(orbitPath)) {
|
|
1061
|
+
return mk(id, label, "working-health", "fail", `orbit ${envName} is missing \u2014 resources can't resolve`);
|
|
1062
|
+
}
|
|
1063
|
+
if (!(0, import_node_fs6.existsSync)(mainPath)) {
|
|
1064
|
+
return skip(id, label, "working-health", `main repo ${envName} not present \u2014 nothing to compare`);
|
|
1065
|
+
}
|
|
1066
|
+
const main2 = parseEnvKeys(mainPath);
|
|
1067
|
+
const orbit = parseEnvKeys(orbitPath);
|
|
1068
|
+
const rewritten = rewrittenKeys(manifest);
|
|
1069
|
+
const detail = [];
|
|
1070
|
+
let status = "pass";
|
|
1071
|
+
if (pgState) {
|
|
1072
|
+
for (const key of pgState.envKeys) {
|
|
1073
|
+
const v = orbit.get(key);
|
|
1074
|
+
if (v === void 0) continue;
|
|
1075
|
+
const path = urlPath(v);
|
|
1076
|
+
if (path !== null && path !== `/${pgState.dbName}`) {
|
|
1077
|
+
status = "fail";
|
|
1078
|
+
detail.push(` ${key} points at "${path}" \u2014 expected "/${pgState.dbName}" (not pointing at the fork)`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const missingInOrbit = [...main2.keys()].filter((k) => !orbit.has(k));
|
|
1083
|
+
const orphanInOrbit = [...orbit.keys()].filter((k) => !main2.has(k));
|
|
1084
|
+
const divergent = [...main2.keys()].filter(
|
|
1085
|
+
(k) => orbit.has(k) && !rewritten.has(k) && orbit.get(k) !== main2.get(k)
|
|
1086
|
+
);
|
|
1087
|
+
if (missingInOrbit.length > 0) {
|
|
1088
|
+
if (status !== "fail") status = "warn";
|
|
1089
|
+
detail.push(` missing in orbit (added to main since fork?): ${missingInOrbit.join(", ")}`);
|
|
1090
|
+
}
|
|
1091
|
+
if (orphanInOrbit.length > 0) {
|
|
1092
|
+
if (status !== "fail") status = "warn";
|
|
1093
|
+
detail.push(` present only in orbit: ${orphanInOrbit.join(", ")}`);
|
|
1094
|
+
}
|
|
1095
|
+
if (divergent.length > 0) {
|
|
1096
|
+
if (status !== "fail") status = "warn";
|
|
1097
|
+
detail.push(` value differs (non-rewritten keys): ${divergent.join(", ")}`);
|
|
1098
|
+
}
|
|
1099
|
+
if (status === "pass") {
|
|
1100
|
+
return mk(id, label, "working-health", "pass", `in sync (${rewritten.size} rewritten keys ignored)`);
|
|
1101
|
+
}
|
|
1102
|
+
return mk(
|
|
1103
|
+
id,
|
|
1104
|
+
label,
|
|
1105
|
+
"working-health",
|
|
1106
|
+
status,
|
|
1107
|
+
status === "fail" ? "env drift includes a broken fork pointer" : "env drift vs main repo",
|
|
1108
|
+
detail
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
async function checkDbReachable(pgState, orbit, connError) {
|
|
1112
|
+
const id = "db.reachable";
|
|
1113
|
+
const label = "Forked DB reachable";
|
|
1114
|
+
if (!pgState) return skip(id, label, "working-health", "no orbit-pg resource");
|
|
1115
|
+
if (!orbit) return mk(id, label, "working-health", "fail", `cannot connect to ${pgState.dbName}: ${connError ?? "unknown"}`);
|
|
1116
|
+
try {
|
|
1117
|
+
const res = await orbit.query(`SELECT current_setting('server_version_num')::int AS v`);
|
|
1118
|
+
const v = res.rows[0]?.v;
|
|
1119
|
+
if (v === pgState.version) {
|
|
1120
|
+
return mk(id, label, "working-health", "pass", `${pgState.dbName} reachable (server ${v})`);
|
|
1121
|
+
}
|
|
1122
|
+
return mk(
|
|
1123
|
+
id,
|
|
1124
|
+
label,
|
|
1125
|
+
"working-health",
|
|
1126
|
+
"warn",
|
|
1127
|
+
`${pgState.dbName} reachable but server version ${v} \u2260 recorded ${pgState.version}`
|
|
1128
|
+
);
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
return mk(id, label, "working-health", "fail", `version probe failed: ${errMsg(e)}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async function checkSchemaDrift(entry, pgState) {
|
|
1134
|
+
const id = "db.schema-drift";
|
|
1135
|
+
const label = "Schema \u2194 migration drift";
|
|
1136
|
+
if (!pgState) return skip(id, label, "working-health", "no orbit-pg resource");
|
|
1137
|
+
const schemaPath = (0, import_node_path5.join)(entry.path, "prisma", "schema.prisma");
|
|
1138
|
+
if (!(0, import_node_fs6.existsSync)(schemaPath)) return skip(id, label, "working-health", "no prisma/schema.prisma in worktree");
|
|
1139
|
+
const res = (0, import_node_child_process5.spawnSync)(
|
|
1140
|
+
"npx",
|
|
1141
|
+
[
|
|
1142
|
+
"prisma",
|
|
1143
|
+
"migrate",
|
|
1144
|
+
"diff",
|
|
1145
|
+
"--from-url",
|
|
1146
|
+
orbitDbUrl(pgState),
|
|
1147
|
+
"--to-schema-datamodel",
|
|
1148
|
+
"prisma/schema.prisma",
|
|
1149
|
+
"--exit-code"
|
|
1150
|
+
],
|
|
1151
|
+
{ cwd: entry.path, encoding: "utf-8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"] }
|
|
1152
|
+
);
|
|
1153
|
+
if (res.status === 0) {
|
|
1154
|
+
return mk(id, label, "working-health", "pass", "live schema matches schema.prisma");
|
|
1155
|
+
}
|
|
1156
|
+
if (res.status === 2) {
|
|
1157
|
+
const summary = (res.stdout || "").trim().split("\n").slice(0, 8);
|
|
1158
|
+
return mk(id, label, "working-health", "warn", "live DB schema drifts from schema.prisma", summary.map((l) => ` ${l}`));
|
|
1159
|
+
}
|
|
1160
|
+
return skip(id, label, "working-health", `prisma migrate diff errored: ${(res.stderr || res.stdout || "").trim().split("\n")[0] ?? "unknown"}`);
|
|
1161
|
+
}
|
|
1162
|
+
function checkPorts(portState) {
|
|
1163
|
+
const id = "ports.liveness";
|
|
1164
|
+
const label = "Allocated ports free";
|
|
1165
|
+
if (!portState) return skip(id, label, "working-health", "no port-range resource");
|
|
1166
|
+
if (portInUse(portState.base)) {
|
|
1167
|
+
return mk(id, label, "working-health", "warn", `port ${portState.base} is bound \u2014 a dev server is likely still running in this orbit`);
|
|
1168
|
+
}
|
|
1169
|
+
return mk(id, label, "working-health", "pass", `port ${portState.base} free`);
|
|
1170
|
+
}
|
|
1171
|
+
async function checkBackupResidue(orbit, connError, pgState) {
|
|
1172
|
+
const id = "db.backup-residue";
|
|
1173
|
+
const label = "No leftover _backup_ tables";
|
|
1174
|
+
if (!pgState) return skip(id, label, "working-health", "no orbit-pg resource");
|
|
1175
|
+
if (!orbit) return skip(id, label, "working-health", `orbit DB unreachable: ${connError ?? "unknown"}`);
|
|
1176
|
+
try {
|
|
1177
|
+
const res = await orbit.query(
|
|
1178
|
+
`SELECT table_name FROM information_schema.tables
|
|
1179
|
+
WHERE table_schema = 'public' AND table_name LIKE '\\_backup\\_%'`
|
|
1180
|
+
);
|
|
1181
|
+
if (res.rows.length === 0) {
|
|
1182
|
+
return mk(id, label, "working-health", "pass", "no migration sidecar tables left behind");
|
|
1183
|
+
}
|
|
1184
|
+
return mk(
|
|
1185
|
+
id,
|
|
1186
|
+
label,
|
|
1187
|
+
"working-health",
|
|
1188
|
+
"warn",
|
|
1189
|
+
`${res.rows.length} leftover _backup_ table(s) \u2014 drop in a follow-up migration`,
|
|
1190
|
+
res.rows.map((r) => ` ${r.table_name}`)
|
|
1191
|
+
);
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
return skip(id, label, "working-health", `query failed: ${e.message}`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function orbitDbUrl(pgState) {
|
|
1197
|
+
const u = new URL(pgState.sourceUrl);
|
|
1198
|
+
u.pathname = `/${pgState.dbName}`;
|
|
1199
|
+
return u.toString();
|
|
1200
|
+
}
|
|
1201
|
+
async function userTables(client) {
|
|
1202
|
+
const res = await client.query(
|
|
1203
|
+
`SELECT table_name FROM information_schema.tables
|
|
1204
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
1205
|
+
AND table_name <> '_prisma_migrations'
|
|
1206
|
+
AND table_name NOT LIKE '\\_backup\\_%'
|
|
1207
|
+
ORDER BY table_name`
|
|
1208
|
+
);
|
|
1209
|
+
return res.rows.map((r) => r.table_name);
|
|
1210
|
+
}
|
|
1211
|
+
async function rowCount(client, table) {
|
|
1212
|
+
try {
|
|
1213
|
+
const res = await client.query(`SELECT count(*)::bigint AS c FROM "public".${quoteIdent(table)}`);
|
|
1214
|
+
return Number(res.rows[0]?.c ?? 0);
|
|
1215
|
+
} catch {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function quoteIdent(ident) {
|
|
1220
|
+
return `"${ident.replace(/"/g, '""')}"`;
|
|
1221
|
+
}
|
|
1222
|
+
function gitTry(cwd, args) {
|
|
1223
|
+
try {
|
|
1224
|
+
const out = (0, import_node_child_process5.execFileSync)("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1225
|
+
return { ok: true, out };
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
return { ok: false, err: e.message };
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function refExists(cwd, ref) {
|
|
1231
|
+
try {
|
|
1232
|
+
(0, import_node_child_process5.execFileSync)("git", ["rev-parse", "--verify", ref], { cwd, stdio: "ignore" });
|
|
1233
|
+
return true;
|
|
1234
|
+
} catch {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function committedMigrations(worktreePath) {
|
|
1239
|
+
const r = gitTry(worktreePath, ["ls-files", "prisma/migrations"]);
|
|
1240
|
+
const out = /* @__PURE__ */ new Set();
|
|
1241
|
+
if (!r.ok) return out;
|
|
1242
|
+
for (const line of r.out.split("\n")) {
|
|
1243
|
+
const m = /^prisma\/migrations\/([^/]+)\//.exec(line.trim());
|
|
1244
|
+
if (m) out.add(m[1]);
|
|
1245
|
+
}
|
|
1246
|
+
return out;
|
|
1247
|
+
}
|
|
1248
|
+
function parseEnvKeys(path) {
|
|
1249
|
+
const out = /* @__PURE__ */ new Map();
|
|
1250
|
+
let text2;
|
|
1251
|
+
try {
|
|
1252
|
+
text2 = (0, import_node_fs6.readFileSync)(path, "utf-8");
|
|
1253
|
+
} catch {
|
|
1254
|
+
return out;
|
|
1255
|
+
}
|
|
1256
|
+
for (const line of text2.split(/\r?\n/)) {
|
|
1257
|
+
const m = ENV_LINE_RE.exec(line);
|
|
1258
|
+
if (!m) continue;
|
|
1259
|
+
out.set(m[2], stripQuotes(m[4]));
|
|
1260
|
+
}
|
|
1261
|
+
return out;
|
|
1262
|
+
}
|
|
1263
|
+
function stripQuotes(raw) {
|
|
1264
|
+
const trimmed = raw.replace(/\s+#.*$/, "").trim();
|
|
1265
|
+
if (trimmed.length >= 2) {
|
|
1266
|
+
const f = trimmed[0];
|
|
1267
|
+
const l = trimmed[trimmed.length - 1];
|
|
1268
|
+
if (f === '"' && l === '"' || f === "'" && l === "'") return trimmed.slice(1, -1);
|
|
1269
|
+
}
|
|
1270
|
+
return trimmed;
|
|
1271
|
+
}
|
|
1272
|
+
function rewrittenKeys(manifest) {
|
|
1273
|
+
const out = /* @__PURE__ */ new Set();
|
|
1274
|
+
for (const r of manifest.resources) {
|
|
1275
|
+
const rewrite = r.config?.rewrite;
|
|
1276
|
+
const env = rewrite?.env;
|
|
1277
|
+
if (Array.isArray(env)) {
|
|
1278
|
+
for (const k of env) if (typeof k === "string") out.add(k);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return out;
|
|
1282
|
+
}
|
|
1283
|
+
function urlPath(value) {
|
|
1284
|
+
try {
|
|
1285
|
+
return new URL(value).pathname;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function findPgState(entry) {
|
|
1291
|
+
for (const r of Object.values(entry.resources)) {
|
|
1292
|
+
if (r.adapter === "orbit-pg") {
|
|
1293
|
+
const s = r.state;
|
|
1294
|
+
if (s && typeof s.dbName === "string" && typeof s.sourceUrl === "string") {
|
|
1295
|
+
return {
|
|
1296
|
+
dbName: s.dbName,
|
|
1297
|
+
sourceUrl: s.sourceUrl,
|
|
1298
|
+
version: typeof s.version === "number" ? s.version : 0,
|
|
1299
|
+
strategy: typeof s.strategy === "string" ? s.strategy : "dump-restore",
|
|
1300
|
+
envKeys: Array.isArray(s.envKeys) ? s.envKeys : ["DATABASE_URL"]
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
function findPortState(entry) {
|
|
1308
|
+
for (const r of Object.values(entry.resources)) {
|
|
1309
|
+
if (r.adapter === "builtin/port-range") {
|
|
1310
|
+
const s = r.state;
|
|
1311
|
+
if (s && typeof s.base === "number") {
|
|
1312
|
+
return {
|
|
1313
|
+
base: s.base,
|
|
1314
|
+
stride: typeof s.stride === "number" ? s.stride : 10,
|
|
1315
|
+
replace: typeof s.replace === "string" ? s.replace : ":3000",
|
|
1316
|
+
envKeys: Array.isArray(s.envKeys) ? s.envKeys : []
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
function listSnapshots(dbName) {
|
|
1324
|
+
if (!(0, import_node_fs6.existsSync)(SNAPSHOT_DIR2)) return [];
|
|
1325
|
+
try {
|
|
1326
|
+
return (0, import_node_fs6.readdirSync)(SNAPSHOT_DIR2).filter((f) => f.includes(`_to_${dbName}.`) || f.endsWith(`_${dbName}.sql.gz`));
|
|
1327
|
+
} catch {
|
|
1328
|
+
return [];
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function portInUse(port) {
|
|
1332
|
+
try {
|
|
1333
|
+
const out = (0, import_node_child_process5.execFileSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
1334
|
+
encoding: "utf-8",
|
|
1335
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1336
|
+
timeout: 500
|
|
1337
|
+
}).trim();
|
|
1338
|
+
return out.length > 0;
|
|
1339
|
+
} catch {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function errMsg(e) {
|
|
1344
|
+
const err2 = e;
|
|
1345
|
+
return err2?.message || err2?.cause?.message || err2?.code || String(e);
|
|
1346
|
+
}
|
|
1347
|
+
function mk(id, label, section, status, summary, detail) {
|
|
1348
|
+
return { id, label, section, status, summary, ...detail && detail.length > 0 ? { detail } : {} };
|
|
1349
|
+
}
|
|
1350
|
+
function skip(id, label, section, summary) {
|
|
1351
|
+
return { id, label, section, status: "skip", summary };
|
|
1352
|
+
}
|
|
1353
|
+
var import_node_child_process5, import_node_fs6, import_node_os3, import_node_path5, import_pg2, Client2, SNAPSHOT_DIR2, ENV_LINE_RE;
|
|
1354
|
+
var init_cleanliness = __esm({
|
|
1355
|
+
"src/server/orbit/cleanliness.ts"() {
|
|
1356
|
+
"use strict";
|
|
1357
|
+
import_node_child_process5 = require("node:child_process");
|
|
1358
|
+
import_node_fs6 = require("node:fs");
|
|
1359
|
+
import_node_os3 = require("node:os");
|
|
1360
|
+
import_node_path5 = require("node:path");
|
|
1361
|
+
import_pg2 = __toESM(require("pg"));
|
|
1362
|
+
init_launch_kit_paths();
|
|
1363
|
+
init_orchestrator();
|
|
1364
|
+
({ Client: Client2 } = import_pg2.default);
|
|
1365
|
+
SNAPSHOT_DIR2 = (0, import_node_path5.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR, "orbit", "snapshots");
|
|
1366
|
+
ENV_LINE_RE = /^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*)(.*)$/;
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
|
|
806
1370
|
// src/server/orbit/env-rewriter.ts
|
|
807
1371
|
function copyEnvFile(srcPath, dstPath) {
|
|
808
|
-
if (!(0,
|
|
1372
|
+
if (!(0, import_node_fs7.existsSync)(srcPath)) {
|
|
809
1373
|
throw new Error(`env source file not found: ${srcPath}`);
|
|
810
1374
|
}
|
|
811
|
-
(0,
|
|
1375
|
+
(0, import_node_fs7.copyFileSync)(srcPath, dstPath);
|
|
812
1376
|
}
|
|
813
1377
|
function rewriteEnvFile(filePath, rewrites) {
|
|
814
|
-
if (!(0,
|
|
1378
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) {
|
|
815
1379
|
throw new Error(`env file not found for rewrite: ${filePath}`);
|
|
816
1380
|
}
|
|
817
|
-
const original = (0,
|
|
1381
|
+
const original = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
|
|
818
1382
|
const lines = original.split(/\r?\n/);
|
|
819
1383
|
const result = { changed: [], unchanged: [], missing: [] };
|
|
820
1384
|
const touched = /* @__PURE__ */ new Set();
|
|
@@ -824,7 +1388,7 @@ function rewriteEnvFile(filePath, rewrites) {
|
|
|
824
1388
|
const [, indent, key, sep, rawValue] = m;
|
|
825
1389
|
if (!(key in rewrites)) return line;
|
|
826
1390
|
touched.add(key);
|
|
827
|
-
const { quote, inner } =
|
|
1391
|
+
const { quote, inner } = stripQuotes2(rawValue);
|
|
828
1392
|
const transform = rewrites[key];
|
|
829
1393
|
const next2 = transform(inner, key);
|
|
830
1394
|
if (next2 === void 0 || next2 === inner) {
|
|
@@ -840,12 +1404,12 @@ function rewriteEnvFile(filePath, rewrites) {
|
|
|
840
1404
|
const next = out.join("\n");
|
|
841
1405
|
if (next !== original) {
|
|
842
1406
|
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
843
|
-
(0,
|
|
844
|
-
(0,
|
|
1407
|
+
(0, import_node_fs7.writeFileSync)(tmp, next, "utf-8");
|
|
1408
|
+
(0, import_node_fs7.renameSync)(tmp, filePath);
|
|
845
1409
|
}
|
|
846
1410
|
return result;
|
|
847
1411
|
}
|
|
848
|
-
function
|
|
1412
|
+
function stripQuotes2(raw) {
|
|
849
1413
|
const trimmed = raw.replace(/\s+#.*$/, "");
|
|
850
1414
|
if (trimmed.length >= 2) {
|
|
851
1415
|
const first = trimmed[0];
|
|
@@ -856,11 +1420,11 @@ function stripQuotes(raw) {
|
|
|
856
1420
|
}
|
|
857
1421
|
return { quote: "", inner: trimmed };
|
|
858
1422
|
}
|
|
859
|
-
var
|
|
1423
|
+
var import_node_fs7, LINE_RE;
|
|
860
1424
|
var init_env_rewriter = __esm({
|
|
861
1425
|
"src/server/orbit/env-rewriter.ts"() {
|
|
862
1426
|
"use strict";
|
|
863
|
-
|
|
1427
|
+
import_node_fs7 = require("node:fs");
|
|
864
1428
|
LINE_RE = /^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*)(.*)$/;
|
|
865
1429
|
}
|
|
866
1430
|
});
|
|
@@ -868,13 +1432,13 @@ var init_env_rewriter = __esm({
|
|
|
868
1432
|
// src/server/orbit/gates/build-lint.ts
|
|
869
1433
|
function finalize(checkPath, ctx, blockers, artifacts) {
|
|
870
1434
|
try {
|
|
871
|
-
(0,
|
|
1435
|
+
(0, import_node_child_process6.execFileSync)("git", ["worktree", "remove", "--force", checkPath], {
|
|
872
1436
|
cwd: ctx.projectRoot,
|
|
873
1437
|
stdio: "ignore"
|
|
874
1438
|
});
|
|
875
1439
|
} catch {
|
|
876
1440
|
try {
|
|
877
|
-
(0,
|
|
1441
|
+
(0, import_node_fs8.rmSync)(checkPath, { recursive: true, force: true });
|
|
878
1442
|
} catch {
|
|
879
1443
|
}
|
|
880
1444
|
}
|
|
@@ -887,9 +1451,9 @@ function finalize(checkPath, ctx, blockers, artifacts) {
|
|
|
887
1451
|
};
|
|
888
1452
|
}
|
|
889
1453
|
function detectInstallCmd(projectRoot) {
|
|
890
|
-
if ((0,
|
|
891
|
-
if ((0,
|
|
892
|
-
if ((0,
|
|
1454
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(projectRoot, "pnpm-lock.yaml"))) return "pnpm install --prefer-offline";
|
|
1455
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(projectRoot, "yarn.lock"))) return "yarn install --prefer-offline";
|
|
1456
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(projectRoot, "bun.lockb"))) return "bun install";
|
|
893
1457
|
return "npm install --prefer-offline --no-audit --no-fund";
|
|
894
1458
|
}
|
|
895
1459
|
function detectLintCmd(worktreePath) {
|
|
@@ -899,11 +1463,11 @@ function detectBuildCmd(worktreePath) {
|
|
|
899
1463
|
return detectScriptCmd(worktreePath, "build");
|
|
900
1464
|
}
|
|
901
1465
|
function detectScriptCmd(worktreePath, scriptName) {
|
|
902
|
-
const pkgPath = (0,
|
|
903
|
-
if (!(0,
|
|
1466
|
+
const pkgPath = (0, import_node_path6.join)(worktreePath, "package.json");
|
|
1467
|
+
if (!(0, import_node_fs8.existsSync)(pkgPath)) return null;
|
|
904
1468
|
let pkg;
|
|
905
1469
|
try {
|
|
906
|
-
pkg = JSON.parse((0,
|
|
1470
|
+
pkg = JSON.parse((0, import_node_fs8.readFileSync)(pkgPath, "utf-8"));
|
|
907
1471
|
} catch {
|
|
908
1472
|
return null;
|
|
909
1473
|
}
|
|
@@ -912,12 +1476,12 @@ function detectScriptCmd(worktreePath, scriptName) {
|
|
|
912
1476
|
return `${pm} run ${scriptName}`;
|
|
913
1477
|
}
|
|
914
1478
|
function loadOrbitEnv(orbitPath) {
|
|
915
|
-
const file = (0,
|
|
916
|
-
if (!(0,
|
|
1479
|
+
const file = (0, import_node_path6.join)(orbitPath, ".env.local");
|
|
1480
|
+
if (!(0, import_node_fs8.existsSync)(file)) return {};
|
|
917
1481
|
const out = {};
|
|
918
1482
|
let content;
|
|
919
1483
|
try {
|
|
920
|
-
content = (0,
|
|
1484
|
+
content = (0, import_node_fs8.readFileSync)(file, "utf-8");
|
|
921
1485
|
} catch {
|
|
922
1486
|
return {};
|
|
923
1487
|
}
|
|
@@ -933,9 +1497,9 @@ function loadOrbitEnv(orbitPath) {
|
|
|
933
1497
|
return out;
|
|
934
1498
|
}
|
|
935
1499
|
function detectPmFromLock(worktreePath) {
|
|
936
|
-
if ((0,
|
|
937
|
-
if ((0,
|
|
938
|
-
if ((0,
|
|
1500
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(worktreePath, "pnpm-lock.yaml"))) return "pnpm";
|
|
1501
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(worktreePath, "yarn.lock"))) return "yarn";
|
|
1502
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(worktreePath, "bun.lockb"))) return "bun";
|
|
939
1503
|
return "npm";
|
|
940
1504
|
}
|
|
941
1505
|
function combineOutput(stdout, stderr) {
|
|
@@ -952,54 +1516,54 @@ ${err2}
|
|
|
952
1516
|
}
|
|
953
1517
|
function saveLog(slug, label, content) {
|
|
954
1518
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
955
|
-
const path = (0,
|
|
956
|
-
(0,
|
|
1519
|
+
const path = (0, import_node_path6.join)(LOG_DIR, `${ts}_${slug}_${label}.log`);
|
|
1520
|
+
(0, import_node_fs8.writeFileSync)(path, content, "utf-8");
|
|
957
1521
|
return path;
|
|
958
1522
|
}
|
|
959
1523
|
function shellQuote2(s) {
|
|
960
1524
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
961
1525
|
}
|
|
962
|
-
var
|
|
1526
|
+
var import_node_child_process6, import_node_fs8, import_node_os4, import_node_path6, LOG_DIR, buildLintGate;
|
|
963
1527
|
var init_build_lint = __esm({
|
|
964
1528
|
"src/server/orbit/gates/build-lint.ts"() {
|
|
965
1529
|
"use strict";
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1530
|
+
import_node_child_process6 = require("node:child_process");
|
|
1531
|
+
import_node_fs8 = require("node:fs");
|
|
1532
|
+
import_node_os4 = require("node:os");
|
|
1533
|
+
import_node_path6 = require("node:path");
|
|
970
1534
|
init_launch_kit_paths();
|
|
971
|
-
LOG_DIR = (0,
|
|
1535
|
+
LOG_DIR = (0, import_node_path6.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR, "orbit", "gate-logs");
|
|
972
1536
|
buildLintGate = {
|
|
973
1537
|
id: "builtin/build-lint",
|
|
974
1538
|
name: "Build + lint on merged state",
|
|
975
1539
|
async detect(ctx) {
|
|
976
1540
|
try {
|
|
977
|
-
(0,
|
|
1541
|
+
(0, import_node_child_process6.execFileSync)("git", ["--version"], { cwd: ctx.projectRoot, stdio: "ignore" });
|
|
978
1542
|
} catch {
|
|
979
1543
|
return { ok: false, reason: "git not available" };
|
|
980
1544
|
}
|
|
981
1545
|
return { ok: true };
|
|
982
1546
|
},
|
|
983
1547
|
async check(ctx, config) {
|
|
984
|
-
(0,
|
|
985
|
-
const checkPath = (0,
|
|
986
|
-
if ((0,
|
|
1548
|
+
(0, import_node_fs8.mkdirSync)(LOG_DIR, { recursive: true });
|
|
1549
|
+
const checkPath = (0, import_node_path6.join)(ctx.projectRoot, ".claude", "worktrees", `__orbit-check-${ctx.entry.slug}__`);
|
|
1550
|
+
if ((0, import_node_fs8.existsSync)(checkPath)) {
|
|
987
1551
|
try {
|
|
988
|
-
(0,
|
|
1552
|
+
(0, import_node_child_process6.execFileSync)("git", ["worktree", "remove", "--force", checkPath], {
|
|
989
1553
|
cwd: ctx.projectRoot,
|
|
990
1554
|
stdio: "ignore"
|
|
991
1555
|
});
|
|
992
1556
|
} catch {
|
|
993
1557
|
}
|
|
994
1558
|
try {
|
|
995
|
-
(0,
|
|
1559
|
+
(0, import_node_fs8.rmSync)(checkPath, { recursive: true, force: true });
|
|
996
1560
|
} catch {
|
|
997
1561
|
}
|
|
998
1562
|
}
|
|
999
1563
|
const blockers = [];
|
|
1000
1564
|
const artifacts = {};
|
|
1001
1565
|
try {
|
|
1002
|
-
(0,
|
|
1566
|
+
(0, import_node_child_process6.execFileSync)(
|
|
1003
1567
|
"git",
|
|
1004
1568
|
["worktree", "add", "--detach", checkPath, ctx.entry.branch],
|
|
1005
1569
|
{ cwd: ctx.projectRoot, stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -1013,7 +1577,7 @@ var init_build_lint = __esm({
|
|
|
1013
1577
|
};
|
|
1014
1578
|
}
|
|
1015
1579
|
try {
|
|
1016
|
-
const mergeRes = (0,
|
|
1580
|
+
const mergeRes = (0, import_node_child_process6.spawnSync)(
|
|
1017
1581
|
"git",
|
|
1018
1582
|
["merge", "--no-commit", "--no-ff", ctx.target],
|
|
1019
1583
|
{ cwd: checkPath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
|
|
@@ -1025,10 +1589,10 @@ var init_build_lint = __esm({
|
|
|
1025
1589
|
}
|
|
1026
1590
|
const orbitEnv = loadOrbitEnv(ctx.entry.path);
|
|
1027
1591
|
const spawnEnv = { ...process.env, ...orbitEnv };
|
|
1028
|
-
if (!(0,
|
|
1592
|
+
if (!(0, import_node_fs8.existsSync)((0, import_node_path6.join)(checkPath, "node_modules"))) {
|
|
1029
1593
|
const installCmd = config.installCmd ?? detectInstallCmd(ctx.projectRoot);
|
|
1030
1594
|
ctx.logger.step(`[build-lint] ${installCmd}`);
|
|
1031
|
-
const installRes = (0,
|
|
1595
|
+
const installRes = (0, import_node_child_process6.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${installCmd}`], {
|
|
1032
1596
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1033
1597
|
encoding: "utf-8",
|
|
1034
1598
|
env: spawnEnv
|
|
@@ -1040,10 +1604,10 @@ var init_build_lint = __esm({
|
|
|
1040
1604
|
return finalize(checkPath, ctx, blockers, artifacts);
|
|
1041
1605
|
}
|
|
1042
1606
|
}
|
|
1043
|
-
if ((0,
|
|
1607
|
+
if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(checkPath, "prisma", "schema.prisma"))) {
|
|
1044
1608
|
const prismaCmd = "npx prisma generate";
|
|
1045
1609
|
ctx.logger.step(`[build-lint] ${prismaCmd}`);
|
|
1046
|
-
const prismaRes = (0,
|
|
1610
|
+
const prismaRes = (0, import_node_child_process6.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${prismaCmd}`], {
|
|
1047
1611
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1048
1612
|
encoding: "utf-8",
|
|
1049
1613
|
env: spawnEnv
|
|
@@ -1063,7 +1627,7 @@ var init_build_lint = __esm({
|
|
|
1063
1627
|
if (tscMode !== false) {
|
|
1064
1628
|
const tscCmd = typeof tscMode === "string" ? tscMode : "npx tsc --noEmit";
|
|
1065
1629
|
ctx.logger.step(`[build-lint] ${tscCmd}`);
|
|
1066
|
-
const tscRes = (0,
|
|
1630
|
+
const tscRes = (0, import_node_child_process6.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${tscCmd}`], {
|
|
1067
1631
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1068
1632
|
encoding: "utf-8",
|
|
1069
1633
|
env: spawnEnv
|
|
@@ -1079,7 +1643,7 @@ var init_build_lint = __esm({
|
|
|
1079
1643
|
const lintCmd = typeof lintMode === "string" ? lintMode : detectLintCmd(checkPath);
|
|
1080
1644
|
if (lintCmd) {
|
|
1081
1645
|
ctx.logger.step(`[build-lint] ${lintCmd}`);
|
|
1082
|
-
const lintRes = (0,
|
|
1646
|
+
const lintRes = (0, import_node_child_process6.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${lintCmd}`], {
|
|
1083
1647
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1084
1648
|
encoding: "utf-8",
|
|
1085
1649
|
env: spawnEnv
|
|
@@ -1098,7 +1662,7 @@ var init_build_lint = __esm({
|
|
|
1098
1662
|
const buildCmd = typeof buildMode === "string" ? buildMode : detectBuildCmd(checkPath);
|
|
1099
1663
|
if (buildCmd) {
|
|
1100
1664
|
ctx.logger.step(`[build-lint] ${buildCmd}`);
|
|
1101
|
-
const buildRes = (0,
|
|
1665
|
+
const buildRes = (0, import_node_child_process6.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${buildCmd}`], {
|
|
1102
1666
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1103
1667
|
encoding: "utf-8",
|
|
1104
1668
|
env: spawnEnv
|
|
@@ -1123,21 +1687,21 @@ var init_build_lint = __esm({
|
|
|
1123
1687
|
});
|
|
1124
1688
|
|
|
1125
1689
|
// src/server/orbit/gates/clean-tree.ts
|
|
1126
|
-
var
|
|
1690
|
+
var import_node_child_process7, import_node_fs9, cleanTreeGate;
|
|
1127
1691
|
var init_clean_tree = __esm({
|
|
1128
1692
|
"src/server/orbit/gates/clean-tree.ts"() {
|
|
1129
1693
|
"use strict";
|
|
1130
|
-
|
|
1131
|
-
|
|
1694
|
+
import_node_child_process7 = require("node:child_process");
|
|
1695
|
+
import_node_fs9 = require("node:fs");
|
|
1132
1696
|
cleanTreeGate = {
|
|
1133
1697
|
id: "builtin/clean-tree",
|
|
1134
1698
|
name: "Orbit worktree has no uncommitted changes",
|
|
1135
1699
|
async detect(ctx) {
|
|
1136
|
-
if (!(0,
|
|
1700
|
+
if (!(0, import_node_fs9.existsSync)(ctx.entry.path)) {
|
|
1137
1701
|
return { ok: false, reason: `orbit worktree path missing: ${ctx.entry.path}` };
|
|
1138
1702
|
}
|
|
1139
1703
|
try {
|
|
1140
|
-
(0,
|
|
1704
|
+
(0, import_node_child_process7.execFileSync)("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
1141
1705
|
cwd: ctx.entry.path,
|
|
1142
1706
|
stdio: "ignore"
|
|
1143
1707
|
});
|
|
@@ -1149,7 +1713,7 @@ var init_clean_tree = __esm({
|
|
|
1149
1713
|
async check(ctx, _config) {
|
|
1150
1714
|
let output;
|
|
1151
1715
|
try {
|
|
1152
|
-
output = (0,
|
|
1716
|
+
output = (0, import_node_child_process7.execFileSync)("git", ["status", "--porcelain"], {
|
|
1153
1717
|
cwd: ctx.entry.path,
|
|
1154
1718
|
encoding: "utf-8",
|
|
1155
1719
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1182,25 +1746,25 @@ var init_clean_tree = __esm({
|
|
|
1182
1746
|
});
|
|
1183
1747
|
|
|
1184
1748
|
// src/server/orbit/gates/mergeability.ts
|
|
1185
|
-
function
|
|
1749
|
+
function refExists2(cwd, ref) {
|
|
1186
1750
|
try {
|
|
1187
|
-
(0,
|
|
1751
|
+
(0, import_node_child_process8.execFileSync)("git", ["rev-parse", "--verify", ref], { cwd, stdio: "ignore" });
|
|
1188
1752
|
return true;
|
|
1189
1753
|
} catch {
|
|
1190
1754
|
return false;
|
|
1191
1755
|
}
|
|
1192
1756
|
}
|
|
1193
|
-
var
|
|
1757
|
+
var import_node_child_process8, mergeabilityGate;
|
|
1194
1758
|
var init_mergeability = __esm({
|
|
1195
1759
|
"src/server/orbit/gates/mergeability.ts"() {
|
|
1196
1760
|
"use strict";
|
|
1197
|
-
|
|
1761
|
+
import_node_child_process8 = require("node:child_process");
|
|
1198
1762
|
mergeabilityGate = {
|
|
1199
1763
|
id: "builtin/mergeability",
|
|
1200
1764
|
name: "Textual mergeability (git merge-tree)",
|
|
1201
1765
|
async detect(ctx) {
|
|
1202
1766
|
try {
|
|
1203
|
-
(0,
|
|
1767
|
+
(0, import_node_child_process8.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
|
|
1204
1768
|
cwd: ctx.projectRoot,
|
|
1205
1769
|
stdio: "ignore"
|
|
1206
1770
|
});
|
|
@@ -1211,10 +1775,10 @@ var init_mergeability = __esm({
|
|
|
1211
1775
|
},
|
|
1212
1776
|
async check(ctx, _config) {
|
|
1213
1777
|
const blockers = [];
|
|
1214
|
-
if (!
|
|
1778
|
+
if (!refExists2(ctx.projectRoot, ctx.target)) {
|
|
1215
1779
|
blockers.push(`target ref "${ctx.target}" does not exist`);
|
|
1216
1780
|
}
|
|
1217
|
-
if (!
|
|
1781
|
+
if (!refExists2(ctx.projectRoot, ctx.entry.branch)) {
|
|
1218
1782
|
blockers.push(`orbit branch "${ctx.entry.branch}" does not exist`);
|
|
1219
1783
|
}
|
|
1220
1784
|
if (blockers.length > 0) {
|
|
@@ -1223,7 +1787,7 @@ var init_mergeability = __esm({
|
|
|
1223
1787
|
let stdout = "";
|
|
1224
1788
|
let exitCode = 0;
|
|
1225
1789
|
try {
|
|
1226
|
-
stdout = (0,
|
|
1790
|
+
stdout = (0, import_node_child_process8.execFileSync)(
|
|
1227
1791
|
"git",
|
|
1228
1792
|
["merge-tree", "--write-tree", "--messages", ctx.target, ctx.entry.branch],
|
|
1229
1793
|
{ cwd: ctx.projectRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -1282,17 +1846,17 @@ var init_gate_registry = __esm({
|
|
|
1282
1846
|
// src/server/orbit/gate-runner.ts
|
|
1283
1847
|
async function runMergeGates(manifest, ctx, options = {}) {
|
|
1284
1848
|
const refs = manifest.gates?.merge ?? DEFAULT_GATES;
|
|
1285
|
-
const
|
|
1286
|
-
if (
|
|
1849
|
+
const skip2 = new Set(options.skipGates ?? []);
|
|
1850
|
+
if (skip2.size > 0) {
|
|
1287
1851
|
const declared = new Set(refs.map((r) => r.adapter));
|
|
1288
|
-
for (const id of
|
|
1852
|
+
for (const id of skip2) {
|
|
1289
1853
|
if (!declared.has(id)) {
|
|
1290
1854
|
ctx.logger.warn(
|
|
1291
1855
|
`--skip-gate "${id}" does not match any declared gate (declared: ${[...declared].join(", ")})`
|
|
1292
1856
|
);
|
|
1293
1857
|
}
|
|
1294
1858
|
}
|
|
1295
|
-
const blockedRequired = refs.filter((r) => r.required &&
|
|
1859
|
+
const blockedRequired = refs.filter((r) => r.required && skip2.has(r.adapter)).map((r) => r.adapter);
|
|
1296
1860
|
if (blockedRequired.length > 0) {
|
|
1297
1861
|
throw new Error(
|
|
1298
1862
|
`cannot skip required gate(s): ${blockedRequired.join(", ")}. Remove "required": true from orbit.json if you really need to bypass.`
|
|
@@ -1302,7 +1866,7 @@ async function runMergeGates(manifest, ctx, options = {}) {
|
|
|
1302
1866
|
const start = Date.now();
|
|
1303
1867
|
const results = [];
|
|
1304
1868
|
for (const ref of refs) {
|
|
1305
|
-
if (
|
|
1869
|
+
if (skip2.has(ref.adapter)) {
|
|
1306
1870
|
ctx.logger.info(`[gate] skip ${ref.adapter} (--skip-gate)`);
|
|
1307
1871
|
continue;
|
|
1308
1872
|
}
|
|
@@ -1361,21 +1925,21 @@ var init_gate_runner = __esm({
|
|
|
1361
1925
|
|
|
1362
1926
|
// src/server/orbit/manifest.ts
|
|
1363
1927
|
function findManifestPath(projectRoot) {
|
|
1364
|
-
return (0,
|
|
1928
|
+
return (0, import_node_path7.join)(projectRoot, DEFAULT_MANIFEST_FILENAME);
|
|
1365
1929
|
}
|
|
1366
1930
|
function manifestExists(projectRoot) {
|
|
1367
|
-
return (0,
|
|
1931
|
+
return (0, import_node_fs10.existsSync)(findManifestPath(projectRoot));
|
|
1368
1932
|
}
|
|
1369
1933
|
function loadManifest(projectRoot) {
|
|
1370
1934
|
const path = findManifestPath(projectRoot);
|
|
1371
|
-
if (!(0,
|
|
1935
|
+
if (!(0, import_node_fs10.existsSync)(path)) {
|
|
1372
1936
|
throw new Error(
|
|
1373
1937
|
`orbit.json not found at ${path}. Run \`launch-orbit init\` to create one.`
|
|
1374
1938
|
);
|
|
1375
1939
|
}
|
|
1376
1940
|
let raw;
|
|
1377
1941
|
try {
|
|
1378
|
-
raw = JSON.parse((0,
|
|
1942
|
+
raw = JSON.parse((0, import_node_fs10.readFileSync)(path, "utf-8"));
|
|
1379
1943
|
} catch (e) {
|
|
1380
1944
|
throw new Error(`orbit.json is not valid JSON: ${e.message}`);
|
|
1381
1945
|
}
|
|
@@ -1520,24 +2084,24 @@ function isRecord(x) {
|
|
|
1520
2084
|
function err(msg, sourcePath) {
|
|
1521
2085
|
return new Error(`orbit.json (${sourcePath}): ${msg}`);
|
|
1522
2086
|
}
|
|
1523
|
-
var
|
|
2087
|
+
var import_node_fs10, import_node_path7, DEFAULT_MANIFEST_FILENAME;
|
|
1524
2088
|
var init_manifest = __esm({
|
|
1525
2089
|
"src/server/orbit/manifest.ts"() {
|
|
1526
2090
|
"use strict";
|
|
1527
|
-
|
|
1528
|
-
|
|
2091
|
+
import_node_fs10 = require("node:fs");
|
|
2092
|
+
import_node_path7 = require("node:path");
|
|
1529
2093
|
DEFAULT_MANIFEST_FILENAME = "orbit.json";
|
|
1530
2094
|
}
|
|
1531
2095
|
});
|
|
1532
2096
|
|
|
1533
2097
|
// src/server/orbit/active.ts
|
|
1534
2098
|
function anchorPaths(projectRoot) {
|
|
1535
|
-
const dir = (0,
|
|
1536
|
-
return { dir, current: (0,
|
|
2099
|
+
const dir = (0, import_node_path8.join)(projectRoot, LAUNCHSECURE_DIR, "orbit");
|
|
2100
|
+
return { dir, current: (0, import_node_path8.join)(dir, "current"), activeJson: (0, import_node_path8.join)(dir, "active.json") };
|
|
1537
2101
|
}
|
|
1538
2102
|
function present(p) {
|
|
1539
2103
|
try {
|
|
1540
|
-
(0,
|
|
2104
|
+
(0, import_node_fs11.lstatSync)(p);
|
|
1541
2105
|
return true;
|
|
1542
2106
|
} catch {
|
|
1543
2107
|
return false;
|
|
@@ -1545,23 +2109,23 @@ function present(p) {
|
|
|
1545
2109
|
}
|
|
1546
2110
|
function setActiveOrbit(entry, envFileName) {
|
|
1547
2111
|
const paths = anchorPaths(entry.projectRoot);
|
|
1548
|
-
(0,
|
|
2112
|
+
(0, import_node_fs11.mkdirSync)(paths.dir, { recursive: true });
|
|
1549
2113
|
const tmpLink = `${paths.current}.tmp.${process.pid}`;
|
|
1550
|
-
if (present(tmpLink)) (0,
|
|
1551
|
-
(0,
|
|
1552
|
-
(0,
|
|
2114
|
+
if (present(tmpLink)) (0, import_node_fs11.rmSync)(tmpLink, { force: true });
|
|
2115
|
+
(0, import_node_fs11.symlinkSync)(entry.path, tmpLink);
|
|
2116
|
+
(0, import_node_fs11.renameSync)(tmpLink, paths.current);
|
|
1553
2117
|
const active = {
|
|
1554
2118
|
slug: entry.slug,
|
|
1555
2119
|
branch: entry.branch,
|
|
1556
2120
|
path: entry.path,
|
|
1557
|
-
envFile: (0,
|
|
2121
|
+
envFile: (0, import_node_path8.join)(entry.path, envFileName),
|
|
1558
2122
|
projectRoot: entry.projectRoot,
|
|
1559
2123
|
anchor: paths.current,
|
|
1560
2124
|
switchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1561
2125
|
};
|
|
1562
2126
|
const tmpJson = `${paths.activeJson}.tmp.${process.pid}`;
|
|
1563
|
-
(0,
|
|
1564
|
-
(0,
|
|
2127
|
+
(0, import_node_fs11.writeFileSync)(tmpJson, JSON.stringify(active, null, 2) + "\n", "utf-8");
|
|
2128
|
+
(0, import_node_fs11.renameSync)(tmpJson, paths.activeJson);
|
|
1565
2129
|
return active;
|
|
1566
2130
|
}
|
|
1567
2131
|
function clearActiveOrbit(projectRoot) {
|
|
@@ -1570,7 +2134,7 @@ function clearActiveOrbit(projectRoot) {
|
|
|
1570
2134
|
for (const p of [paths.current, paths.activeJson]) {
|
|
1571
2135
|
if (present(p)) {
|
|
1572
2136
|
try {
|
|
1573
|
-
(0,
|
|
2137
|
+
(0, import_node_fs11.rmSync)(p, { force: true });
|
|
1574
2138
|
cleared = true;
|
|
1575
2139
|
} catch {
|
|
1576
2140
|
}
|
|
@@ -1582,17 +2146,17 @@ function readActiveOrbit(projectRoot) {
|
|
|
1582
2146
|
const { activeJson } = anchorPaths(projectRoot);
|
|
1583
2147
|
if (!present(activeJson)) return null;
|
|
1584
2148
|
try {
|
|
1585
|
-
return JSON.parse((0,
|
|
2149
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(activeJson, "utf-8"));
|
|
1586
2150
|
} catch {
|
|
1587
2151
|
return null;
|
|
1588
2152
|
}
|
|
1589
2153
|
}
|
|
1590
|
-
var
|
|
2154
|
+
var import_node_fs11, import_node_path8;
|
|
1591
2155
|
var init_active = __esm({
|
|
1592
2156
|
"src/server/orbit/active.ts"() {
|
|
1593
2157
|
"use strict";
|
|
1594
|
-
|
|
1595
|
-
|
|
2158
|
+
import_node_fs11 = require("node:fs");
|
|
2159
|
+
import_node_path8 = require("node:path");
|
|
1596
2160
|
init_launch_kit_paths();
|
|
1597
2161
|
}
|
|
1598
2162
|
});
|
|
@@ -1623,7 +2187,7 @@ async function create(opts) {
|
|
|
1623
2187
|
const envFileName = opts.envFileName ?? manifest.envFile ?? DEFAULT_ENV_FILE;
|
|
1624
2188
|
const ctx = {
|
|
1625
2189
|
projectRoot: opts.projectRoot,
|
|
1626
|
-
manifestPath: (0,
|
|
2190
|
+
manifestPath: (0, import_node_path9.join)(opts.projectRoot, "orbit.json"),
|
|
1627
2191
|
manifest,
|
|
1628
2192
|
logger: opts.logger,
|
|
1629
2193
|
envFileName
|
|
@@ -1677,13 +2241,13 @@ async function create(opts) {
|
|
|
1677
2241
|
Object.assign(rewriteFns, adapter.envRewrites(state, ctx));
|
|
1678
2242
|
}
|
|
1679
2243
|
}
|
|
1680
|
-
const srcEnv = (0,
|
|
1681
|
-
const dstEnv = (0,
|
|
1682
|
-
if ((0,
|
|
2244
|
+
const srcEnv = (0, import_node_path9.join)(opts.projectRoot, envFileName);
|
|
2245
|
+
const dstEnv = (0, import_node_path9.join)(worktreeState.path, envFileName);
|
|
2246
|
+
if ((0, import_node_fs12.existsSync)(srcEnv) && !(0, import_node_fs12.existsSync)(dstEnv)) {
|
|
1683
2247
|
opts.logger.step(`copy ${envFileName} into worktree`);
|
|
1684
2248
|
copyEnvFile(srcEnv, dstEnv);
|
|
1685
2249
|
}
|
|
1686
|
-
if ((0,
|
|
2250
|
+
if ((0, import_node_fs12.existsSync)(dstEnv) && Object.keys(rewriteFns).length > 0) {
|
|
1687
2251
|
opts.logger.step(`rewrite ${envFileName} (${Object.keys(rewriteFns).join(", ")})`);
|
|
1688
2252
|
const r = rewriteEnvFile(dstEnv, rewriteFns);
|
|
1689
2253
|
if (r.missing.length > 0) {
|
|
@@ -1703,8 +2267,8 @@ async function create(opts) {
|
|
|
1703
2267
|
opts.logger.ok(`registered ${slug}`);
|
|
1704
2268
|
try {
|
|
1705
2269
|
opts.logger.step(`generate chart graph for worktree`);
|
|
1706
|
-
const chartEntry = (0,
|
|
1707
|
-
const result = (0,
|
|
2270
|
+
const chartEntry = (0, import_node_path9.join)(__dirname, "graph-mcp-entry.js");
|
|
2271
|
+
const result = (0, import_node_child_process9.spawnSync)(
|
|
1708
2272
|
process.execPath,
|
|
1709
2273
|
[chartEntry, "generate"],
|
|
1710
2274
|
{
|
|
@@ -1752,10 +2316,23 @@ async function drop(opts) {
|
|
|
1752
2316
|
if (!entry) {
|
|
1753
2317
|
return { slug, backups: {}, freed: {} };
|
|
1754
2318
|
}
|
|
2319
|
+
if (!opts.force) {
|
|
2320
|
+
const report = await check({ branch: opts.branch, projectRoot: opts.projectRoot, logger: opts.logger, base: opts.base });
|
|
2321
|
+
if (!report.dropSafe) {
|
|
2322
|
+
const blockers = report.checks.filter(
|
|
2323
|
+
(c) => c.section === "drop-safety" && (c.status === "fail" || c.status === "warn")
|
|
2324
|
+
);
|
|
2325
|
+
throw new Error(
|
|
2326
|
+
`refusing to drop "${slug}" \u2014 not drop-safe:
|
|
2327
|
+
` + blockers.map((b) => ` \u2717 ${b.label}: ${b.summary}`).join("\n") + `
|
|
2328
|
+
Resolve the above, or re-run with --force to drop anyway (work will be lost).`
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
1755
2332
|
const manifest = loadManifestOrThrow(opts.projectRoot);
|
|
1756
2333
|
const ctx = {
|
|
1757
2334
|
projectRoot: opts.projectRoot,
|
|
1758
|
-
manifestPath: (0,
|
|
2335
|
+
manifestPath: (0, import_node_path9.join)(opts.projectRoot, "orbit.json"),
|
|
1759
2336
|
manifest,
|
|
1760
2337
|
logger: opts.logger,
|
|
1761
2338
|
envFileName: manifest.envFile ?? DEFAULT_ENV_FILE
|
|
@@ -1776,7 +2353,7 @@ async function drop(opts) {
|
|
|
1776
2353
|
if (result.backupPath) backups[ref.name] = result.backupPath;
|
|
1777
2354
|
freed[ref.name] = state.state;
|
|
1778
2355
|
}
|
|
1779
|
-
if ((0,
|
|
2356
|
+
if ((0, import_node_fs12.existsSync)(entry.path)) {
|
|
1780
2357
|
const blockers = findBlockingPids(entry.path);
|
|
1781
2358
|
if (blockers.length > 0) {
|
|
1782
2359
|
opts.logger.warn(`${blockers.length} process(es) holding files in worktree \u2014 terminating:`);
|
|
@@ -1795,10 +2372,10 @@ async function drop(opts) {
|
|
|
1795
2372
|
} catch (e) {
|
|
1796
2373
|
opts.logger.warn(`worktree remove failed (continuing anyway): ${e.message}`);
|
|
1797
2374
|
}
|
|
1798
|
-
if ((0,
|
|
2375
|
+
if ((0, import_node_fs12.existsSync)(entry.path)) {
|
|
1799
2376
|
opts.logger.step(`remove leftover dir at ${entry.path}`);
|
|
1800
2377
|
try {
|
|
1801
|
-
(0,
|
|
2378
|
+
(0, import_node_fs12.rmSync)(entry.path, { recursive: true, force: true });
|
|
1802
2379
|
} catch (e) {
|
|
1803
2380
|
opts.logger.warn(`leftover dir cleanup failed: ${e.message}`);
|
|
1804
2381
|
}
|
|
@@ -1845,7 +2422,7 @@ function doctor() {
|
|
|
1845
2422
|
if (s) return s;
|
|
1846
2423
|
s = /* @__PURE__ */ new Set();
|
|
1847
2424
|
try {
|
|
1848
|
-
const out = (0,
|
|
2425
|
+
const out = (0, import_node_child_process9.execFileSync)("git", ["worktree", "list", "--porcelain"], {
|
|
1849
2426
|
cwd: projectRoot,
|
|
1850
2427
|
encoding: "utf-8",
|
|
1851
2428
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1859,7 +2436,7 @@ function doctor() {
|
|
|
1859
2436
|
return s;
|
|
1860
2437
|
};
|
|
1861
2438
|
for (const w of listWorktrees()) {
|
|
1862
|
-
if (!(0,
|
|
2439
|
+
if (!(0, import_node_fs12.existsSync)(w.path)) {
|
|
1863
2440
|
issues.push({ kind: "missing-worktree", slug: w.slug, detail: `path ${w.path} does not exist` });
|
|
1864
2441
|
continue;
|
|
1865
2442
|
}
|
|
@@ -1873,10 +2450,47 @@ function doctor() {
|
|
|
1873
2450
|
}
|
|
1874
2451
|
return issues;
|
|
1875
2452
|
}
|
|
2453
|
+
async function check(opts) {
|
|
2454
|
+
const slug = slugify(opts.branch);
|
|
2455
|
+
const entry = lookupWorktree(slug);
|
|
2456
|
+
if (!entry) throw new Error(`no orbit registered for branch "${opts.branch}" (slug ${slug})`);
|
|
2457
|
+
const manifest = loadManifestOrThrow(opts.projectRoot);
|
|
2458
|
+
const base = opts.base ?? resolveBaseBranch(opts.projectRoot);
|
|
2459
|
+
return runCleanlinessReport({ entry, manifest, base, deep: opts.deep, logger: opts.logger });
|
|
2460
|
+
}
|
|
2461
|
+
async function checkAll(opts) {
|
|
2462
|
+
const manifest = loadManifestOrThrow(opts.projectRoot);
|
|
2463
|
+
const base = opts.base ?? resolveBaseBranch(opts.projectRoot);
|
|
2464
|
+
const mine = listWorktrees().filter((w) => w.projectRoot === opts.projectRoot);
|
|
2465
|
+
const reports = [];
|
|
2466
|
+
for (const entry of mine) {
|
|
2467
|
+
reports.push(await runCleanlinessReport({ entry, manifest, base, deep: opts.deep, logger: opts.logger }));
|
|
2468
|
+
}
|
|
2469
|
+
return reports;
|
|
2470
|
+
}
|
|
2471
|
+
function resolveBaseBranch(projectRoot) {
|
|
2472
|
+
try {
|
|
2473
|
+
const head = (0, import_node_child_process9.execFileSync)("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], {
|
|
2474
|
+
cwd: projectRoot,
|
|
2475
|
+
encoding: "utf-8",
|
|
2476
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2477
|
+
}).trim();
|
|
2478
|
+
if (head) return head.replace(/^origin\//, "");
|
|
2479
|
+
} catch {
|
|
2480
|
+
}
|
|
2481
|
+
for (const candidate of ["origin/main", "origin/master", "main", "master"]) {
|
|
2482
|
+
try {
|
|
2483
|
+
(0, import_node_child_process9.execFileSync)("git", ["rev-parse", "--verify", candidate], { cwd: projectRoot, stdio: "ignore" });
|
|
2484
|
+
return candidate.replace(/^origin\//, "");
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return "main";
|
|
2489
|
+
}
|
|
1876
2490
|
function findBlockingPids(path) {
|
|
1877
2491
|
let out;
|
|
1878
2492
|
try {
|
|
1879
|
-
out = (0,
|
|
2493
|
+
out = (0, import_node_child_process9.execFileSync)("lsof", ["-F", "pc", "+D", path], {
|
|
1880
2494
|
encoding: "utf-8",
|
|
1881
2495
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1882
2496
|
});
|
|
@@ -1944,22 +2558,22 @@ async function checkMergeable(opts) {
|
|
|
1944
2558
|
async function merge(opts) {
|
|
1945
2559
|
const start = Date.now();
|
|
1946
2560
|
const cleanup = opts.cleanup ?? "full";
|
|
1947
|
-
const
|
|
2561
|
+
const check2 = await checkMergeable({
|
|
1948
2562
|
branch: opts.branch,
|
|
1949
2563
|
target: opts.target,
|
|
1950
2564
|
skipGates: opts.skipGates,
|
|
1951
2565
|
projectRoot: opts.projectRoot,
|
|
1952
2566
|
logger: opts.logger
|
|
1953
2567
|
});
|
|
1954
|
-
if (!
|
|
1955
|
-
const blockers =
|
|
2568
|
+
if (!check2.passed) {
|
|
2569
|
+
const blockers = check2.gates.flatMap((g) => g.blockers);
|
|
1956
2570
|
opts.logger.error(`gates blocked the merge \u2014 fix the above and retry`);
|
|
1957
2571
|
return {
|
|
1958
|
-
slug:
|
|
1959
|
-
branch:
|
|
2572
|
+
slug: check2.slug,
|
|
2573
|
+
branch: check2.branch,
|
|
1960
2574
|
target: opts.target,
|
|
1961
2575
|
merged: false,
|
|
1962
|
-
gates:
|
|
2576
|
+
gates: check2.gates,
|
|
1963
2577
|
cleanedUp: false,
|
|
1964
2578
|
blockedBy: blockers,
|
|
1965
2579
|
durationMs: Date.now() - start
|
|
@@ -1968,70 +2582,72 @@ async function merge(opts) {
|
|
|
1968
2582
|
const targetWorktree = findWorktreeFor(opts.projectRoot, opts.target);
|
|
1969
2583
|
if (!targetWorktree) {
|
|
1970
2584
|
return {
|
|
1971
|
-
slug:
|
|
1972
|
-
branch:
|
|
2585
|
+
slug: check2.slug,
|
|
2586
|
+
branch: check2.branch,
|
|
1973
2587
|
target: opts.target,
|
|
1974
2588
|
merged: false,
|
|
1975
|
-
gates:
|
|
2589
|
+
gates: check2.gates,
|
|
1976
2590
|
cleanedUp: false,
|
|
1977
2591
|
blockedBy: [`no worktree currently has "${opts.target}" checked out \u2014 check it out in main and retry`],
|
|
1978
2592
|
durationMs: Date.now() - start
|
|
1979
2593
|
};
|
|
1980
2594
|
}
|
|
1981
|
-
opts.logger.step(`git merge --ff-only ${
|
|
1982
|
-
const mergeRes = (0,
|
|
2595
|
+
opts.logger.step(`git merge --ff-only ${check2.branch} (in ${targetWorktree})`);
|
|
2596
|
+
const mergeRes = (0, import_node_child_process9.spawnSync)(
|
|
1983
2597
|
"git",
|
|
1984
|
-
["merge", "--ff-only",
|
|
2598
|
+
["merge", "--ff-only", check2.branch],
|
|
1985
2599
|
{ cwd: targetWorktree, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
|
|
1986
2600
|
);
|
|
1987
2601
|
if (mergeRes.status !== 0) {
|
|
1988
2602
|
const stderr = (mergeRes.stderr ?? "").toString().trim();
|
|
1989
2603
|
opts.logger.warn(`ff-only failed (${stderr.split("\n")[0]}); attempting non-ff merge`);
|
|
1990
|
-
const merge2 = (0,
|
|
2604
|
+
const merge2 = (0, import_node_child_process9.spawnSync)(
|
|
1991
2605
|
"git",
|
|
1992
|
-
["merge", "--no-ff", "-m", `Merge orbit ${
|
|
2606
|
+
["merge", "--no-ff", "-m", `Merge orbit ${check2.slug} into ${opts.target}`, check2.branch],
|
|
1993
2607
|
{ cwd: targetWorktree, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
|
|
1994
2608
|
);
|
|
1995
2609
|
if (merge2.status !== 0) {
|
|
1996
2610
|
const detail = (merge2.stderr ?? merge2.stdout ?? "").toString().trim();
|
|
1997
2611
|
return {
|
|
1998
|
-
slug:
|
|
1999
|
-
branch:
|
|
2612
|
+
slug: check2.slug,
|
|
2613
|
+
branch: check2.branch,
|
|
2000
2614
|
target: opts.target,
|
|
2001
2615
|
merged: false,
|
|
2002
|
-
gates:
|
|
2616
|
+
gates: check2.gates,
|
|
2003
2617
|
cleanedUp: false,
|
|
2004
2618
|
blockedBy: [`git merge failed despite passing gates: ${detail.split("\n")[0] ?? "unknown"}`],
|
|
2005
2619
|
durationMs: Date.now() - start
|
|
2006
2620
|
};
|
|
2007
2621
|
}
|
|
2008
2622
|
}
|
|
2009
|
-
const mergeSha = (0,
|
|
2623
|
+
const mergeSha = (0, import_node_child_process9.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
2010
2624
|
cwd: targetWorktree,
|
|
2011
2625
|
encoding: "utf-8"
|
|
2012
2626
|
}).trim();
|
|
2013
|
-
opts.logger.ok(`merged ${
|
|
2627
|
+
opts.logger.ok(`merged ${check2.branch} \u2192 ${opts.target} (${mergeSha.slice(0, 7)})`);
|
|
2014
2628
|
let backupPath;
|
|
2015
2629
|
let cleanedUp = false;
|
|
2016
2630
|
if (cleanup === "full") {
|
|
2017
|
-
opts.logger.step(`cleanup: dropping orbit ${
|
|
2631
|
+
opts.logger.step(`cleanup: dropping orbit ${check2.slug}`);
|
|
2018
2632
|
const dropResult = await drop({
|
|
2019
2633
|
branch: opts.branch,
|
|
2020
2634
|
backup: true,
|
|
2635
|
+
force: true,
|
|
2636
|
+
// already gated by the merge gates above
|
|
2021
2637
|
projectRoot: opts.projectRoot,
|
|
2022
2638
|
logger: opts.logger
|
|
2023
2639
|
});
|
|
2024
2640
|
backupPath = dropResult.backups.db;
|
|
2025
2641
|
cleanedUp = true;
|
|
2026
|
-
opts.logger.ok(`cleaned up ${
|
|
2642
|
+
opts.logger.ok(`cleaned up ${check2.slug} (backup: ${backupPath ?? "n/a"})`);
|
|
2027
2643
|
}
|
|
2028
2644
|
return {
|
|
2029
|
-
slug:
|
|
2030
|
-
branch:
|
|
2645
|
+
slug: check2.slug,
|
|
2646
|
+
branch: check2.branch,
|
|
2031
2647
|
target: opts.target,
|
|
2032
2648
|
merged: true,
|
|
2033
2649
|
mergeSha,
|
|
2034
|
-
gates:
|
|
2650
|
+
gates: check2.gates,
|
|
2035
2651
|
cleanedUp,
|
|
2036
2652
|
backupPath,
|
|
2037
2653
|
durationMs: Date.now() - start
|
|
@@ -2039,7 +2655,7 @@ async function merge(opts) {
|
|
|
2039
2655
|
}
|
|
2040
2656
|
function findWorktreeFor(projectRoot, branch) {
|
|
2041
2657
|
try {
|
|
2042
|
-
const out = (0,
|
|
2658
|
+
const out = (0, import_node_child_process9.execFileSync)("git", ["worktree", "list", "--porcelain"], {
|
|
2043
2659
|
cwd: projectRoot,
|
|
2044
2660
|
encoding: "utf-8"
|
|
2045
2661
|
});
|
|
@@ -2096,14 +2712,15 @@ function formatAge(iso) {
|
|
|
2096
2712
|
const d = Math.floor(h / 24);
|
|
2097
2713
|
return `${d}d`;
|
|
2098
2714
|
}
|
|
2099
|
-
var
|
|
2715
|
+
var import_node_child_process9, import_node_fs12, import_node_path9, DEFAULT_ENV_FILE;
|
|
2100
2716
|
var init_orchestrator = __esm({
|
|
2101
2717
|
"src/server/orbit/orchestrator.ts"() {
|
|
2102
2718
|
"use strict";
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2719
|
+
import_node_child_process9 = require("node:child_process");
|
|
2720
|
+
import_node_fs12 = require("node:fs");
|
|
2721
|
+
import_node_path9 = require("node:path");
|
|
2106
2722
|
init_adapter_registry();
|
|
2723
|
+
init_cleanliness();
|
|
2107
2724
|
init_env_rewriter();
|
|
2108
2725
|
init_gate_runner();
|
|
2109
2726
|
init_manifest();
|
|
@@ -2186,7 +2803,9 @@ async function handleTool(name, args) {
|
|
|
2186
2803
|
const branch = String(args.branch ?? "");
|
|
2187
2804
|
if (!branch) return text({ error: "branch is required" });
|
|
2188
2805
|
const backup = args.backup === false ? false : true;
|
|
2189
|
-
const
|
|
2806
|
+
const force = args.force === true;
|
|
2807
|
+
const base = args.base;
|
|
2808
|
+
const result = await drop({ branch, backup, force, base, projectRoot, logger });
|
|
2190
2809
|
return text(result);
|
|
2191
2810
|
}
|
|
2192
2811
|
case "orbit.doctor": {
|
|
@@ -2202,6 +2821,17 @@ async function handleTool(name, args) {
|
|
|
2202
2821
|
const result = await checkMergeable({ branch, target, skipGates, projectRoot, logger });
|
|
2203
2822
|
return text(result);
|
|
2204
2823
|
}
|
|
2824
|
+
case "orbit.check": {
|
|
2825
|
+
const all = args.all === true;
|
|
2826
|
+
const base = args.base;
|
|
2827
|
+
const deep = args.deep === true;
|
|
2828
|
+
if (all) {
|
|
2829
|
+
return text(await checkAll({ projectRoot, logger, base, deep }));
|
|
2830
|
+
}
|
|
2831
|
+
const branch = String(args.branch ?? "");
|
|
2832
|
+
if (!branch) return text({ error: "branch is required (or pass all: true)" });
|
|
2833
|
+
return text(await check({ branch, projectRoot, logger, base, deep }));
|
|
2834
|
+
}
|
|
2205
2835
|
case "orbit.merge": {
|
|
2206
2836
|
const branch = String(args.branch ?? "");
|
|
2207
2837
|
const target = String(args.target ?? "");
|
|
@@ -2356,12 +2986,14 @@ var init_orbit_mcp = __esm({
|
|
|
2356
2986
|
},
|
|
2357
2987
|
{
|
|
2358
2988
|
name: "orbit.drop",
|
|
2359
|
-
description: "Tear down an orbit: backup the database (default true), drop the database, terminate any process holding files inside the worktree (chart/beacon MCPs, dev servers \u2014 via lsof+SIGTERM), remove the worktree, sweep any leftover on-disk dir, deregister. Idempotent: safe to retry on a partial state (e.g. git already removed the worktree but the registry still claims it). Returns { slug, backups, freed }.",
|
|
2989
|
+
description: "Tear down an orbit: backup the database (default true), drop the database, terminate any process holding files inside the worktree (chart/beacon MCPs, dev servers \u2014 via lsof+SIGTERM), remove the worktree, sweep any leftover on-disk dir, deregister. Idempotent: safe to retry on a partial state (e.g. git already removed the worktree but the registry still claims it).\n\nDrop-safety guard: by default this runs the drop-safety checks first and THROWS if the orbit still has uncommitted/unmerged work or orbit-only DB data \u2014 pass `force: true` to override (work will be lost). Returns { slug, backups, freed }.",
|
|
2360
2990
|
inputSchema: {
|
|
2361
2991
|
type: "object",
|
|
2362
2992
|
properties: {
|
|
2363
2993
|
branch: { type: "string", description: "Branch name to drop." },
|
|
2364
|
-
backup: { type: "boolean", description: "Whether to pg_dump first (default true)." }
|
|
2994
|
+
backup: { type: "boolean", description: "Whether to pg_dump first (default true)." },
|
|
2995
|
+
force: { type: "boolean", description: "Bypass the drop-safety guard (default false)." },
|
|
2996
|
+
base: { type: "string", description: "Base branch for the guard's merged-check (default: detected base)." }
|
|
2365
2997
|
},
|
|
2366
2998
|
required: ["branch"]
|
|
2367
2999
|
}
|
|
@@ -2388,6 +3020,19 @@ var init_orbit_mcp = __esm({
|
|
|
2388
3020
|
required: ["branch", "target"]
|
|
2389
3021
|
}
|
|
2390
3022
|
},
|
|
3023
|
+
{
|
|
3024
|
+
name: "orbit.check",
|
|
3025
|
+
description: "Cleanliness report for a registered orbit \u2014 two verdicts in one pass:\n \u2022 DROP-SAFE: can this orbit be deleted without losing work? (working tree clean, branch merged into base, no orbit-only DB rows vs source, no applied-but-uncommitted migrations, fork snapshot present)\n \u2022 HEALTHY: is it in a sane state to keep working in? (registry consistent, env parity with the main repo, forked DB reachable + version match, ports free, no leftover _backup_ tables; with `deep:true` also a prisma schema-drift check).\n\nAll checks are read-only. Pass `all: true` (omit branch) to sweep every orbit registered for this repo. `base` overrides the branch the merged-check measures against (default: detected base branch). DB checks use the orbit's recorded sourceUrl \u2014 no env needed; they skip gracefully if the DB is unreachable.\n\nReturns one report (or an array with all:true): { slug, branch, base, dropSafe, healthy, checks: [{ id, label, section, status: pass|warn|fail|skip, summary, detail }], durationMs }.",
|
|
3026
|
+
inputSchema: {
|
|
3027
|
+
type: "object",
|
|
3028
|
+
properties: {
|
|
3029
|
+
branch: { type: "string", description: "Orbit branch to check. Omit when all=true." },
|
|
3030
|
+
all: { type: "boolean", description: "Check every orbit registered for this repo instead of one branch." },
|
|
3031
|
+
base: { type: "string", description: "Branch the merged-check measures against (default: detected base)." },
|
|
3032
|
+
deep: { type: "boolean", description: "Also run the prisma schema-drift check (slower)." }
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
},
|
|
2391
3036
|
{
|
|
2392
3037
|
name: "orbit.merge",
|
|
2393
3038
|
description: 'Run gates, and if all pass, perform the merge into the target branch. Default cleanup behavior is `full` \u2014 after a successful merge, the orbit is dropped (DB pg_dump\'d, then DROP DATABASE; worktree removed; branch deleted). Use cleanup="none" to keep the orbit around post-merge.\n\nPass `skipGates: ["builtin/build-lint"]` to bypass specific gates by adapter id. Gates marked `required: true` in orbit.json reject skipping and throw.\n\nReturns: { slug, branch, target, merged, mergeSha, gates, cleanedUp, backupPath, durationMs, blockedBy }. If merged is false, blockedBy lists what stopped the merge (gate failures or git errors).',
|
|
@@ -2415,18 +3060,23 @@ var init_orbit_mcp = __esm({
|
|
|
2415
3060
|
});
|
|
2416
3061
|
|
|
2417
3062
|
// src/server/orbit-entry.ts
|
|
2418
|
-
var
|
|
2419
|
-
var
|
|
3063
|
+
var import_node_fs13 = require("node:fs");
|
|
3064
|
+
var import_node_path10 = require("node:path");
|
|
2420
3065
|
init_orchestrator();
|
|
2421
3066
|
init_logger();
|
|
2422
3067
|
function parseFlags(argv) {
|
|
2423
3068
|
const positional = [];
|
|
2424
|
-
const flags = { emitCd: false, noBackup: false, skipGates: [] };
|
|
3069
|
+
const flags = { emitCd: false, noBackup: false, skipGates: [], deep: false, all: false, json: false, force: false };
|
|
2425
3070
|
for (let i = 0; i < argv.length; i++) {
|
|
2426
3071
|
const a = argv[i];
|
|
2427
3072
|
if (a === "--emit-cd") flags.emitCd = true;
|
|
2428
3073
|
else if (a === "--no-backup") flags.noBackup = true;
|
|
3074
|
+
else if (a === "--force") flags.force = true;
|
|
2429
3075
|
else if (a === "--base-ref") flags.baseRef = argv[++i];
|
|
3076
|
+
else if (a === "--base") flags.base = argv[++i];
|
|
3077
|
+
else if (a === "--deep") flags.deep = true;
|
|
3078
|
+
else if (a === "--all") flags.all = true;
|
|
3079
|
+
else if (a === "--json") flags.json = true;
|
|
2430
3080
|
else if (a === "--profile") flags.profile = argv[++i];
|
|
2431
3081
|
else if (a === "--skip-gate") {
|
|
2432
3082
|
const id = argv[++i];
|
|
@@ -2520,10 +3170,12 @@ ${JSON.stringify(result, null, 2)}
|
|
|
2520
3170
|
}
|
|
2521
3171
|
case "drop": {
|
|
2522
3172
|
const branch = positional[0];
|
|
2523
|
-
if (!branch) die("usage: launch-orbit drop <branch> [--no-backup]");
|
|
3173
|
+
if (!branch) die("usage: launch-orbit drop <branch> [--no-backup] [--force]");
|
|
2524
3174
|
const result = await drop({
|
|
2525
3175
|
branch,
|
|
2526
3176
|
backup: !flags.noBackup,
|
|
3177
|
+
force: flags.force,
|
|
3178
|
+
base: flags.base,
|
|
2527
3179
|
projectRoot,
|
|
2528
3180
|
logger
|
|
2529
3181
|
});
|
|
@@ -2583,6 +3235,23 @@ ${JSON.stringify(result, null, 2)}
|
|
|
2583
3235
|
if (!result.merged) process.exit(1);
|
|
2584
3236
|
return;
|
|
2585
3237
|
}
|
|
3238
|
+
case "check": {
|
|
3239
|
+
const reports = flags.all ? await checkAll({ projectRoot, logger, base: flags.base, deep: flags.deep }) : await (async () => {
|
|
3240
|
+
const branch = positional[0];
|
|
3241
|
+
if (!branch) die("usage: launch-orbit check <branch> [--base <ref>] [--deep] [--json] (or --all)");
|
|
3242
|
+
return [await check({ branch, projectRoot, logger, base: flags.base, deep: flags.deep })];
|
|
3243
|
+
})();
|
|
3244
|
+
if (flags.json) {
|
|
3245
|
+
process.stdout.write(`${JSON.stringify(flags.all ? reports : reports[0], null, 2)}
|
|
3246
|
+
`);
|
|
3247
|
+
} else {
|
|
3248
|
+
if (reports.length === 0) process.stdout.write("(no orbits registered for this repo)\n");
|
|
3249
|
+
for (const r of reports) process.stdout.write(formatReport(r));
|
|
3250
|
+
if (flags.all && reports.length > 1) process.stdout.write(formatRollup(reports));
|
|
3251
|
+
}
|
|
3252
|
+
if (reports.some((r) => !r.healthy)) process.exit(1);
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
2586
3255
|
default:
|
|
2587
3256
|
die(`unknown subcommand: ${subcommand}`);
|
|
2588
3257
|
}
|
|
@@ -2591,9 +3260,76 @@ ${JSON.stringify(result, null, 2)}
|
|
|
2591
3260
|
process.exit(1);
|
|
2592
3261
|
}
|
|
2593
3262
|
}
|
|
3263
|
+
var USE_COLOR = !!process.stdout.isTTY && process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb";
|
|
3264
|
+
var paint = (codes) => (s) => USE_COLOR ? `\x1B[${codes}m${s}\x1B[0m` : s;
|
|
3265
|
+
var C = {
|
|
3266
|
+
green: paint("32"),
|
|
3267
|
+
red: paint("31"),
|
|
3268
|
+
yellow: paint("33"),
|
|
3269
|
+
cyan: paint("36"),
|
|
3270
|
+
dim: paint("2"),
|
|
3271
|
+
bold: paint("1"),
|
|
3272
|
+
greenBadge: (s) => USE_COLOR ? `\x1B[1;37;42m ${s} \x1B[0m` : `[ ${s} ]`,
|
|
3273
|
+
redBadge: (s) => USE_COLOR ? `\x1B[1;37;41m ${s} \x1B[0m` : `[ ${s} ]`
|
|
3274
|
+
};
|
|
3275
|
+
var STATUS = {
|
|
3276
|
+
pass: { glyph: "\u2713", color: C.green },
|
|
3277
|
+
warn: { glyph: "\u26A0", color: C.yellow },
|
|
3278
|
+
fail: { glyph: "\u2717", color: C.red },
|
|
3279
|
+
skip: { glyph: "\u25CB", color: C.dim }
|
|
3280
|
+
};
|
|
3281
|
+
var SECTION_BLURB = {
|
|
3282
|
+
"drop-safety": { title: "Drop-safety", sub: "can this orbit be deleted without losing work?" },
|
|
3283
|
+
"working-health": { title: "Working-health", sub: "is it in a sane state to keep working in?" }
|
|
3284
|
+
};
|
|
3285
|
+
function formatReport(r) {
|
|
3286
|
+
const out = [];
|
|
3287
|
+
const badge = (label, ok) => ok ? C.greenBadge(`${label}: YES`) : C.redBadge(`${label}: NO`);
|
|
3288
|
+
out.push("");
|
|
3289
|
+
out.push(` ${C.cyan("\u25C6")} ${C.bold(`orbit ${r.slug}`)} ${C.dim(`[${r.branch}]`)}`);
|
|
3290
|
+
out.push(` ${C.dim(`base ${r.base} \xB7 ${r.durationMs}ms`)}`);
|
|
3291
|
+
out.push("");
|
|
3292
|
+
out.push(` ${badge("DROP-SAFE", r.dropSafe)} ${badge("HEALTHY", r.healthy)}`);
|
|
3293
|
+
out.push(` ${(r.dropSafe ? C.green : C.yellow)(r.recommendation)}`);
|
|
3294
|
+
for (const key of ["drop-safety", "working-health"]) {
|
|
3295
|
+
const blurb = SECTION_BLURB[key];
|
|
3296
|
+
out.push("");
|
|
3297
|
+
out.push(` ${C.bold(blurb.title)} ${C.dim("\u2014 " + blurb.sub)}`);
|
|
3298
|
+
for (const c of r.checks.filter((c2) => c2.section === key)) {
|
|
3299
|
+
const s = STATUS[c.status];
|
|
3300
|
+
out.push(` ${s.color(s.glyph)} ${c.label} ${C.dim("\xB7")} ${s.color(c.summary)}`);
|
|
3301
|
+
for (const d of c.detail ?? []) out.push(` ${C.dim("\u2022")} ${C.dim(d.trim())}`);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
if (r.actions.length > 0) {
|
|
3305
|
+
out.push("");
|
|
3306
|
+
out.push(` ${C.bold("Actions")}`);
|
|
3307
|
+
for (const a of r.actions) out.push(` ${C.cyan("\u2192")} ${a}`);
|
|
3308
|
+
}
|
|
3309
|
+
out.push("");
|
|
3310
|
+
out.push(` ${C.dim(countLine(r))}`);
|
|
3311
|
+
out.push("");
|
|
3312
|
+
return out.join("\n") + "\n";
|
|
3313
|
+
}
|
|
3314
|
+
function countLine(r) {
|
|
3315
|
+
const n = (st) => r.checks.filter((c) => c.status === st).length;
|
|
3316
|
+
return `${n("pass")} passed \xB7 ${n("warn")} warning \xB7 ${n("fail")} failed \xB7 ${n("skip")} skipped`;
|
|
3317
|
+
}
|
|
3318
|
+
function formatRollup(reports) {
|
|
3319
|
+
const out = [];
|
|
3320
|
+
out.push(` ${C.bold("Summary")} ${C.dim(`\u2014 ${reports.length} orbit${reports.length === 1 ? "" : "s"}`)}`);
|
|
3321
|
+
const w = Math.max(...reports.map((r) => r.slug.length), 4);
|
|
3322
|
+
for (const r of reports) {
|
|
3323
|
+
const drop2 = r.dropSafe ? C.green("drop-safe") : C.red("keep ");
|
|
3324
|
+
const health = r.healthy ? C.green("healthy ") : C.yellow("issues ");
|
|
3325
|
+
out.push(` ${r.slug.padEnd(w)} ${drop2} ${health} ${C.dim(r.branch)}`);
|
|
3326
|
+
}
|
|
3327
|
+
out.push("");
|
|
3328
|
+
return out.join("\n") + "\n";
|
|
3329
|
+
}
|
|
2594
3330
|
function runInit(projectRoot) {
|
|
2595
|
-
const path = (0,
|
|
2596
|
-
if ((0,
|
|
3331
|
+
const path = (0, import_node_path10.join)(projectRoot, "orbit.json");
|
|
3332
|
+
if ((0, import_node_fs13.existsSync)(path)) {
|
|
2597
3333
|
process.stderr.write(`[launch-orbit] orbit.json already exists at ${path}
|
|
2598
3334
|
`);
|
|
2599
3335
|
process.exit(1);
|
|
@@ -2648,7 +3384,7 @@ function runInit(projectRoot) {
|
|
|
2648
3384
|
full: { resources: ["db", "ports"] }
|
|
2649
3385
|
}
|
|
2650
3386
|
};
|
|
2651
|
-
(0,
|
|
3387
|
+
(0, import_node_fs13.writeFileSync)(path, JSON.stringify(starter, null, 2) + "\n", "utf-8");
|
|
2652
3388
|
process.stdout.write(`\u2713 wrote ${path}
|
|
2653
3389
|
`);
|
|
2654
3390
|
}
|