@launchsecure/launch-kit 0.0.34 → 0.0.36
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/server/cli.js +277 -33
- package/dist/server/init-entry.js +741 -230
- package/dist/server/launch-bot-entry.js +4078 -0
- package/dist/server/orbit-entry.js +969 -136
- package/dist/server/radar-docker-init-entry.js +326 -32
- package/dist/server/rover-entry.js +624 -124
- package/package.json +4 -3
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/SKILL.md +53 -22
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/briefs.mjs +152 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/kickoff/SKILL.md +167 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +41 -9
|
@@ -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,16 +2084,83 @@ 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
|
|
|
2097
|
+
// src/server/orbit/active.ts
|
|
2098
|
+
function anchorPaths(projectRoot) {
|
|
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") };
|
|
2101
|
+
}
|
|
2102
|
+
function present(p) {
|
|
2103
|
+
try {
|
|
2104
|
+
(0, import_node_fs11.lstatSync)(p);
|
|
2105
|
+
return true;
|
|
2106
|
+
} catch {
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function setActiveOrbit(entry, envFileName) {
|
|
2111
|
+
const paths = anchorPaths(entry.projectRoot);
|
|
2112
|
+
(0, import_node_fs11.mkdirSync)(paths.dir, { recursive: true });
|
|
2113
|
+
const tmpLink = `${paths.current}.tmp.${process.pid}`;
|
|
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);
|
|
2117
|
+
const active = {
|
|
2118
|
+
slug: entry.slug,
|
|
2119
|
+
branch: entry.branch,
|
|
2120
|
+
path: entry.path,
|
|
2121
|
+
envFile: (0, import_node_path8.join)(entry.path, envFileName),
|
|
2122
|
+
projectRoot: entry.projectRoot,
|
|
2123
|
+
anchor: paths.current,
|
|
2124
|
+
switchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2125
|
+
};
|
|
2126
|
+
const tmpJson = `${paths.activeJson}.tmp.${process.pid}`;
|
|
2127
|
+
(0, import_node_fs11.writeFileSync)(tmpJson, JSON.stringify(active, null, 2) + "\n", "utf-8");
|
|
2128
|
+
(0, import_node_fs11.renameSync)(tmpJson, paths.activeJson);
|
|
2129
|
+
return active;
|
|
2130
|
+
}
|
|
2131
|
+
function clearActiveOrbit(projectRoot) {
|
|
2132
|
+
const paths = anchorPaths(projectRoot);
|
|
2133
|
+
let cleared = false;
|
|
2134
|
+
for (const p of [paths.current, paths.activeJson]) {
|
|
2135
|
+
if (present(p)) {
|
|
2136
|
+
try {
|
|
2137
|
+
(0, import_node_fs11.rmSync)(p, { force: true });
|
|
2138
|
+
cleared = true;
|
|
2139
|
+
} catch {
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return cleared;
|
|
2144
|
+
}
|
|
2145
|
+
function readActiveOrbit(projectRoot) {
|
|
2146
|
+
const { activeJson } = anchorPaths(projectRoot);
|
|
2147
|
+
if (!present(activeJson)) return null;
|
|
2148
|
+
try {
|
|
2149
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(activeJson, "utf-8"));
|
|
2150
|
+
} catch {
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
var import_node_fs11, import_node_path8;
|
|
2155
|
+
var init_active = __esm({
|
|
2156
|
+
"src/server/orbit/active.ts"() {
|
|
2157
|
+
"use strict";
|
|
2158
|
+
import_node_fs11 = require("node:fs");
|
|
2159
|
+
import_node_path8 = require("node:path");
|
|
2160
|
+
init_launch_kit_paths();
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
|
|
1533
2164
|
// src/server/orbit/slug.ts
|
|
1534
2165
|
function slugify(branch) {
|
|
1535
2166
|
let s = branch.toLowerCase();
|
|
@@ -1556,7 +2187,7 @@ async function create(opts) {
|
|
|
1556
2187
|
const envFileName = opts.envFileName ?? manifest.envFile ?? DEFAULT_ENV_FILE;
|
|
1557
2188
|
const ctx = {
|
|
1558
2189
|
projectRoot: opts.projectRoot,
|
|
1559
|
-
manifestPath: (0,
|
|
2190
|
+
manifestPath: (0, import_node_path9.join)(opts.projectRoot, "orbit.json"),
|
|
1560
2191
|
manifest,
|
|
1561
2192
|
logger: opts.logger,
|
|
1562
2193
|
envFileName
|
|
@@ -1610,13 +2241,13 @@ async function create(opts) {
|
|
|
1610
2241
|
Object.assign(rewriteFns, adapter.envRewrites(state, ctx));
|
|
1611
2242
|
}
|
|
1612
2243
|
}
|
|
1613
|
-
const srcEnv = (0,
|
|
1614
|
-
const dstEnv = (0,
|
|
1615
|
-
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)) {
|
|
1616
2247
|
opts.logger.step(`copy ${envFileName} into worktree`);
|
|
1617
2248
|
copyEnvFile(srcEnv, dstEnv);
|
|
1618
2249
|
}
|
|
1619
|
-
if ((0,
|
|
2250
|
+
if ((0, import_node_fs12.existsSync)(dstEnv) && Object.keys(rewriteFns).length > 0) {
|
|
1620
2251
|
opts.logger.step(`rewrite ${envFileName} (${Object.keys(rewriteFns).join(", ")})`);
|
|
1621
2252
|
const r = rewriteEnvFile(dstEnv, rewriteFns);
|
|
1622
2253
|
if (r.missing.length > 0) {
|
|
@@ -1636,8 +2267,8 @@ async function create(opts) {
|
|
|
1636
2267
|
opts.logger.ok(`registered ${slug}`);
|
|
1637
2268
|
try {
|
|
1638
2269
|
opts.logger.step(`generate chart graph for worktree`);
|
|
1639
|
-
const chartEntry = (0,
|
|
1640
|
-
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)(
|
|
1641
2272
|
process.execPath,
|
|
1642
2273
|
[chartEntry, "generate"],
|
|
1643
2274
|
{
|
|
@@ -1685,10 +2316,23 @@ async function drop(opts) {
|
|
|
1685
2316
|
if (!entry) {
|
|
1686
2317
|
return { slug, backups: {}, freed: {} };
|
|
1687
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
|
+
}
|
|
1688
2332
|
const manifest = loadManifestOrThrow(opts.projectRoot);
|
|
1689
2333
|
const ctx = {
|
|
1690
2334
|
projectRoot: opts.projectRoot,
|
|
1691
|
-
manifestPath: (0,
|
|
2335
|
+
manifestPath: (0, import_node_path9.join)(opts.projectRoot, "orbit.json"),
|
|
1692
2336
|
manifest,
|
|
1693
2337
|
logger: opts.logger,
|
|
1694
2338
|
envFileName: manifest.envFile ?? DEFAULT_ENV_FILE
|
|
@@ -1709,7 +2353,7 @@ async function drop(opts) {
|
|
|
1709
2353
|
if (result.backupPath) backups[ref.name] = result.backupPath;
|
|
1710
2354
|
freed[ref.name] = state.state;
|
|
1711
2355
|
}
|
|
1712
|
-
if ((0,
|
|
2356
|
+
if ((0, import_node_fs12.existsSync)(entry.path)) {
|
|
1713
2357
|
const blockers = findBlockingPids(entry.path);
|
|
1714
2358
|
if (blockers.length > 0) {
|
|
1715
2359
|
opts.logger.warn(`${blockers.length} process(es) holding files in worktree \u2014 terminating:`);
|
|
@@ -1728,15 +2372,19 @@ async function drop(opts) {
|
|
|
1728
2372
|
} catch (e) {
|
|
1729
2373
|
opts.logger.warn(`worktree remove failed (continuing anyway): ${e.message}`);
|
|
1730
2374
|
}
|
|
1731
|
-
if ((0,
|
|
2375
|
+
if ((0, import_node_fs12.existsSync)(entry.path)) {
|
|
1732
2376
|
opts.logger.step(`remove leftover dir at ${entry.path}`);
|
|
1733
2377
|
try {
|
|
1734
|
-
(0,
|
|
2378
|
+
(0, import_node_fs12.rmSync)(entry.path, { recursive: true, force: true });
|
|
1735
2379
|
} catch (e) {
|
|
1736
2380
|
opts.logger.warn(`leftover dir cleanup failed: ${e.message}`);
|
|
1737
2381
|
}
|
|
1738
2382
|
}
|
|
1739
2383
|
await deregisterWorktree(slug);
|
|
2384
|
+
if (readActiveOrbit(opts.projectRoot)?.slug === slug) {
|
|
2385
|
+
clearActiveOrbit(opts.projectRoot);
|
|
2386
|
+
opts.logger.step("cleared active-orbit anchor (current)");
|
|
2387
|
+
}
|
|
1740
2388
|
opts.logger.ok(`dropped ${slug}`);
|
|
1741
2389
|
return { slug, backups, freed };
|
|
1742
2390
|
}
|
|
@@ -1753,12 +2401,19 @@ function switchTo(opts) {
|
|
|
1753
2401
|
const slug = slugify(opts.branch);
|
|
1754
2402
|
const entry = lookupWorktree(slug);
|
|
1755
2403
|
if (!entry) throw new Error(`no orbit registered for branch "${opts.branch}" (slug ${slug})`);
|
|
2404
|
+
const active = setActiveOrbit(entry, DEFAULT_ENV_FILE);
|
|
1756
2405
|
return {
|
|
1757
2406
|
path: entry.path,
|
|
1758
|
-
envFile:
|
|
2407
|
+
envFile: active.envFile,
|
|
2408
|
+
anchor: active.anchor,
|
|
2409
|
+
activeJson: anchorPaths(entry.projectRoot).activeJson,
|
|
2410
|
+
nextAction: `cd "${active.anchor}"`,
|
|
1759
2411
|
entry
|
|
1760
2412
|
};
|
|
1761
2413
|
}
|
|
2414
|
+
function deactivate(opts) {
|
|
2415
|
+
return { cleared: clearActiveOrbit(opts.projectRoot), projectRoot: opts.projectRoot };
|
|
2416
|
+
}
|
|
1762
2417
|
function doctor() {
|
|
1763
2418
|
const issues = [];
|
|
1764
2419
|
const trackedByRoot = /* @__PURE__ */ new Map();
|
|
@@ -1767,7 +2422,7 @@ function doctor() {
|
|
|
1767
2422
|
if (s) return s;
|
|
1768
2423
|
s = /* @__PURE__ */ new Set();
|
|
1769
2424
|
try {
|
|
1770
|
-
const out = (0,
|
|
2425
|
+
const out = (0, import_node_child_process9.execFileSync)("git", ["worktree", "list", "--porcelain"], {
|
|
1771
2426
|
cwd: projectRoot,
|
|
1772
2427
|
encoding: "utf-8",
|
|
1773
2428
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -1781,7 +2436,7 @@ function doctor() {
|
|
|
1781
2436
|
return s;
|
|
1782
2437
|
};
|
|
1783
2438
|
for (const w of listWorktrees()) {
|
|
1784
|
-
if (!(0,
|
|
2439
|
+
if (!(0, import_node_fs12.existsSync)(w.path)) {
|
|
1785
2440
|
issues.push({ kind: "missing-worktree", slug: w.slug, detail: `path ${w.path} does not exist` });
|
|
1786
2441
|
continue;
|
|
1787
2442
|
}
|
|
@@ -1795,10 +2450,47 @@ function doctor() {
|
|
|
1795
2450
|
}
|
|
1796
2451
|
return issues;
|
|
1797
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
|
+
}
|
|
1798
2490
|
function findBlockingPids(path) {
|
|
1799
2491
|
let out;
|
|
1800
2492
|
try {
|
|
1801
|
-
out = (0,
|
|
2493
|
+
out = (0, import_node_child_process9.execFileSync)("lsof", ["-F", "pc", "+D", path], {
|
|
1802
2494
|
encoding: "utf-8",
|
|
1803
2495
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1804
2496
|
});
|
|
@@ -1866,22 +2558,22 @@ async function checkMergeable(opts) {
|
|
|
1866
2558
|
async function merge(opts) {
|
|
1867
2559
|
const start = Date.now();
|
|
1868
2560
|
const cleanup = opts.cleanup ?? "full";
|
|
1869
|
-
const
|
|
2561
|
+
const check2 = await checkMergeable({
|
|
1870
2562
|
branch: opts.branch,
|
|
1871
2563
|
target: opts.target,
|
|
1872
2564
|
skipGates: opts.skipGates,
|
|
1873
2565
|
projectRoot: opts.projectRoot,
|
|
1874
2566
|
logger: opts.logger
|
|
1875
2567
|
});
|
|
1876
|
-
if (!
|
|
1877
|
-
const blockers =
|
|
2568
|
+
if (!check2.passed) {
|
|
2569
|
+
const blockers = check2.gates.flatMap((g) => g.blockers);
|
|
1878
2570
|
opts.logger.error(`gates blocked the merge \u2014 fix the above and retry`);
|
|
1879
2571
|
return {
|
|
1880
|
-
slug:
|
|
1881
|
-
branch:
|
|
2572
|
+
slug: check2.slug,
|
|
2573
|
+
branch: check2.branch,
|
|
1882
2574
|
target: opts.target,
|
|
1883
2575
|
merged: false,
|
|
1884
|
-
gates:
|
|
2576
|
+
gates: check2.gates,
|
|
1885
2577
|
cleanedUp: false,
|
|
1886
2578
|
blockedBy: blockers,
|
|
1887
2579
|
durationMs: Date.now() - start
|
|
@@ -1890,70 +2582,72 @@ async function merge(opts) {
|
|
|
1890
2582
|
const targetWorktree = findWorktreeFor(opts.projectRoot, opts.target);
|
|
1891
2583
|
if (!targetWorktree) {
|
|
1892
2584
|
return {
|
|
1893
|
-
slug:
|
|
1894
|
-
branch:
|
|
2585
|
+
slug: check2.slug,
|
|
2586
|
+
branch: check2.branch,
|
|
1895
2587
|
target: opts.target,
|
|
1896
2588
|
merged: false,
|
|
1897
|
-
gates:
|
|
2589
|
+
gates: check2.gates,
|
|
1898
2590
|
cleanedUp: false,
|
|
1899
2591
|
blockedBy: [`no worktree currently has "${opts.target}" checked out \u2014 check it out in main and retry`],
|
|
1900
2592
|
durationMs: Date.now() - start
|
|
1901
2593
|
};
|
|
1902
2594
|
}
|
|
1903
|
-
opts.logger.step(`git merge --ff-only ${
|
|
1904
|
-
const mergeRes = (0,
|
|
2595
|
+
opts.logger.step(`git merge --ff-only ${check2.branch} (in ${targetWorktree})`);
|
|
2596
|
+
const mergeRes = (0, import_node_child_process9.spawnSync)(
|
|
1905
2597
|
"git",
|
|
1906
|
-
["merge", "--ff-only",
|
|
2598
|
+
["merge", "--ff-only", check2.branch],
|
|
1907
2599
|
{ cwd: targetWorktree, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
|
|
1908
2600
|
);
|
|
1909
2601
|
if (mergeRes.status !== 0) {
|
|
1910
2602
|
const stderr = (mergeRes.stderr ?? "").toString().trim();
|
|
1911
2603
|
opts.logger.warn(`ff-only failed (${stderr.split("\n")[0]}); attempting non-ff merge`);
|
|
1912
|
-
const merge2 = (0,
|
|
2604
|
+
const merge2 = (0, import_node_child_process9.spawnSync)(
|
|
1913
2605
|
"git",
|
|
1914
|
-
["merge", "--no-ff", "-m", `Merge orbit ${
|
|
2606
|
+
["merge", "--no-ff", "-m", `Merge orbit ${check2.slug} into ${opts.target}`, check2.branch],
|
|
1915
2607
|
{ cwd: targetWorktree, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
|
|
1916
2608
|
);
|
|
1917
2609
|
if (merge2.status !== 0) {
|
|
1918
2610
|
const detail = (merge2.stderr ?? merge2.stdout ?? "").toString().trim();
|
|
1919
2611
|
return {
|
|
1920
|
-
slug:
|
|
1921
|
-
branch:
|
|
2612
|
+
slug: check2.slug,
|
|
2613
|
+
branch: check2.branch,
|
|
1922
2614
|
target: opts.target,
|
|
1923
2615
|
merged: false,
|
|
1924
|
-
gates:
|
|
2616
|
+
gates: check2.gates,
|
|
1925
2617
|
cleanedUp: false,
|
|
1926
2618
|
blockedBy: [`git merge failed despite passing gates: ${detail.split("\n")[0] ?? "unknown"}`],
|
|
1927
2619
|
durationMs: Date.now() - start
|
|
1928
2620
|
};
|
|
1929
2621
|
}
|
|
1930
2622
|
}
|
|
1931
|
-
const mergeSha = (0,
|
|
2623
|
+
const mergeSha = (0, import_node_child_process9.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
1932
2624
|
cwd: targetWorktree,
|
|
1933
2625
|
encoding: "utf-8"
|
|
1934
2626
|
}).trim();
|
|
1935
|
-
opts.logger.ok(`merged ${
|
|
2627
|
+
opts.logger.ok(`merged ${check2.branch} \u2192 ${opts.target} (${mergeSha.slice(0, 7)})`);
|
|
1936
2628
|
let backupPath;
|
|
1937
2629
|
let cleanedUp = false;
|
|
1938
2630
|
if (cleanup === "full") {
|
|
1939
|
-
opts.logger.step(`cleanup: dropping orbit ${
|
|
2631
|
+
opts.logger.step(`cleanup: dropping orbit ${check2.slug}`);
|
|
1940
2632
|
const dropResult = await drop({
|
|
1941
2633
|
branch: opts.branch,
|
|
1942
2634
|
backup: true,
|
|
2635
|
+
force: true,
|
|
2636
|
+
// already gated by the merge gates above
|
|
1943
2637
|
projectRoot: opts.projectRoot,
|
|
1944
2638
|
logger: opts.logger
|
|
1945
2639
|
});
|
|
1946
2640
|
backupPath = dropResult.backups.db;
|
|
1947
2641
|
cleanedUp = true;
|
|
1948
|
-
opts.logger.ok(`cleaned up ${
|
|
2642
|
+
opts.logger.ok(`cleaned up ${check2.slug} (backup: ${backupPath ?? "n/a"})`);
|
|
1949
2643
|
}
|
|
1950
2644
|
return {
|
|
1951
|
-
slug:
|
|
1952
|
-
branch:
|
|
2645
|
+
slug: check2.slug,
|
|
2646
|
+
branch: check2.branch,
|
|
1953
2647
|
target: opts.target,
|
|
1954
2648
|
merged: true,
|
|
1955
2649
|
mergeSha,
|
|
1956
|
-
gates:
|
|
2650
|
+
gates: check2.gates,
|
|
1957
2651
|
cleanedUp,
|
|
1958
2652
|
backupPath,
|
|
1959
2653
|
durationMs: Date.now() - start
|
|
@@ -1961,7 +2655,7 @@ async function merge(opts) {
|
|
|
1961
2655
|
}
|
|
1962
2656
|
function findWorktreeFor(projectRoot, branch) {
|
|
1963
2657
|
try {
|
|
1964
|
-
const out = (0,
|
|
2658
|
+
const out = (0, import_node_child_process9.execFileSync)("git", ["worktree", "list", "--porcelain"], {
|
|
1965
2659
|
cwd: projectRoot,
|
|
1966
2660
|
encoding: "utf-8"
|
|
1967
2661
|
});
|
|
@@ -2018,18 +2712,20 @@ function formatAge(iso) {
|
|
|
2018
2712
|
const d = Math.floor(h / 24);
|
|
2019
2713
|
return `${d}d`;
|
|
2020
2714
|
}
|
|
2021
|
-
var
|
|
2715
|
+
var import_node_child_process9, import_node_fs12, import_node_path9, DEFAULT_ENV_FILE;
|
|
2022
2716
|
var init_orchestrator = __esm({
|
|
2023
2717
|
"src/server/orbit/orchestrator.ts"() {
|
|
2024
2718
|
"use strict";
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2719
|
+
import_node_child_process9 = require("node:child_process");
|
|
2720
|
+
import_node_fs12 = require("node:fs");
|
|
2721
|
+
import_node_path9 = require("node:path");
|
|
2028
2722
|
init_adapter_registry();
|
|
2723
|
+
init_cleanliness();
|
|
2029
2724
|
init_env_rewriter();
|
|
2030
2725
|
init_gate_runner();
|
|
2031
2726
|
init_manifest();
|
|
2032
2727
|
init_registry();
|
|
2728
|
+
init_active();
|
|
2033
2729
|
init_slug();
|
|
2034
2730
|
DEFAULT_ENV_FILE = ".env.local";
|
|
2035
2731
|
}
|
|
@@ -2093,16 +2789,23 @@ async function handleTool(name, args) {
|
|
|
2093
2789
|
const result = switchTo({ branch, projectRoot });
|
|
2094
2790
|
return text({
|
|
2095
2791
|
slug: result.entry.slug,
|
|
2792
|
+
branch: result.entry.branch,
|
|
2793
|
+
anchor: result.anchor,
|
|
2096
2794
|
path: result.path,
|
|
2097
2795
|
envFile: result.envFile,
|
|
2098
|
-
nextAction:
|
|
2796
|
+
nextAction: result.nextAction
|
|
2099
2797
|
});
|
|
2100
2798
|
}
|
|
2799
|
+
case "orbit.deactivate": {
|
|
2800
|
+
return text(deactivate({ projectRoot }));
|
|
2801
|
+
}
|
|
2101
2802
|
case "orbit.drop": {
|
|
2102
2803
|
const branch = String(args.branch ?? "");
|
|
2103
2804
|
if (!branch) return text({ error: "branch is required" });
|
|
2104
2805
|
const backup = args.backup === false ? false : true;
|
|
2105
|
-
const
|
|
2806
|
+
const force = args.force === true;
|
|
2807
|
+
const base = args.base;
|
|
2808
|
+
const result = await drop({ branch, backup, force, base, projectRoot, logger });
|
|
2106
2809
|
return text(result);
|
|
2107
2810
|
}
|
|
2108
2811
|
case "orbit.doctor": {
|
|
@@ -2118,6 +2821,17 @@ async function handleTool(name, args) {
|
|
|
2118
2821
|
const result = await checkMergeable({ branch, target, skipGates, projectRoot, logger });
|
|
2119
2822
|
return text(result);
|
|
2120
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
|
+
}
|
|
2121
2835
|
case "orbit.merge": {
|
|
2122
2836
|
const branch = String(args.branch ?? "");
|
|
2123
2837
|
const target = String(args.target ?? "");
|
|
@@ -2256,23 +2970,30 @@ var init_orbit_mcp = __esm({
|
|
|
2256
2970
|
},
|
|
2257
2971
|
{
|
|
2258
2972
|
name: "orbit.switch",
|
|
2259
|
-
description:
|
|
2973
|
+
description: 'Activate an orbit by branch name. Atomically repoints the stable anchor <project>/.launchsecure/orbit/current at the orbit\'s worktree and records active.json, then returns `anchor` (the current/ path) + `nextAction`. AFTER calling this, operate on the anchor: Read/Edit/Write under `.launchsecure/orbit/current/<path>`, pass `project_root: ".launchsecure/orbit/current"` to chart, and run the returned `nextAction` (cd into current) so commands execute in the orbit. Switching to a different orbit just repoints the anchor; re-run nextAction afterwards.',
|
|
2260
2974
|
inputSchema: {
|
|
2261
2975
|
type: "object",
|
|
2262
2976
|
properties: {
|
|
2263
|
-
branch: { type: "string", description: "Branch name to
|
|
2977
|
+
branch: { type: "string", description: "Branch name of the orbit to activate." }
|
|
2264
2978
|
},
|
|
2265
2979
|
required: ["branch"]
|
|
2266
2980
|
}
|
|
2267
2981
|
},
|
|
2982
|
+
{
|
|
2983
|
+
name: "orbit.deactivate",
|
|
2984
|
+
description: "Clear the active-orbit anchor for this project \u2014 removes <project>/.launchsecure/orbit/current and active.json so no orbit is active. Use when you're done working in an orbit and want subsequent work to target the main checkout. No-op if nothing is active.",
|
|
2985
|
+
inputSchema: { type: "object", properties: {} }
|
|
2986
|
+
},
|
|
2268
2987
|
{
|
|
2269
2988
|
name: "orbit.drop",
|
|
2270
|
-
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 }.",
|
|
2271
2990
|
inputSchema: {
|
|
2272
2991
|
type: "object",
|
|
2273
2992
|
properties: {
|
|
2274
2993
|
branch: { type: "string", description: "Branch name to drop." },
|
|
2275
|
-
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)." }
|
|
2276
2997
|
},
|
|
2277
2998
|
required: ["branch"]
|
|
2278
2999
|
}
|
|
@@ -2299,6 +3020,19 @@ var init_orbit_mcp = __esm({
|
|
|
2299
3020
|
required: ["branch", "target"]
|
|
2300
3021
|
}
|
|
2301
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
|
+
},
|
|
2302
3036
|
{
|
|
2303
3037
|
name: "orbit.merge",
|
|
2304
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).',
|
|
@@ -2326,18 +3060,23 @@ var init_orbit_mcp = __esm({
|
|
|
2326
3060
|
});
|
|
2327
3061
|
|
|
2328
3062
|
// src/server/orbit-entry.ts
|
|
2329
|
-
var
|
|
2330
|
-
var
|
|
3063
|
+
var import_node_fs13 = require("node:fs");
|
|
3064
|
+
var import_node_path10 = require("node:path");
|
|
2331
3065
|
init_orchestrator();
|
|
2332
3066
|
init_logger();
|
|
2333
3067
|
function parseFlags(argv) {
|
|
2334
3068
|
const positional = [];
|
|
2335
|
-
const flags = { emitCd: false, noBackup: false, skipGates: [] };
|
|
3069
|
+
const flags = { emitCd: false, noBackup: false, skipGates: [], deep: false, all: false, json: false, force: false };
|
|
2336
3070
|
for (let i = 0; i < argv.length; i++) {
|
|
2337
3071
|
const a = argv[i];
|
|
2338
3072
|
if (a === "--emit-cd") flags.emitCd = true;
|
|
2339
3073
|
else if (a === "--no-backup") flags.noBackup = true;
|
|
3074
|
+
else if (a === "--force") flags.force = true;
|
|
2340
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;
|
|
2341
3080
|
else if (a === "--profile") flags.profile = argv[++i];
|
|
2342
3081
|
else if (a === "--skip-gate") {
|
|
2343
3082
|
const id = argv[++i];
|
|
@@ -2413,20 +3152,30 @@ ${JSON.stringify(result, null, 2)}
|
|
|
2413
3152
|
if (!branch) die("usage: launch-orbit switch <branch> [--emit-cd]");
|
|
2414
3153
|
const result = switchTo({ branch, projectRoot });
|
|
2415
3154
|
if (flags.emitCd) {
|
|
2416
|
-
process.stdout.write(`cd ${result.
|
|
3155
|
+
process.stdout.write(`cd ${result.anchor}
|
|
2417
3156
|
`);
|
|
2418
3157
|
} else {
|
|
2419
|
-
process.stdout.write(
|
|
2420
|
-
|
|
3158
|
+
process.stdout.write(
|
|
3159
|
+
`${JSON.stringify({ anchor: result.anchor, path: result.path, envFile: result.envFile, nextAction: result.nextAction }, null, 2)}
|
|
3160
|
+
`
|
|
3161
|
+
);
|
|
2421
3162
|
}
|
|
2422
3163
|
return;
|
|
2423
3164
|
}
|
|
3165
|
+
case "deactivate": {
|
|
3166
|
+
const result = deactivate({ projectRoot });
|
|
3167
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
3168
|
+
`);
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
2424
3171
|
case "drop": {
|
|
2425
3172
|
const branch = positional[0];
|
|
2426
|
-
if (!branch) die("usage: launch-orbit drop <branch> [--no-backup]");
|
|
3173
|
+
if (!branch) die("usage: launch-orbit drop <branch> [--no-backup] [--force]");
|
|
2427
3174
|
const result = await drop({
|
|
2428
3175
|
branch,
|
|
2429
3176
|
backup: !flags.noBackup,
|
|
3177
|
+
force: flags.force,
|
|
3178
|
+
base: flags.base,
|
|
2430
3179
|
projectRoot,
|
|
2431
3180
|
logger
|
|
2432
3181
|
});
|
|
@@ -2486,6 +3235,23 @@ ${JSON.stringify(result, null, 2)}
|
|
|
2486
3235
|
if (!result.merged) process.exit(1);
|
|
2487
3236
|
return;
|
|
2488
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
|
+
}
|
|
2489
3255
|
default:
|
|
2490
3256
|
die(`unknown subcommand: ${subcommand}`);
|
|
2491
3257
|
}
|
|
@@ -2494,9 +3260,76 @@ ${JSON.stringify(result, null, 2)}
|
|
|
2494
3260
|
process.exit(1);
|
|
2495
3261
|
}
|
|
2496
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
|
+
}
|
|
2497
3330
|
function runInit(projectRoot) {
|
|
2498
|
-
const path = (0,
|
|
2499
|
-
if ((0,
|
|
3331
|
+
const path = (0, import_node_path10.join)(projectRoot, "orbit.json");
|
|
3332
|
+
if ((0, import_node_fs13.existsSync)(path)) {
|
|
2500
3333
|
process.stderr.write(`[launch-orbit] orbit.json already exists at ${path}
|
|
2501
3334
|
`);
|
|
2502
3335
|
process.exit(1);
|
|
@@ -2551,7 +3384,7 @@ function runInit(projectRoot) {
|
|
|
2551
3384
|
full: { resources: ["db", "ports"] }
|
|
2552
3385
|
}
|
|
2553
3386
|
};
|
|
2554
|
-
(0,
|
|
3387
|
+
(0, import_node_fs13.writeFileSync)(path, JSON.stringify(starter, null, 2) + "\n", "utf-8");
|
|
2555
3388
|
process.stdout.write(`\u2713 wrote ${path}
|
|
2556
3389
|
`);
|
|
2557
3390
|
}
|