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.
- package/dist/server/bundle.cjs +531 -107
- package/package.json +1 -1
- package/public/control-plane/assets/index-OSHzCl4w.css +1 -0
- package/public/control-plane/assets/index-XZAadN0I.js +77 -0
- package/public/control-plane/index.html +2 -2
- package/public/control-plane/assets/index-FDSDCWEl.css +0 -1
- package/public/control-plane/assets/index-oM_6VuRn.js +0 -77
package/dist/server/bundle.cjs
CHANGED
|
@@ -1301,8 +1301,8 @@ var require_node = __commonJS({
|
|
|
1301
1301
|
break;
|
|
1302
1302
|
case "PIPE":
|
|
1303
1303
|
case "TCP":
|
|
1304
|
-
var
|
|
1305
|
-
stream2 = new
|
|
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
|
|
25937
|
-
return new
|
|
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
|
|
26060
|
-
if (
|
|
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
|
|
27678
|
-
if (config.nativeConnectionString)
|
|
27679
|
-
this.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:
|
|
27684
|
+
value: cp2.password
|
|
27685
27685
|
});
|
|
27686
|
-
this.database =
|
|
27687
|
-
this.host =
|
|
27688
|
-
this.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:
|
|
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:
|
|
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 = [
|
|
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(
|
|
32264
|
-
if (!
|
|
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:
|
|
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
|
|
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
|
|
35178
|
+
function extractExactQueryValue(body) {
|
|
34946
35179
|
if (body === null || typeof body !== "object") return null;
|
|
34947
35180
|
const obj = body;
|
|
34948
|
-
|
|
34949
|
-
|
|
34950
|
-
|
|
34951
|
-
|
|
34952
|
-
|
|
34953
|
-
|
|
34954
|
-
|
|
34955
|
-
|
|
34956
|
-
|
|
34957
|
-
|
|
34958
|
-
}
|
|
34959
|
-
|
|
34960
|
-
|
|
34961
|
-
|
|
34962
|
-
|
|
34963
|
-
|
|
34964
|
-
|
|
34965
|
-
|
|
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
|
|
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(
|
|
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/
|
|
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
|
-
|
|
35010
|
-
const value = extractProbeValueFromSearch(body);
|
|
35232
|
+
const value = extractExactQueryValue(body);
|
|
35011
35233
|
return {
|
|
35012
35234
|
check: "ingest_roundtrip",
|
|
35013
|
-
status: value
|
|
35014
|
-
message: value
|
|
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
|
|
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
|
-
|
|
35095
|
-
|
|
35096
|
-
|
|
35097
|
-
|
|
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 ? "
|
|
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
|
|
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
|
-
|
|
36562
|
-
|
|
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
|
|
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 } =
|
|
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);
|