iranti-control-plane 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1301,8 +1301,8 @@ var require_node = __commonJS({
1301
1301
  break;
1302
1302
  case "PIPE":
1303
1303
  case "TCP":
1304
- var net2 = require("net");
1305
- stream2 = new net2.Socket({
1304
+ var net3 = require("net");
1305
+ stream2 = new net3.Socket({
1306
1306
  fd: fd2,
1307
1307
  readable: false,
1308
1308
  writable: true
@@ -25933,8 +25933,8 @@ var require_stream = __commonJS({
25933
25933
  };
25934
25934
  function getNodejsStreamFuncs() {
25935
25935
  function getStream2(ssl) {
25936
- const net2 = require("net");
25937
- return new net2.Socket();
25936
+ const net3 = require("net");
25937
+ return new net3.Socket();
25938
25938
  }
25939
25939
  function getSecureStream2(options) {
25940
25940
  const tls = require("tls");
@@ -26056,8 +26056,8 @@ var require_connection = __commonJS({
26056
26056
  options.key = self.ssl.key;
26057
26057
  }
26058
26058
  }
26059
- const net2 = require("net");
26060
- if (net2.isIP && net2.isIP(host) === 0) {
26059
+ const net3 = require("net");
26060
+ if (net3.isIP && net3.isIP(host) === 0) {
26061
26061
  options.servername = host;
26062
26062
  }
26063
26063
  try {
@@ -27674,18 +27674,18 @@ var require_client2 = __commonJS({
27674
27674
  this._connecting = false;
27675
27675
  this._connected = false;
27676
27676
  this._queryable = true;
27677
- const cp = this.connectionParameters = new ConnectionParameters(config);
27678
- if (config.nativeConnectionString) cp.nativeConnectionString = config.nativeConnectionString;
27679
- this.user = cp.user;
27677
+ const cp2 = this.connectionParameters = new ConnectionParameters(config);
27678
+ if (config.nativeConnectionString) cp2.nativeConnectionString = config.nativeConnectionString;
27679
+ this.user = cp2.user;
27680
27680
  Object.defineProperty(this, "password", {
27681
27681
  configurable: true,
27682
27682
  enumerable: false,
27683
27683
  writable: true,
27684
- value: cp.password
27684
+ value: cp2.password
27685
27685
  });
27686
- this.database = cp.database;
27687
- this.host = cp.host;
27688
- this.port = cp.port;
27686
+ this.database = cp2.database;
27687
+ this.host = cp2.host;
27688
+ this.port = cp2.port;
27689
27689
  this.namedQueries = {};
27690
27690
  };
27691
27691
  Client2.Query = NativeQuery;
@@ -28595,6 +28595,12 @@ function runtimeRootCandidates() {
28595
28595
  add(joinPortable((0, import_os2.homedir)(), ".iranti"));
28596
28596
  return Array.from(candidates);
28597
28597
  }
28598
+ function classifyRuntimeRoot(runtimeRoot) {
28599
+ const leaf = basenamePortable(resolvePortable(runtimeRoot)).toLowerCase();
28600
+ if (leaf === ".iranti-runtime") return "primary";
28601
+ if (leaf === ".iranti") return "legacy";
28602
+ return "custom";
28603
+ }
28598
28604
 
28599
28605
  // src/server/lib/instance-authority.ts
28600
28606
  function deriveInstanceId(instanceDir) {
@@ -31706,7 +31712,135 @@ logsRouter.use((err, _req, res, _next) => {
31706
31712
  var import_express6 = __toESM(require_express2(), 1);
31707
31713
  var import_promises4 = require("fs/promises");
31708
31714
  var import_path7 = require("path");
31715
+ var net = __toESM(require("net"), 1);
31709
31716
  init_esm();
31717
+
31718
+ // src/server/lib/doctor-remediation.ts
31719
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
31720
+ function quoteCommandArg(value) {
31721
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
31722
+ }
31723
+ function buildIrantiDoctorCommand(scope) {
31724
+ const quotedRoot = quoteCommandArg(scope.runtimeRoot);
31725
+ return `iranti doctor --instance ${scope.instanceName} --root ${quotedRoot}`;
31726
+ }
31727
+ function buildDockerContainerName(scope) {
31728
+ const suffix = scope.instanceName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
31729
+ return suffix ? `iranti_pgvector_${suffix}` : "iranti_pgvector";
31730
+ }
31731
+ function parseDatabaseTarget(databaseUrl2) {
31732
+ if (!databaseUrl2) return null;
31733
+ try {
31734
+ const parsed = new URL(databaseUrl2);
31735
+ const host = parsed.hostname || null;
31736
+ return {
31737
+ host,
31738
+ port: parsed.port ? Number.parseInt(parsed.port, 10) : 5432,
31739
+ name: parsed.pathname.replace(/^\//, "") || null,
31740
+ user: parsed.username ? decodeURIComponent(parsed.username) : null,
31741
+ password: parsed.password ? decodeURIComponent(parsed.password) : null,
31742
+ isLocal: host ? LOCAL_HOSTS.has(host.toLowerCase()) : false
31743
+ };
31744
+ } catch {
31745
+ return null;
31746
+ }
31747
+ }
31748
+ function classifyPgvectorIssue(detail) {
31749
+ const normalized = (detail ?? "").trim().toLowerCase();
31750
+ if (/does not exist|unknown database|3d000/.test(normalized)) {
31751
+ return "database_missing";
31752
+ }
31753
+ if (/extension "vector" is not available|pgvector extension is not installed|does not have the pgvector extension installed|create extension if not exists vector|no pgvector/.test(
31754
+ normalized
31755
+ )) {
31756
+ return "missing_extension";
31757
+ }
31758
+ if (/unreachable|database not reachable|econnrefused|connect timeout|timed out|failed to connect|could not connect|connection terminated/.test(
31759
+ normalized
31760
+ )) {
31761
+ return "database_unreachable";
31762
+ }
31763
+ return "generic";
31764
+ }
31765
+ function buildCreateDatabaseCommand(target) {
31766
+ if (!target.host || !target.port || !target.user || !target.name) return null;
31767
+ return {
31768
+ label: "Create the missing database",
31769
+ command: `createdb -h ${quoteCommandArg(target.host)} -p ${target.port} -U ${quoteCommandArg(target.user)} ${quoteCommandArg(target.name)}`,
31770
+ allowRun: false
31771
+ };
31772
+ }
31773
+ function buildCreateExtensionCommand(target) {
31774
+ if (!target.host || !target.port || !target.user || !target.name) return null;
31775
+ return {
31776
+ label: "Enable pgvector in this database",
31777
+ command: `psql -h ${quoteCommandArg(target.host)} -p ${target.port} -U ${quoteCommandArg(target.user)} -d ${quoteCommandArg(target.name)} -c "CREATE EXTENSION IF NOT EXISTS vector;"`,
31778
+ allowRun: false
31779
+ };
31780
+ }
31781
+ function buildDockerRunCommand(scope, target) {
31782
+ if (!target.isLocal || !target.port || !target.name || !target.user) return null;
31783
+ const password = target.password && target.password.trim() !== "" ? target.password : "postgres";
31784
+ return {
31785
+ label: "Start a local pgvector database with Docker",
31786
+ command: `docker run -d --name ${buildDockerContainerName(scope)} -e POSTGRES_USER=${quoteCommandArg(target.user)} -e POSTGRES_PASSWORD=${quoteCommandArg(password)} -e POSTGRES_DB=${quoteCommandArg(target.name)} -p ${target.port}:5432 pgvector/pgvector:pg16`,
31787
+ allowRun: false
31788
+ };
31789
+ }
31790
+ function buildPgvectorRemediation(scope, detail) {
31791
+ const target = parseDatabaseTarget(scope.databaseUrl);
31792
+ const issue = classifyPgvectorIssue(detail);
31793
+ const commands = [];
31794
+ const doctorCommand = {
31795
+ label: "Re-run Iranti doctor",
31796
+ command: buildIrantiDoctorCommand(scope),
31797
+ allowRun: true
31798
+ };
31799
+ if (!target) {
31800
+ return {
31801
+ operatorNote: "This instance does not have a usable DATABASE_URL yet. The safest local path on macOS, Windows, and Linux is a pgvector Docker container plus a refreshed instance env.",
31802
+ commands: [doctorCommand]
31803
+ };
31804
+ }
31805
+ if (issue === "database_missing") {
31806
+ const createDb = buildCreateDatabaseCommand(target);
31807
+ const createExtension = buildCreateExtensionCommand(target);
31808
+ if (createDb) commands.push(createDb);
31809
+ if (createExtension) commands.push(createExtension);
31810
+ commands.push(doctorCommand);
31811
+ return {
31812
+ operatorNote: target.isLocal ? "The PostgreSQL server is reachable, but the target database does not exist yet. Create the database, enable pgvector, then rerun doctor. If you would rather avoid local PostgreSQL setup, switch this instance to a Docker-backed pgvector database." : "The configured PostgreSQL server is reachable, but the target database does not exist yet. Create the database, enable pgvector, then rerun doctor.",
31813
+ commands
31814
+ };
31815
+ }
31816
+ if (issue === "missing_extension") {
31817
+ const createExtension = buildCreateExtensionCommand(target);
31818
+ if (createExtension) commands.push(createExtension);
31819
+ commands.push(doctorCommand);
31820
+ return {
31821
+ operatorNote: target.isLocal ? "This PostgreSQL server is reachable, but it does not expose pgvector. If you can install extensions here, run the psql command below. Otherwise move this instance to a Docker-backed pgvector database." : "This instance points at a non-local PostgreSQL server. That server must provide the pgvector extension for the default vector backend.",
31822
+ commands
31823
+ };
31824
+ }
31825
+ if (issue === "database_unreachable") {
31826
+ const dockerCommand = buildDockerRunCommand(scope, target);
31827
+ if (dockerCommand) commands.push(dockerCommand);
31828
+ commands.push(doctorCommand);
31829
+ return {
31830
+ operatorNote: target.isLocal ? "Best cross-platform local fix: start a pgvector Docker container on the configured localhost port, then rerun doctor. The Docker command below works on macOS, Windows, and Linux." : "This instance points at a non-local PostgreSQL host and the pgvector backend is unreachable. Verify that the database server is online, reachable from this machine, and still matches DATABASE_URL.",
31831
+ commands
31832
+ };
31833
+ }
31834
+ const fallbackDockerCommand = buildDockerRunCommand(scope, target);
31835
+ if (fallbackDockerCommand) commands.push(fallbackDockerCommand);
31836
+ commands.push(doctorCommand);
31837
+ return {
31838
+ operatorNote: target.isLocal ? "Iranti could not complete the pgvector check cleanly. If the current local PostgreSQL server is unreliable, the Docker pgvector path below is the fastest cross-platform reset on macOS, Windows, and Linux." : "Iranti could not complete the pgvector check cleanly. Verify the configured PostgreSQL server, then rerun doctor.",
31839
+ commands
31840
+ };
31841
+ }
31842
+
31843
+ // src/server/routes/control-plane/setup.ts
31710
31844
  var { Pool: Pool5 } = esm_default;
31711
31845
  var setupRouter = (0, import_express6.Router)();
31712
31846
  function scopeSummary2(scope) {
@@ -31716,6 +31850,22 @@ function scopeSummary2(scope) {
31716
31850
  source: scope.source
31717
31851
  };
31718
31852
  }
31853
+ function primaryRuntimeRootHint(scope) {
31854
+ if (classifyRuntimeRoot(scope.runtimeRoot) !== "legacy") return "";
31855
+ return ` This instance lives under the legacy runtime root ${scope.runtimeRoot}. Newer Iranti instances usually live under ~/.iranti-runtime, so Control Plane is intentionally showing both roots.`;
31856
+ }
31857
+ function checkRuntimeRoot(scope) {
31858
+ if (classifyRuntimeRoot(scope.runtimeRoot) !== "legacy") return null;
31859
+ return {
31860
+ id: "runtime_root",
31861
+ label: "Instance storage",
31862
+ status: "warning",
31863
+ message: `This instance is stored under the legacy runtime root ${scope.runtimeRoot}.`,
31864
+ actionRequired: "Migrate this instance to the primary runtime root so new instances, runtime commands, and future repair flows all converge on the same home.",
31865
+ cliCommand: null,
31866
+ repairAction: "control-plane:migrate-root"
31867
+ };
31868
+ }
31719
31869
  async function resolveScopeOrThrow2(instanceRef) {
31720
31870
  const scope = await resolveInstanceAuthority(instanceRef);
31721
31871
  if (!scope) {
@@ -31756,6 +31906,23 @@ function normalizeProviderId3(value) {
31756
31906
  const normalized = value.trim().toLowerCase();
31757
31907
  return normalized === "anthropic" ? "claude" : normalized;
31758
31908
  }
31909
+ async function isTcpPortReachable(host, port, timeoutMs = 1200) {
31910
+ return await new Promise((resolve10) => {
31911
+ const socket = new net.Socket();
31912
+ let settled = false;
31913
+ const finish = (reachable) => {
31914
+ if (settled) return;
31915
+ settled = true;
31916
+ socket.destroy();
31917
+ resolve10(reachable);
31918
+ };
31919
+ socket.setTimeout(timeoutMs);
31920
+ socket.once("connect", () => finish(true));
31921
+ socket.once("timeout", () => finish(false));
31922
+ socket.once("error", () => finish(false));
31923
+ socket.connect(port, host);
31924
+ });
31925
+ }
31759
31926
  function providerLabel(providerId) {
31760
31927
  switch (normalizeProviderId3(providerId)) {
31761
31928
  case "claude":
@@ -31780,15 +31947,17 @@ function providerLabel(providerId) {
31780
31947
  }
31781
31948
  async function checkDatabase(scope, pool2) {
31782
31949
  const cliCommand = `iranti doctor --instance ${scope.instanceName} --debug`;
31950
+ const target = parseDatabaseTarget(scope.databaseUrl);
31951
+ const legacyRootNote = primaryRuntimeRootHint(scope);
31783
31952
  if (!pool2) {
31784
31953
  return {
31785
31954
  id: "database",
31786
31955
  label: "Database connection",
31787
31956
  status: "incomplete",
31788
31957
  message: "DATABASE_URL is not configured for this instance.",
31789
- actionRequired: `Add DATABASE_URL to ${scope.instanceEnvPath}, then restart the instance.`,
31958
+ actionRequired: `Add DATABASE_URL to ${scope.instanceEnvPath}, then restart the instance.${legacyRootNote}`,
31790
31959
  cliCommand,
31791
- repairAction: null
31960
+ repairAction: "control-plane:open-configure-db"
31792
31961
  };
31793
31962
  }
31794
31963
  try {
@@ -31814,14 +31983,29 @@ async function checkDatabase(scope, pool2) {
31814
31983
  repairAction: null
31815
31984
  };
31816
31985
  } catch {
31986
+ if (target?.isLocal && target.host && target.port) {
31987
+ const reachable = await isTcpPortReachable(target.host, target.port);
31988
+ if (!reachable) {
31989
+ const dbLabel = target.name ? `${target.host}:${target.port}/${target.name}` : `${target.host}:${target.port}`;
31990
+ return {
31991
+ id: "database",
31992
+ label: "Database connection",
31993
+ status: "incomplete",
31994
+ message: `Database not reachable for this instance. ${target.host}:${target.port} is not accepting connections.`,
31995
+ actionRequired: `DATABASE_URL in ${scope.instanceEnvPath} currently points to ${dbLabel}, but nothing is listening there right now. Start the database on that port or update DATABASE_URL to the actual database target, then rerun doctor.${legacyRootNote}`,
31996
+ cliCommand,
31997
+ repairAction: "control-plane:open-configure-db"
31998
+ };
31999
+ }
32000
+ }
31817
32001
  return {
31818
32002
  id: "database",
31819
32003
  label: "Database connection",
31820
32004
  status: "incomplete",
31821
32005
  message: "Database not reachable for this instance.",
31822
- actionRequired: `Verify DATABASE_URL in ${scope.instanceEnvPath}, then rerun doctor.`,
32006
+ actionRequired: `Verify DATABASE_URL in ${scope.instanceEnvPath}, then rerun doctor.${legacyRootNote}`,
31823
32007
  cliCommand,
31824
- repairAction: null
32008
+ repairAction: "control-plane:open-configure-db"
31825
32009
  };
31826
32010
  }
31827
32011
  }
@@ -31962,16 +32146,26 @@ function checkProjectIntegration(scope, projectStep) {
31962
32146
  }
31963
32147
  async function buildSetupStatus(scope) {
31964
32148
  const firstRunDetected = await readFirstRunDetected(scope);
32149
+ const runtimeRootKind = classifyRuntimeRoot(scope.runtimeRoot);
31965
32150
  return withScopedPool2(scope.databaseUrl, async (pool2) => {
32151
+ const runtimeRootStep = checkRuntimeRoot(scope);
31966
32152
  const databaseStep = await checkDatabase(scope, pool2);
31967
32153
  const providerStep = checkProvider(scope);
31968
32154
  const projectStep = checkProjectBinding(scope);
31969
32155
  const integrationStep = checkProjectIntegration(scope, projectStep);
31970
- const steps = [databaseStep, providerStep, projectStep, integrationStep];
32156
+ const steps = [
32157
+ ...runtimeRootStep ? [runtimeRootStep] : [],
32158
+ databaseStep,
32159
+ providerStep,
32160
+ projectStep,
32161
+ integrationStep
32162
+ ];
31971
32163
  const isFullyConfigured = steps.every((step) => step.status === "complete" || step.status === "not_applicable" || step.status === "warning");
31972
32164
  return {
31973
32165
  instanceId: scope.instanceId,
31974
32166
  scope: scopeSummary2(scope),
32167
+ runtimeRoot: scope.runtimeRoot,
32168
+ runtimeRootKind,
31975
32169
  steps,
31976
32170
  isFullyConfigured,
31977
32171
  firstRunDetected
@@ -32042,7 +32236,9 @@ async function doctorCheckRuntimeAvailability(scope) {
32042
32236
  label: "Runtime availability",
32043
32237
  status: "pass",
32044
32238
  message: `Iranti runtime is running and reachable at ${scope.apiBaseUrl}.`,
32045
- repairAction: null
32239
+ repairAction: null,
32240
+ operatorNote: null,
32241
+ commands: []
32046
32242
  };
32047
32243
  }
32048
32244
  return {
@@ -32050,7 +32246,9 @@ async function doctorCheckRuntimeAvailability(scope) {
32050
32246
  label: "Runtime availability",
32051
32247
  status: res.status >= 500 ? "fail" : "warn",
32052
32248
  message: `Iranti runtime responded with HTTP ${res.status}; runtime-only checks may be incomplete.`,
32053
- repairAction: null
32249
+ repairAction: null,
32250
+ operatorNote: null,
32251
+ commands: []
32054
32252
  };
32055
32253
  } catch {
32056
32254
  return {
@@ -32058,7 +32256,9 @@ async function doctorCheckRuntimeAvailability(scope) {
32058
32256
  label: "Runtime availability",
32059
32257
  status: "warn",
32060
32258
  message: `Iranti runtime is not running or not reachable at ${scope.apiBaseUrl}; runtime-only checks were skipped.`,
32061
- repairAction: null
32259
+ repairAction: null,
32260
+ operatorNote: null,
32261
+ commands: []
32062
32262
  };
32063
32263
  } finally {
32064
32264
  clearTimeout(timeout);
@@ -32260,18 +32460,20 @@ repairRouter.post(
32260
32460
  }
32261
32461
  }
32262
32462
  );
32263
- async function doctorCheckDatabase(databaseUrl2) {
32264
- if (!databaseUrl2) {
32463
+ async function doctorCheckDatabase(scope) {
32464
+ if (!scope.databaseUrl) {
32265
32465
  return {
32266
32466
  id: "database_reachability",
32267
32467
  label: "Database connection",
32268
32468
  status: "fail",
32269
32469
  message: "DATABASE_URL is not configured for this instance.",
32270
- repairAction: null
32470
+ repairAction: null,
32471
+ operatorNote: "This instance cannot reach PostgreSQL until DATABASE_URL is configured.",
32472
+ commands: []
32271
32473
  };
32272
32474
  }
32273
32475
  const pool2 = new Pool6({
32274
- connectionString: databaseUrl2,
32476
+ connectionString: scope.databaseUrl,
32275
32477
  max: 1,
32276
32478
  idleTimeoutMillis: 1e3,
32277
32479
  connectionTimeoutMillis: 2e3
@@ -32286,15 +32488,21 @@ async function doctorCheckDatabase(databaseUrl2) {
32286
32488
  label: "Database connection",
32287
32489
  status: "pass",
32288
32490
  message: "Database connection is healthy.",
32289
- repairAction: null
32491
+ repairAction: null,
32492
+ operatorNote: null,
32493
+ commands: []
32290
32494
  };
32291
- } catch {
32495
+ } catch (error) {
32496
+ const detail = error instanceof Error ? error.message : String(error);
32497
+ const remediation = buildPgvectorRemediation(scope, detail);
32292
32498
  return {
32293
32499
  id: "database_reachability",
32294
32500
  label: "Database connection",
32295
32501
  status: "fail",
32296
32502
  message: "Database not reachable for this instance. Check DATABASE_URL and the database service.",
32297
- repairAction: null
32503
+ repairAction: null,
32504
+ operatorNote: remediation.operatorNote,
32505
+ commands: remediation.commands
32298
32506
  };
32299
32507
  } finally {
32300
32508
  await pool2.end().catch(() => {
@@ -32310,7 +32518,9 @@ function doctorCheckProvider(scope) {
32310
32518
  label: "Provider configuration",
32311
32519
  status: hasKey ? "pass" : "warn",
32312
32520
  message: hasKey ? "At least one LLM provider key is configured." : "No LLM provider key found in this instance env. Configure a provider key, then restart Iranti.",
32313
- repairAction: null
32521
+ repairAction: null,
32522
+ operatorNote: null,
32523
+ commands: []
32314
32524
  };
32315
32525
  }
32316
32526
  function doctorCheckProjectBindings(scope) {
@@ -32319,7 +32529,9 @@ function doctorCheckProjectBindings(scope) {
32319
32529
  label: "Project bindings",
32320
32530
  status: scope.boundProjects.length > 0 ? "pass" : "warn",
32321
32531
  message: scope.boundProjects.length > 0 ? `${scope.boundProjects.length} bound project${scope.boundProjects.length === 1 ? "" : "s"} discovered for this instance.` : "No bound projects found for this instance. Project integration checks cannot be run until a project is bound.",
32322
- repairAction: null
32532
+ repairAction: null,
32533
+ operatorNote: null,
32534
+ commands: []
32323
32535
  };
32324
32536
  }
32325
32537
  function projectRepairUrl(scope, projectPath, kind) {
@@ -32335,14 +32547,18 @@ function doctorProjectChecks(scope) {
32335
32547
  label: `MCP integration (${projectName})`,
32336
32548
  status: integration.anyMcpPresent && integration.anyMcpHasIranti ? "pass" : "warn",
32337
32549
  message: integration.anyMcpPresent && integration.anyMcpHasIranti ? `A workspace MCP file is present and configured for ${project.projectPath}.` : `No workspace MCP file with an Iranti entry was found for ${project.projectPath}.`,
32338
- repairAction: integration.anyMcpPresent && integration.anyMcpHasIranti ? null : projectRepairUrl(scope, project.projectPath, "mcp-json")
32550
+ repairAction: integration.anyMcpPresent && integration.anyMcpHasIranti ? null : projectRepairUrl(scope, project.projectPath, "mcp-json"),
32551
+ operatorNote: null,
32552
+ commands: []
32339
32553
  };
32340
32554
  const claudeCheck = {
32341
32555
  id: `claude_md_integration:${project.projectPath}`,
32342
32556
  label: `CLAUDE.md integration (${projectName})`,
32343
32557
  status: integration.claudeMdPresent && integration.claudeMdHasIranti ? "pass" : "warn",
32344
32558
  message: integration.claudeMdPresent && integration.claudeMdHasIranti ? `CLAUDE.md references Iranti for ${project.projectPath}.` : `CLAUDE.md is missing or does not reference Iranti for ${project.projectPath}.`,
32345
- repairAction: integration.claudeMdPresent && integration.claudeMdHasIranti ? null : projectRepairUrl(scope, project.projectPath, "claude-md")
32559
+ repairAction: integration.claudeMdPresent && integration.claudeMdHasIranti ? null : projectRepairUrl(scope, project.projectPath, "claude-md"),
32560
+ operatorNote: null,
32561
+ commands: []
32346
32562
  };
32347
32563
  return [mcpCheck, claudeCheck];
32348
32564
  });
@@ -32350,20 +32566,23 @@ function doctorProjectChecks(scope) {
32350
32566
  function slugifyDoctorCheckId(value) {
32351
32567
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
32352
32568
  }
32353
- function mapCliDoctorChecks(payload) {
32569
+ function mapCliDoctorChecks(payload, scope) {
32354
32570
  const checks = payload.checks.map((check) => ({
32355
32571
  id: `iranti_doctor:${slugifyDoctorCheckId(check.name)}`,
32356
32572
  label: check.name,
32357
32573
  status: check.status,
32358
32574
  message: check.detail,
32359
- repairAction: null
32575
+ repairAction: null,
32576
+ ...check.name.toLowerCase().includes("vector backend") && (scope.env["IRANTI_VECTOR_BACKEND"] ?? "pgvector").trim().toLowerCase() === "pgvector" && check.status !== "pass" ? buildPgvectorRemediation(scope, check.detail) : { operatorNote: null, commands: [] }
32360
32577
  }));
32361
32578
  const remediations = (payload.remediations ?? []).map((detail, index) => ({
32362
32579
  id: `iranti_doctor:remediation_${index + 1}`,
32363
32580
  label: `Recommended remediation ${index + 1}`,
32364
32581
  status: payload.status === "fail" ? "fail" : "warn",
32365
32582
  message: detail,
32366
- repairAction: null
32583
+ repairAction: null,
32584
+ operatorNote: null,
32585
+ commands: []
32367
32586
  }));
32368
32587
  return [...checks, ...remediations];
32369
32588
  }
@@ -32372,7 +32591,7 @@ repairRouter.post("/:instanceId/doctor", async (req, res, next) => {
32372
32591
  const scope = await resolveScopeOr404(req.params["instanceId"], res);
32373
32592
  if (!scope) return;
32374
32593
  const [dbCheck, runtimeCheck] = await Promise.all([
32375
- doctorCheckDatabase(scope.databaseUrl),
32594
+ doctorCheckDatabase(scope),
32376
32595
  doctorCheckRuntimeAvailability(scope)
32377
32596
  ]);
32378
32597
  let checks;
@@ -32385,7 +32604,7 @@ repairRouter.post("/:instanceId/doctor", async (req, res, next) => {
32385
32604
  scope.runtimeRoot,
32386
32605
  "--json"
32387
32606
  ], { allowNonZeroExit: true });
32388
- checks = mapCliDoctorChecks(doctor.json);
32607
+ checks = mapCliDoctorChecks(doctor.json, scope);
32389
32608
  } catch (error) {
32390
32609
  checks = [
32391
32610
  {
@@ -32393,7 +32612,9 @@ repairRouter.post("/:instanceId/doctor", async (req, res, next) => {
32393
32612
  label: "Iranti doctor",
32394
32613
  status: "warn",
32395
32614
  message: error instanceof Error ? error.message : String(error),
32396
- repairAction: null
32615
+ repairAction: null,
32616
+ operatorNote: null,
32617
+ commands: []
32397
32618
  },
32398
32619
  runtimeCheck,
32399
32620
  dbCheck,
@@ -34726,6 +34947,15 @@ var import_express14 = __toESM(require_express2(), 1);
34726
34947
  init_esm();
34727
34948
  var diagnosticsRouter = (0, import_express14.Router)();
34728
34949
  var { Pool: Pool7 } = esm_default;
34950
+ var DIAGNOSTIC_ENTITY_TYPE = "__diagnostics__";
34951
+ var DIAGNOSTIC_ENTITY_ID = "__probe__";
34952
+ var ROUNDTRIP_KEY = "roundtrip_marker";
34953
+ var ROUNDTRIP_VALUE = "control-plane-roundtrip-ok";
34954
+ var ROUNDTRIP_SUMMARY = "Stable control-plane round-trip probe";
34955
+ var VECTOR_PROBE_KEY = "semantic_probe";
34956
+ var VECTOR_PROBE_VALUE = "Shared memory helps research teams preserve handoffs and working context.";
34957
+ var VECTOR_PROBE_SUMMARY = "Semantic probe for vector-search diagnostics";
34958
+ var VECTOR_PROBE_QUERY = "team memory for research workflows";
34729
34959
  var lastDiagnosticResult = /* @__PURE__ */ new Map();
34730
34960
  function scopeSummary4(scope) {
34731
34961
  return {
@@ -34753,6 +34983,9 @@ function buildRunCommand(scope) {
34753
34983
  const quotedRoot = /\s/.test(scope.runtimeRoot) ? `"${scope.runtimeRoot}"` : scope.runtimeRoot;
34754
34984
  return `iranti run --instance ${scope.instanceName} --root ${quotedRoot}`;
34755
34985
  }
34986
+ function buildKbQueryUrl(scope, key) {
34987
+ return `${scope.apiBaseUrl}/kb/query/${encodeURIComponent(DIAGNOSTIC_ENTITY_TYPE)}/${encodeURIComponent(DIAGNOSTIC_ENTITY_ID)}/${encodeURIComponent(key)}`;
34988
+ }
34756
34989
  async function withScopedPool3(databaseUrl2, fn) {
34757
34990
  if (!databaseUrl2) return fn(null);
34758
34991
  const pool2 = new Pool7({
@@ -34942,46 +35175,37 @@ async function checkVectorBackend2(scope, pool2) {
34942
35175
  };
34943
35176
  return withTimeout(work(), 3e3, "vector_backend");
34944
35177
  }
34945
- function extractProbeValueFromSearch(body) {
35178
+ function extractExactQueryValue(body) {
34946
35179
  if (body === null || typeof body !== "object") return null;
34947
35180
  const obj = body;
34948
- const items = Array.isArray(obj["results"]) ? obj["results"] : Array.isArray(obj["facts"]) ? obj["facts"] : Array.isArray(obj["items"]) ? obj["items"] : [];
34949
- for (const item of items) {
34950
- if (!item || typeof item !== "object") continue;
34951
- const fact = item;
34952
- if (!String(fact["entity"] ?? "").startsWith("__diagnostics__")) continue;
34953
- if (fact["key"] !== "probe_timestamp") continue;
34954
- const value = fact["value"] ?? fact["valueRaw"] ?? fact["valueSummary"];
34955
- if (value !== null && value !== void 0) return String(value);
34956
- }
34957
- return null;
34958
- }
34959
- async function deleteProbeFact(scope) {
34960
- try {
34961
- await fetch(`${scope.apiBaseUrl}/kb/delete?entity=__diagnostics__/__probe__&key=probe_timestamp`, {
34962
- method: "DELETE",
34963
- headers: buildHeaders4(scope)
34964
- });
34965
- } catch {
34966
- }
35181
+ if (obj["found"] !== true) return null;
35182
+ const raw = obj["value"];
35183
+ if (raw === null || raw === void 0) return null;
35184
+ if (typeof raw === "string") return raw;
35185
+ if (typeof raw === "object" && raw !== null && typeof raw["text"] === "string") {
35186
+ return String(raw["text"]);
35187
+ }
35188
+ return String(raw);
35189
+ }
35190
+ async function writeDiagnosticFact(scope, key, value, summary) {
35191
+ return fetch(`${scope.apiBaseUrl}/kb/write`, {
35192
+ method: "POST",
35193
+ headers: buildHeaders4(scope),
35194
+ body: JSON.stringify({
35195
+ entity: `${DIAGNOSTIC_ENTITY_TYPE}/${DIAGNOSTIC_ENTITY_ID}`,
35196
+ agent: "control_plane_operator",
35197
+ key,
35198
+ value,
35199
+ summary,
35200
+ confidence: 50,
35201
+ source: "control_plane_diagnostics"
35202
+ })
35203
+ });
34967
35204
  }
34968
35205
  async function checkIngestRoundtrip(scope) {
34969
35206
  const start = Date.now();
34970
- const probeTimestamp = (/* @__PURE__ */ new Date()).toISOString();
34971
35207
  const work = async () => {
34972
- const writeRes = await fetch(`${scope.apiBaseUrl}/kb/write`, {
34973
- method: "POST",
34974
- headers: buildHeaders4(scope),
34975
- body: JSON.stringify({
34976
- entity: "__diagnostics__/__probe__",
34977
- agent: "control_plane_operator",
34978
- key: "probe_timestamp",
34979
- value: probeTimestamp,
34980
- summary: "Diagnostic probe",
34981
- confidence: 50,
34982
- source: "control_plane_diagnostics"
34983
- })
34984
- });
35208
+ const writeRes = await writeDiagnosticFact(scope, ROUNDTRIP_KEY, ROUNDTRIP_VALUE, ROUNDTRIP_SUMMARY);
34985
35209
  if (!writeRes.ok) {
34986
35210
  return {
34987
35211
  check: "ingest_roundtrip",
@@ -34991,27 +35215,25 @@ async function checkIngestRoundtrip(scope) {
34991
35215
  durationMs: Date.now() - start
34992
35216
  };
34993
35217
  }
34994
- const queryRes = await fetch(`${scope.apiBaseUrl}/kb/search?query=probe_timestamp&limit=5`, {
35218
+ const queryRes = await fetch(buildKbQueryUrl(scope, ROUNDTRIP_KEY), {
34995
35219
  method: "GET",
34996
35220
  headers: buildHeaders4(scope)
34997
35221
  });
34998
35222
  if (!queryRes.ok) {
34999
- await deleteProbeFact(scope);
35000
35223
  return {
35001
35224
  check: "ingest_roundtrip",
35002
35225
  status: "fail",
35003
- message: `Read probe failed: GET /kb/search returned HTTP ${queryRes.status}`,
35226
+ message: `Read probe failed: GET /kb/query returned HTTP ${queryRes.status}`,
35004
35227
  fixHint: null,
35005
35228
  durationMs: Date.now() - start
35006
35229
  };
35007
35230
  }
35008
35231
  const body = await queryRes.json();
35009
- await deleteProbeFact(scope);
35010
- const value = extractProbeValueFromSearch(body);
35232
+ const value = extractExactQueryValue(body);
35011
35233
  return {
35012
35234
  check: "ingest_roundtrip",
35013
- status: value !== null ? "pass" : "fail",
35014
- message: value !== null ? "Ingest round-trip succeeded" : "Probe fact was written but could not be read back",
35235
+ status: value === ROUNDTRIP_VALUE ? "pass" : "fail",
35236
+ message: value === ROUNDTRIP_VALUE ? "Ingest round-trip succeeded" : "Probe fact was written but could not be read back exactly",
35015
35237
  fixHint: null,
35016
35238
  durationMs: Date.now() - start
35017
35239
  };
@@ -35075,7 +35297,17 @@ function extractSearchItems(body) {
35075
35297
  async function checkVectorSearch(scope) {
35076
35298
  const start = Date.now();
35077
35299
  const work = async () => {
35078
- const res = await fetch(`${scope.apiBaseUrl}/kb/search?query=diagnostic+probe&limit=1`, {
35300
+ const writeRes = await writeDiagnosticFact(scope, VECTOR_PROBE_KEY, VECTOR_PROBE_VALUE, VECTOR_PROBE_SUMMARY);
35301
+ if (!writeRes.ok) {
35302
+ return {
35303
+ check: "vector_search_check",
35304
+ status: "fail",
35305
+ message: `Write probe failed: POST /kb/write returned HTTP ${writeRes.status}`,
35306
+ fixHint: `Check API key scopes for ${scope.instanceName}.`,
35307
+ durationMs: Date.now() - start
35308
+ };
35309
+ }
35310
+ const res = await fetch(`${scope.apiBaseUrl}/kb/search?query=${encodeURIComponent(VECTOR_PROBE_QUERY)}&limit=5`, {
35079
35311
  method: "GET",
35080
35312
  headers: buildHeaders4(scope)
35081
35313
  });
@@ -35088,22 +35320,31 @@ async function checkVectorSearch(scope) {
35088
35320
  durationMs: Date.now() - start
35089
35321
  };
35090
35322
  }
35323
+ let matchedProbe = false;
35091
35324
  let hasVectorScore = false;
35092
35325
  try {
35093
35326
  const body = await res.json();
35094
- hasVectorScore = extractSearchItems(body).some((item) => {
35095
- if (!item || typeof item !== "object") return false;
35096
- const score = item["vectorScore"];
35097
- return typeof score === "number" && score > 0;
35098
- });
35327
+ const items = extractSearchItems(body);
35328
+ for (const item of items) {
35329
+ if (!item || typeof item !== "object") continue;
35330
+ const fact = item;
35331
+ const entity = String(fact["entity"] ?? "");
35332
+ const key = String(fact["key"] ?? "");
35333
+ if (entity !== `${DIAGNOSTIC_ENTITY_TYPE}/${DIAGNOSTIC_ENTITY_ID}` || key !== VECTOR_PROBE_KEY) continue;
35334
+ matchedProbe = true;
35335
+ const score = fact["vectorScore"];
35336
+ hasVectorScore = typeof score === "number" && score > 0;
35337
+ break;
35338
+ }
35099
35339
  } catch {
35340
+ matchedProbe = true;
35100
35341
  hasVectorScore = true;
35101
35342
  }
35102
35343
  return {
35103
35344
  check: "vector_search_check",
35104
- status: hasVectorScore ? "pass" : "warn",
35105
- message: hasVectorScore ? "Vector search returned results with non-zero vectorScore" : "Vector search returned 200 but vectorScore=0 for all results",
35106
- fixHint: null,
35345
+ status: matchedProbe && hasVectorScore ? "pass" : "warn",
35346
+ message: matchedProbe && hasVectorScore ? "Semantic vector probe returned a non-zero vectorScore" : matchedProbe ? "Semantic probe surfaced, but only through lexical matching (vectorScore=0)" : "Semantic vector probe did not appear in hybrid search results",
35347
+ fixHint: matchedProbe && hasVectorScore ? null : matchedProbe ? "Vector backend is reachable, so this usually means embeddings are stale or the semantic probe is underweighted. Re-run doctor and inspect vector index consistency if real search feels weak." : "Vector backend is reachable, but semantic ranking did not surface the known probe. Re-run doctor and inspect vector index consistency if this persists.",
35107
35348
  durationMs: Date.now() - start
35108
35349
  };
35109
35350
  };
@@ -36444,6 +36685,44 @@ function createLifecycleError(code, message, detail) {
36444
36685
  function sleep(ms) {
36445
36686
  return new Promise((resolve10) => setTimeout(resolve10, ms));
36446
36687
  }
36688
+ async function readInstanceStatus(name, runtimeRoot, timeoutMs) {
36689
+ const status = await runIrantiJson(["status", "--root", runtimeRoot, "--json"], {
36690
+ timeoutMs
36691
+ });
36692
+ return status.json.instances.find((entry) => entry.name === name) ?? null;
36693
+ }
36694
+ async function waitForRuntimeReady(name, runtimeRoot) {
36695
+ const timeoutMs = toPositiveInteger(process.env["IRANTI_CP_START_CONFIRM_TIMEOUT_MS"], 15e3);
36696
+ const intervalMs = toPositiveInteger(process.env["IRANTI_CP_START_CONFIRM_POLL_MS"], 500);
36697
+ const statusTimeoutMs = Math.min(timeoutMs, 5e3);
36698
+ const deadline = Date.now() + timeoutMs;
36699
+ let lastObserved = "no runtime status observed";
36700
+ let lastClassification = "missing";
36701
+ while (Date.now() <= deadline) {
36702
+ try {
36703
+ const instance = await readInstanceStatus(name, runtimeRoot, statusTimeoutMs);
36704
+ if (!instance) {
36705
+ lastObserved = `instance '${name}' not reported by iranti status`;
36706
+ lastClassification = "missing";
36707
+ } else {
36708
+ lastObserved = instance.runtime.detail;
36709
+ lastClassification = instance.runtime.classification;
36710
+ if (instance.runtime.classification === "running" || instance.runtime.classification === "unhealthy") {
36711
+ return instance;
36712
+ }
36713
+ }
36714
+ } catch (error) {
36715
+ lastObserved = error instanceof Error ? error.message : String(error);
36716
+ lastClassification = "unknown";
36717
+ }
36718
+ await sleep(intervalMs);
36719
+ }
36720
+ throw createLifecycleError(
36721
+ "START_CONFIRMATION_TIMEOUT",
36722
+ `Iranti instance '${name}' did not become ready within ${Math.ceil(timeoutMs / 1e3)}s after recovery.`,
36723
+ { runtimeRoot, classification: lastClassification, detail: lastObserved }
36724
+ );
36725
+ }
36447
36726
  async function waitForConfirmedStart(name, runtimeRoot, pid, child) {
36448
36727
  const timeoutMs = toPositiveInteger(process.env["IRANTI_CP_START_CONFIRM_TIMEOUT_MS"], 15e3);
36449
36728
  const intervalMs = toPositiveInteger(process.env["IRANTI_CP_START_CONFIRM_POLL_MS"], 500);
@@ -36460,10 +36739,7 @@ async function waitForConfirmedStart(name, runtimeRoot, pid, child) {
36460
36739
  );
36461
36740
  }
36462
36741
  try {
36463
- const status = await runIrantiJson(["status", "--root", runtimeRoot, "--json"], {
36464
- timeoutMs: statusTimeoutMs
36465
- });
36466
- const instance = status.json.instances.find((entry) => entry.name === name);
36742
+ const instance = await readInstanceStatus(name, runtimeRoot, statusTimeoutMs);
36467
36743
  if (!instance) {
36468
36744
  lastObserved = `instance '${name}' not reported by iranti status`;
36469
36745
  lastClassification = "missing";
@@ -36473,13 +36749,6 @@ async function waitForConfirmedStart(name, runtimeRoot, pid, child) {
36473
36749
  if (instance.runtime.classification === "running" && instance.runtime.running && instance.runtime.state?.pid === pid) {
36474
36750
  return instance;
36475
36751
  }
36476
- if (instance.runtime.classification === "invalid") {
36477
- throw createLifecycleError(
36478
- "START_RUNTIME_INVALID",
36479
- `Iranti reported invalid runtime state for '${name}' after spawn: ${instance.runtime.detail}`,
36480
- { pid, runtimeRoot, classification: instance.runtime.classification, detail: instance.runtime.detail }
36481
- );
36482
- }
36483
36752
  }
36484
36753
  } catch (error) {
36485
36754
  if (typeof error === "object" && error && "code" in error && typeof error.code === "string") {
@@ -36491,11 +36760,17 @@ async function waitForConfirmedStart(name, runtimeRoot, pid, child) {
36491
36760
  await sleep(intervalMs);
36492
36761
  }
36493
36762
  throw createLifecycleError(
36494
- "START_CONFIRMATION_TIMEOUT",
36495
- `Iranti instance '${name}' did not become running within ${Math.ceil(timeoutMs / 1e3)}s after spawn.`,
36763
+ lastClassification === "invalid" ? "START_RUNTIME_INVALID" : "START_CONFIRMATION_TIMEOUT",
36764
+ lastClassification === "invalid" ? `Iranti reported invalid runtime state for '${name}' after spawn: ${lastObserved}` : `Iranti instance '${name}' did not become running within ${Math.ceil(timeoutMs / 1e3)}s after spawn.`,
36496
36765
  { pid, runtimeRoot, classification: lastClassification, detail: lastObserved }
36497
36766
  );
36498
36767
  }
36768
+ async function recoverInvalidRuntime(name, runtimeRoot) {
36769
+ await runIrantiCommand(["instance", "restart", name, "--root", runtimeRoot], {
36770
+ timeoutMs: 6e4
36771
+ });
36772
+ return waitForRuntimeReady(name, runtimeRoot);
36773
+ }
36499
36774
  lifecycleRouter.post("/:name/start", async (req, res) => {
36500
36775
  const { name } = req.params;
36501
36776
  if (!isValidInstanceName(name)) {
@@ -36558,20 +36833,37 @@ lifecycleRouter.post("/:name/start", async (req, res) => {
36558
36833
  return;
36559
36834
  }
36560
36835
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
36561
- const confirmed = await waitForConfirmedStart(name, runtimeRoot, pid, child);
36562
- managedProcesses.set(name, { child, pid, startedAt });
36836
+ let confirmed;
36837
+ let recovered = false;
36838
+ try {
36839
+ confirmed = await waitForConfirmedStart(name, runtimeRoot, pid, child);
36840
+ } catch (error) {
36841
+ const code = typeof error === "object" && error && "code" in error ? String(error.code ?? "") : "";
36842
+ if (code !== "START_RUNTIME_INVALID") throw error;
36843
+ confirmed = await recoverInvalidRuntime(name, runtimeRoot);
36844
+ recovered = true;
36845
+ }
36846
+ if (!recovered) {
36847
+ managedProcesses.set(name, { child, pid, startedAt });
36848
+ } else {
36849
+ managedProcesses.delete(name);
36850
+ }
36563
36851
  auditLog("START", name, {
36564
36852
  pid,
36565
36853
  startedAt,
36566
36854
  runtimeRoot,
36567
36855
  runtimeClassification: confirmed.runtime.classification,
36568
- runtimeDetail: confirmed.runtime.detail
36856
+ runtimeDetail: confirmed.runtime.detail,
36857
+ recovered
36569
36858
  });
36570
36859
  const response = {
36571
36860
  instanceName: name,
36572
36861
  pid,
36573
36862
  status: "started",
36574
- startedAt
36863
+ startedAt,
36864
+ recovered: recovered || void 0,
36865
+ recoveryAction: recovered ? "restart" : void 0,
36866
+ note: recovered ? "Control Plane detected invalid runtime metadata after spawn and recovered the instance with a restart." : void 0
36575
36867
  };
36576
36868
  res.status(200).json(response);
36577
36869
  } catch (err) {
@@ -37086,6 +37378,9 @@ function parseEnvFile2(filePath) {
37086
37378
  }
37087
37379
  return parsed;
37088
37380
  }
37381
+ function buildSimpleEnvFile(entries) {
37382
+ return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join("\n") + "\n";
37383
+ }
37089
37384
  function diffEnvKeys(before, after) {
37090
37385
  const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
37091
37386
  return Array.from(keys).filter((key) => before[key] !== after[key]).sort();
@@ -37139,10 +37434,62 @@ function instancePaths(runtimeRoot, name) {
37139
37434
  envFile: (0, import_path13.join)(instanceDir, ".env")
37140
37435
  };
37141
37436
  }
37437
+ async function moveDirectory(fromPath, toPath) {
37438
+ try {
37439
+ await (0, import_promises7.rename)(fromPath, toPath);
37440
+ } catch (error) {
37441
+ const code = error.code;
37442
+ if (code !== "EXDEV") throw error;
37443
+ await (0, import_promises7.cp)(fromPath, toPath, { recursive: true, force: true });
37444
+ await (0, import_promises7.rm)(fromPath, { recursive: true, force: true });
37445
+ }
37446
+ }
37447
+ async function rewriteProjectBindingInstanceEnv(projectPath, oldEnvPath, newEnvPath) {
37448
+ const bindingPath = (0, import_path13.join)(projectPath, ".env.iranti");
37449
+ if (!(0, import_fs8.existsSync)(bindingPath)) return;
37450
+ const parsed = parseSimpleEnv2(await (0, import_promises7.readFile)(bindingPath, "utf8"));
37451
+ const current = parsed["IRANTI_INSTANCE_ENV"]?.trim() ?? "";
37452
+ if (!current) return;
37453
+ if ((0, import_path13.resolve)(current) !== (0, import_path13.resolve)(oldEnvPath)) return;
37454
+ parsed["IRANTI_INSTANCE_ENV"] = newEnvPath;
37455
+ await (0, import_promises7.writeFile)(bindingPath, buildSimpleEnvFile(parsed), "utf8");
37456
+ }
37457
+ async function rewriteMovedInstanceEnv(instanceDir, previousInstanceDir) {
37458
+ const envPath = (0, import_path13.join)(instanceDir, ".env");
37459
+ if (!(0, import_fs8.existsSync)(envPath)) return;
37460
+ const parsed = parseSimpleEnv2(await (0, import_promises7.readFile)(envPath, "utf8"));
37461
+ const oldBase = (0, import_path13.resolve)(previousInstanceDir);
37462
+ const nextEscalation = (0, import_path13.join)(instanceDir, "escalation");
37463
+ const nextRequestLog = (0, import_path13.join)(instanceDir, "logs", "api-requests.log");
37464
+ const currentEscalation = parsed["IRANTI_ESCALATION_DIR"]?.trim() ?? "";
37465
+ const currentRequestLog = parsed["IRANTI_REQUEST_LOG_FILE"]?.trim() ?? "";
37466
+ if (!currentEscalation || (0, import_path13.resolve)(currentEscalation).startsWith(oldBase)) {
37467
+ parsed["IRANTI_ESCALATION_DIR"] = nextEscalation;
37468
+ }
37469
+ if (!currentRequestLog || (0, import_path13.resolve)((0, import_path13.dirname)(currentRequestLog)).startsWith((0, import_path13.resolve)((0, import_path13.join)(previousInstanceDir, "logs")))) {
37470
+ parsed["IRANTI_REQUEST_LOG_FILE"] = nextRequestLog;
37471
+ }
37472
+ await (0, import_promises7.writeFile)(envPath, buildSimpleEnvFile(parsed), "utf8");
37473
+ }
37474
+ async function rewriteMovedInstanceMeta(instanceDir, name) {
37475
+ const metaPath = (0, import_path13.join)(instanceDir, "instance.json");
37476
+ if (!(0, import_fs8.existsSync)(metaPath)) return;
37477
+ const parsed = JSON.parse(await (0, import_promises7.readFile)(metaPath, "utf8"));
37478
+ parsed["name"] = name;
37479
+ parsed["instanceDir"] = instanceDir;
37480
+ parsed["envFile"] = (0, import_path13.join)(instanceDir, ".env");
37481
+ await (0, import_promises7.writeFile)(metaPath, `${JSON.stringify(parsed, null, 2)}
37482
+ `, "utf8");
37483
+ }
37484
+ async function clearMovedRuntimeMetadata(instanceDir) {
37485
+ const runtimePath = (0, import_path13.join)(instanceDir, "runtime.json");
37486
+ if (!(0, import_fs8.existsSync)(runtimePath)) return;
37487
+ await (0, import_promises7.rm)(runtimePath, { force: true });
37488
+ }
37142
37489
  function escapePgIdentifier(value) {
37143
37490
  return `"${value.replace(/"/g, '""')}"`;
37144
37491
  }
37145
- function parseDatabaseTarget(dbUrl) {
37492
+ function parseDatabaseTarget2(dbUrl) {
37146
37493
  const parsed = new URL(dbUrl);
37147
37494
  const databaseName = decodeURIComponent(parsed.pathname.replace(/^\//, "").trim());
37148
37495
  if (!databaseName) {
@@ -37158,7 +37505,7 @@ function parseDatabaseTarget(dbUrl) {
37158
37505
  };
37159
37506
  }
37160
37507
  async function dropDatabase(dbUrl) {
37161
- const { adminUrl, databaseName } = parseDatabaseTarget(dbUrl);
37508
+ const { adminUrl, databaseName } = parseDatabaseTarget2(dbUrl);
37162
37509
  const pool2 = new Pool8({ connectionString: adminUrl });
37163
37510
  try {
37164
37511
  await pool2.query(
@@ -37447,6 +37794,83 @@ instanceLifecycleRouter.delete("/instances/:name", async (req, res) => {
37447
37794
  droppedDatabase: droppedDatabaseName
37448
37795
  });
37449
37796
  });
37797
+ instanceLifecycleRouter.post("/instances/:name/migrate-root", async (req, res) => {
37798
+ const { name } = req.params;
37799
+ if (!isValidInstanceName2(name)) {
37800
+ res.status(400).json({
37801
+ error: "Invalid instance name. Use only alphanumeric characters, hyphens, and underscores (1-64 chars).",
37802
+ code: "INVALID_PARAM"
37803
+ });
37804
+ return;
37805
+ }
37806
+ const resolvedAuthority = await resolveInstanceAuthority(name);
37807
+ if (!resolvedAuthority) {
37808
+ res.status(404).json({
37809
+ error: `Instance "${name}" not found.`,
37810
+ code: "NOT_FOUND"
37811
+ });
37812
+ return;
37813
+ }
37814
+ const currentRoot = resolvedAuthority.runtimeRoot;
37815
+ const targetRoot = preferredRuntimeRoot();
37816
+ if ((0, import_path13.resolve)(currentRoot) === (0, import_path13.resolve)(targetRoot)) {
37817
+ res.status(200).json({
37818
+ ok: true,
37819
+ name,
37820
+ migrated: false,
37821
+ runtimeRoot: currentRoot,
37822
+ runtimeRootKind: classifyRuntimeRoot(currentRoot),
37823
+ instanceId: resolvedAuthority.instanceId,
37824
+ note: "Instance is already stored in the preferred runtime root.",
37825
+ updatedBindings: 0
37826
+ });
37827
+ return;
37828
+ }
37829
+ if (await isInstanceRunning(currentRoot, name)) {
37830
+ res.status(409).json({
37831
+ error: `Instance "${name}" is still running. Stop or recover it before migrating the runtime root.`,
37832
+ code: "INSTANCE_RUNNING"
37833
+ });
37834
+ return;
37835
+ }
37836
+ const targetInstanceDir = (0, import_path13.join)(targetRoot, "instances", name);
37837
+ if ((0, import_fs8.existsSync)(targetInstanceDir)) {
37838
+ res.status(409).json({
37839
+ error: `Target runtime root already contains an instance named "${name}".`,
37840
+ code: "TARGET_EXISTS"
37841
+ });
37842
+ return;
37843
+ }
37844
+ const oldEnvPath = resolvedAuthority.instanceEnvPath;
37845
+ const newEnvPath = (0, import_path13.join)(targetInstanceDir, ".env");
37846
+ try {
37847
+ await (0, import_promises7.mkdir)((0, import_path13.join)(targetRoot, "instances"), { recursive: true });
37848
+ await moveDirectory(resolvedAuthority.instanceDir, targetInstanceDir);
37849
+ await rewriteMovedInstanceEnv(targetInstanceDir, resolvedAuthority.instanceDir);
37850
+ await rewriteMovedInstanceMeta(targetInstanceDir, name);
37851
+ await clearMovedRuntimeMetadata(targetInstanceDir);
37852
+ let updatedBindings = 0;
37853
+ for (const project of resolvedAuthority.boundProjects) {
37854
+ await rewriteProjectBindingInstanceEnv(project.projectPath, oldEnvPath, newEnvPath);
37855
+ updatedBindings += 1;
37856
+ }
37857
+ res.status(200).json({
37858
+ ok: true,
37859
+ name,
37860
+ migrated: true,
37861
+ runtimeRoot: targetRoot,
37862
+ runtimeRootKind: classifyRuntimeRoot(targetRoot),
37863
+ instanceId: deriveInstanceId(targetInstanceDir),
37864
+ note: "Instance moved to the preferred runtime root and bound project env files were updated.",
37865
+ updatedBindings
37866
+ });
37867
+ } catch (error) {
37868
+ res.status(500).json({
37869
+ error: commandFailureMessage(error),
37870
+ code: "MIGRATE_FAILED"
37871
+ });
37872
+ }
37873
+ });
37450
37874
 
37451
37875
  // src/server/routes/control-plane/project-bindings.ts
37452
37876
  var import_express25 = __toESM(require_express2(), 1);