@launchsecure/launch-kit 0.0.35 → 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.
@@ -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
- return { kind: "binary", pgDump: dumpOnPath, psql: psqlOnPath, version: probeVersion(dumpOnPath) };
365
- }
366
- if (dumpOnPath || psqlOnPath) {
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, import_node_fs6.existsSync)(srcPath)) {
1372
+ if (!(0, import_node_fs7.existsSync)(srcPath)) {
809
1373
  throw new Error(`env source file not found: ${srcPath}`);
810
1374
  }
811
- (0, import_node_fs6.copyFileSync)(srcPath, dstPath);
1375
+ (0, import_node_fs7.copyFileSync)(srcPath, dstPath);
812
1376
  }
813
1377
  function rewriteEnvFile(filePath, rewrites) {
814
- if (!(0, import_node_fs6.existsSync)(filePath)) {
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, import_node_fs6.readFileSync)(filePath, "utf-8");
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 } = stripQuotes(rawValue);
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, import_node_fs6.writeFileSync)(tmp, next, "utf-8");
844
- (0, import_node_fs6.renameSync)(tmp, filePath);
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 stripQuotes(raw) {
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 import_node_fs6, LINE_RE;
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
- import_node_fs6 = require("node:fs");
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, import_node_child_process5.execFileSync)("git", ["worktree", "remove", "--force", checkPath], {
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, import_node_fs7.rmSync)(checkPath, { recursive: true, force: true });
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, import_node_fs7.existsSync)((0, import_node_path5.join)(projectRoot, "pnpm-lock.yaml"))) return "pnpm install --prefer-offline";
891
- if ((0, import_node_fs7.existsSync)((0, import_node_path5.join)(projectRoot, "yarn.lock"))) return "yarn install --prefer-offline";
892
- if ((0, import_node_fs7.existsSync)((0, import_node_path5.join)(projectRoot, "bun.lockb"))) return "bun install";
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, import_node_path5.join)(worktreePath, "package.json");
903
- if (!(0, import_node_fs7.existsSync)(pkgPath)) return null;
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, import_node_fs7.readFileSync)(pkgPath, "utf-8"));
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, import_node_path5.join)(orbitPath, ".env.local");
916
- if (!(0, import_node_fs7.existsSync)(file)) return {};
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, import_node_fs7.readFileSync)(file, "utf-8");
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, import_node_fs7.existsSync)((0, import_node_path5.join)(worktreePath, "pnpm-lock.yaml"))) return "pnpm";
937
- if ((0, import_node_fs7.existsSync)((0, import_node_path5.join)(worktreePath, "yarn.lock"))) return "yarn";
938
- if ((0, import_node_fs7.existsSync)((0, import_node_path5.join)(worktreePath, "bun.lockb"))) return "bun";
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, import_node_path5.join)(LOG_DIR, `${ts}_${slug}_${label}.log`);
956
- (0, import_node_fs7.writeFileSync)(path, content, "utf-8");
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 import_node_child_process5, import_node_fs7, import_node_os3, import_node_path5, LOG_DIR, buildLintGate;
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
- import_node_child_process5 = require("node:child_process");
967
- import_node_fs7 = require("node:fs");
968
- import_node_os3 = require("node:os");
969
- import_node_path5 = require("node:path");
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, import_node_path5.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR, "orbit", "gate-logs");
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, import_node_child_process5.execFileSync)("git", ["--version"], { cwd: ctx.projectRoot, stdio: "ignore" });
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, import_node_fs7.mkdirSync)(LOG_DIR, { recursive: true });
985
- const checkPath = (0, import_node_path5.join)(ctx.projectRoot, ".claude", "worktrees", `__orbit-check-${ctx.entry.slug}__`);
986
- if ((0, import_node_fs7.existsSync)(checkPath)) {
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, import_node_child_process5.execFileSync)("git", ["worktree", "remove", "--force", checkPath], {
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, import_node_fs7.rmSync)(checkPath, { recursive: true, force: true });
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, import_node_child_process5.execFileSync)(
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, import_node_child_process5.spawnSync)(
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, import_node_fs7.existsSync)((0, import_node_path5.join)(checkPath, "node_modules"))) {
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, import_node_child_process5.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${installCmd}`], {
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, import_node_fs7.existsSync)((0, import_node_path5.join)(checkPath, "prisma", "schema.prisma"))) {
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, import_node_child_process5.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${prismaCmd}`], {
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, import_node_child_process5.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${tscCmd}`], {
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, import_node_child_process5.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${lintCmd}`], {
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, import_node_child_process5.spawnSync)("bash", ["-c", `cd ${shellQuote2(checkPath)} && ${buildCmd}`], {
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 import_node_child_process6, import_node_fs8, cleanTreeGate;
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
- import_node_child_process6 = require("node:child_process");
1131
- import_node_fs8 = require("node:fs");
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, import_node_fs8.existsSync)(ctx.entry.path)) {
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, import_node_child_process6.execFileSync)("git", ["rev-parse", "--is-inside-work-tree"], {
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, import_node_child_process6.execFileSync)("git", ["status", "--porcelain"], {
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 refExists(cwd, ref) {
1749
+ function refExists2(cwd, ref) {
1186
1750
  try {
1187
- (0, import_node_child_process7.execFileSync)("git", ["rev-parse", "--verify", ref], { cwd, stdio: "ignore" });
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 import_node_child_process7, mergeabilityGate;
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
- import_node_child_process7 = require("node:child_process");
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, import_node_child_process7.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
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 (!refExists(ctx.projectRoot, ctx.target)) {
1778
+ if (!refExists2(ctx.projectRoot, ctx.target)) {
1215
1779
  blockers.push(`target ref "${ctx.target}" does not exist`);
1216
1780
  }
1217
- if (!refExists(ctx.projectRoot, ctx.entry.branch)) {
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, import_node_child_process7.execFileSync)(
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 skip = new Set(options.skipGates ?? []);
1286
- if (skip.size > 0) {
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 skip) {
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 && skip.has(r.adapter)).map((r) => r.adapter);
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 (skip.has(ref.adapter)) {
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, import_node_path6.join)(projectRoot, DEFAULT_MANIFEST_FILENAME);
1928
+ return (0, import_node_path7.join)(projectRoot, DEFAULT_MANIFEST_FILENAME);
1365
1929
  }
1366
1930
  function manifestExists(projectRoot) {
1367
- return (0, import_node_fs9.existsSync)(findManifestPath(projectRoot));
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, import_node_fs9.existsSync)(path)) {
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, import_node_fs9.readFileSync)(path, "utf-8"));
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 import_node_fs9, import_node_path6, DEFAULT_MANIFEST_FILENAME;
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
- import_node_fs9 = require("node:fs");
1528
- import_node_path6 = require("node:path");
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, import_node_path7.join)(projectRoot, LAUNCHSECURE_DIR, "orbit");
1536
- return { dir, current: (0, import_node_path7.join)(dir, "current"), activeJson: (0, import_node_path7.join)(dir, "active.json") };
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, import_node_fs10.lstatSync)(p);
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, import_node_fs10.mkdirSync)(paths.dir, { recursive: true });
2112
+ (0, import_node_fs11.mkdirSync)(paths.dir, { recursive: true });
1549
2113
  const tmpLink = `${paths.current}.tmp.${process.pid}`;
1550
- if (present(tmpLink)) (0, import_node_fs10.rmSync)(tmpLink, { force: true });
1551
- (0, import_node_fs10.symlinkSync)(entry.path, tmpLink);
1552
- (0, import_node_fs10.renameSync)(tmpLink, paths.current);
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, import_node_path7.join)(entry.path, envFileName),
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, import_node_fs10.writeFileSync)(tmpJson, JSON.stringify(active, null, 2) + "\n", "utf-8");
1564
- (0, import_node_fs10.renameSync)(tmpJson, paths.activeJson);
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, import_node_fs10.rmSync)(p, { force: true });
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, import_node_fs10.readFileSync)(activeJson, "utf-8"));
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 import_node_fs10, import_node_path7;
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
- import_node_fs10 = require("node:fs");
1595
- import_node_path7 = require("node:path");
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, import_node_path8.join)(opts.projectRoot, "orbit.json"),
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, import_node_path8.join)(opts.projectRoot, envFileName);
1681
- const dstEnv = (0, import_node_path8.join)(worktreeState.path, envFileName);
1682
- if ((0, import_node_fs11.existsSync)(srcEnv) && !(0, import_node_fs11.existsSync)(dstEnv)) {
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, import_node_fs11.existsSync)(dstEnv) && Object.keys(rewriteFns).length > 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, import_node_path8.join)(__dirname, "graph-mcp-entry.js");
1707
- const result = (0, import_node_child_process8.spawnSync)(
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, import_node_path8.join)(opts.projectRoot, "orbit.json"),
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, import_node_fs11.existsSync)(entry.path)) {
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, import_node_fs11.existsSync)(entry.path)) {
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, import_node_fs11.rmSync)(entry.path, { recursive: true, force: true });
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, import_node_child_process8.execFileSync)("git", ["worktree", "list", "--porcelain"], {
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, import_node_fs11.existsSync)(w.path)) {
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, import_node_child_process8.execFileSync)("lsof", ["-F", "pc", "+D", path], {
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 check = await checkMergeable({
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 (!check.passed) {
1955
- const blockers = check.gates.flatMap((g) => g.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: check.slug,
1959
- branch: check.branch,
2572
+ slug: check2.slug,
2573
+ branch: check2.branch,
1960
2574
  target: opts.target,
1961
2575
  merged: false,
1962
- gates: check.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: check.slug,
1972
- branch: check.branch,
2585
+ slug: check2.slug,
2586
+ branch: check2.branch,
1973
2587
  target: opts.target,
1974
2588
  merged: false,
1975
- gates: check.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 ${check.branch} (in ${targetWorktree})`);
1982
- const mergeRes = (0, import_node_child_process8.spawnSync)(
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", check.branch],
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, import_node_child_process8.spawnSync)(
2604
+ const merge2 = (0, import_node_child_process9.spawnSync)(
1991
2605
  "git",
1992
- ["merge", "--no-ff", "-m", `Merge orbit ${check.slug} into ${opts.target}`, check.branch],
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: check.slug,
1999
- branch: check.branch,
2612
+ slug: check2.slug,
2613
+ branch: check2.branch,
2000
2614
  target: opts.target,
2001
2615
  merged: false,
2002
- gates: check.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, import_node_child_process8.execFileSync)("git", ["rev-parse", "HEAD"], {
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 ${check.branch} \u2192 ${opts.target} (${mergeSha.slice(0, 7)})`);
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 ${check.slug}`);
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 ${check.slug} (backup: ${backupPath ?? "n/a"})`);
2642
+ opts.logger.ok(`cleaned up ${check2.slug} (backup: ${backupPath ?? "n/a"})`);
2027
2643
  }
2028
2644
  return {
2029
- slug: check.slug,
2030
- branch: check.branch,
2645
+ slug: check2.slug,
2646
+ branch: check2.branch,
2031
2647
  target: opts.target,
2032
2648
  merged: true,
2033
2649
  mergeSha,
2034
- gates: check.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, import_node_child_process8.execFileSync)("git", ["worktree", "list", "--porcelain"], {
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 import_node_child_process8, import_node_fs11, import_node_path8, DEFAULT_ENV_FILE;
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
- import_node_child_process8 = require("node:child_process");
2104
- import_node_fs11 = require("node:fs");
2105
- import_node_path8 = require("node:path");
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 result = await drop({ branch, backup, projectRoot, logger });
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 import_node_fs12 = require("node:fs");
2419
- var import_node_path9 = require("node:path");
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, import_node_path9.join)(projectRoot, "orbit.json");
2596
- if ((0, import_node_fs12.existsSync)(path)) {
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, import_node_fs12.writeFileSync)(path, JSON.stringify(starter, null, 2) + "\n", "utf-8");
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
  }