@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.
@@ -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,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 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
 
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, import_node_path7.join)(opts.projectRoot, "orbit.json"),
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, import_node_path7.join)(opts.projectRoot, envFileName);
1614
- const dstEnv = (0, import_node_path7.join)(worktreeState.path, envFileName);
1615
- if ((0, import_node_fs10.existsSync)(srcEnv) && !(0, import_node_fs10.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)) {
1616
2247
  opts.logger.step(`copy ${envFileName} into worktree`);
1617
2248
  copyEnvFile(srcEnv, dstEnv);
1618
2249
  }
1619
- if ((0, import_node_fs10.existsSync)(dstEnv) && Object.keys(rewriteFns).length > 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, import_node_path7.join)(__dirname, "graph-mcp-entry.js");
1640
- 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)(
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, import_node_path7.join)(opts.projectRoot, "orbit.json"),
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, import_node_fs10.existsSync)(entry.path)) {
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, import_node_fs10.existsSync)(entry.path)) {
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, import_node_fs10.rmSync)(entry.path, { recursive: true, force: true });
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: (0, import_node_path7.join)(entry.path, DEFAULT_ENV_FILE),
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, import_node_child_process8.execFileSync)("git", ["worktree", "list", "--porcelain"], {
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, import_node_fs10.existsSync)(w.path)) {
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, import_node_child_process8.execFileSync)("lsof", ["-F", "pc", "+D", path], {
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 check = await checkMergeable({
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 (!check.passed) {
1877
- const blockers = check.gates.flatMap((g) => g.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: check.slug,
1881
- branch: check.branch,
2572
+ slug: check2.slug,
2573
+ branch: check2.branch,
1882
2574
  target: opts.target,
1883
2575
  merged: false,
1884
- gates: check.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: check.slug,
1894
- branch: check.branch,
2585
+ slug: check2.slug,
2586
+ branch: check2.branch,
1895
2587
  target: opts.target,
1896
2588
  merged: false,
1897
- gates: check.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 ${check.branch} (in ${targetWorktree})`);
1904
- 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)(
1905
2597
  "git",
1906
- ["merge", "--ff-only", check.branch],
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, import_node_child_process8.spawnSync)(
2604
+ const merge2 = (0, import_node_child_process9.spawnSync)(
1913
2605
  "git",
1914
- ["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],
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: check.slug,
1921
- branch: check.branch,
2612
+ slug: check2.slug,
2613
+ branch: check2.branch,
1922
2614
  target: opts.target,
1923
2615
  merged: false,
1924
- gates: check.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, import_node_child_process8.execFileSync)("git", ["rev-parse", "HEAD"], {
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 ${check.branch} \u2192 ${opts.target} (${mergeSha.slice(0, 7)})`);
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 ${check.slug}`);
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 ${check.slug} (backup: ${backupPath ?? "n/a"})`);
2642
+ opts.logger.ok(`cleaned up ${check2.slug} (backup: ${backupPath ?? "n/a"})`);
1949
2643
  }
1950
2644
  return {
1951
- slug: check.slug,
1952
- branch: check.branch,
2645
+ slug: check2.slug,
2646
+ branch: check2.branch,
1953
2647
  target: opts.target,
1954
2648
  merged: true,
1955
2649
  mergeSha,
1956
- gates: check.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, import_node_child_process8.execFileSync)("git", ["worktree", "list", "--porcelain"], {
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 import_node_child_process8, import_node_fs10, import_node_path7, DEFAULT_ENV_FILE;
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
- import_node_child_process8 = require("node:child_process");
2026
- import_node_fs10 = require("node:fs");
2027
- import_node_path7 = 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");
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: `cd ${result.path}`
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 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 });
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: "Look up the path + env file of an existing orbit by branch name. Does NOT change any shell \u2014 the caller is expected to `cd` into the returned path. Useful for agents that need to operate inside an already-created orbit.",
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 look up." }
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 import_node_fs11 = require("node:fs");
2330
- var import_node_path8 = require("node:path");
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.path}
3155
+ process.stdout.write(`cd ${result.anchor}
2417
3156
  `);
2418
3157
  } else {
2419
- process.stdout.write(`${JSON.stringify({ path: result.path, envFile: result.envFile, nextAction: `cd ${result.path}` }, null, 2)}
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, import_node_path8.join)(projectRoot, "orbit.json");
2499
- if ((0, import_node_fs11.existsSync)(path)) {
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, import_node_fs11.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");
2555
3388
  process.stdout.write(`\u2713 wrote ${path}
2556
3389
  `);
2557
3390
  }