relmio 0.8.0 → 0.9.0
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/CHANGELOG.md +39 -0
- package/README.md +50 -0
- package/docs/ai-assistant.md +230 -0
- package/docs/brand.md +14 -13
- package/docs/images/brand/relmio-logo-rounded.svg +25 -0
- package/docs/images/brand/relmio-logo.png +0 -0
- package/docs/maintenance.md +61 -0
- package/docs/security.md +43 -0
- package/docs/vps-and-n8n.md +3 -0
- package/package.json +1 -1
- package/src/browser.js +3 -2
- package/src/cli.js +21 -8
- package/src/domain/assistant-templates.js +207 -0
- package/src/domain/assistant.js +363 -0
- package/src/domain/safety.js +4 -1
- package/src/services/assistant-installer.js +514 -0
- package/src/services/discovery.js +28 -1
- package/src/services/installer.js +13 -5
- package/src/ui/assistant.css +154 -0
- package/src/ui/assistant.html +351 -0
- package/src/ui/assistant.js +290 -0
- package/src/ui/index.html +7 -12
- package/src/ui/local.html +7 -12
- package/src/ui/relmio-icon-rounded.svg +25 -0
- package/src/ui/relmio-icon.png +0 -0
- package/src/ui/styles.css +0 -4
- package/src/web/server.js +261 -26
- package/docs/images/brand/relmio-mark.svg +0 -10
package/src/web/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import packageManifest from "../../package.json" with { type: "json" };
|
|
|
5
5
|
|
|
6
6
|
import { discoverN8n, discoverNetworks } from "../services/discovery.js";
|
|
7
7
|
import { installSidecar } from "../services/installer.js";
|
|
8
|
+
import { installAssistant } from "../services/assistant-installer.js";
|
|
8
9
|
import {
|
|
9
10
|
getAuthStatus,
|
|
10
11
|
readAuthContents,
|
|
@@ -19,6 +20,9 @@ import {
|
|
|
19
20
|
validatePort,
|
|
20
21
|
} from "../domain/validation.js";
|
|
21
22
|
import { SIDECAR_HOSTNAME } from "../domain/templates.js";
|
|
23
|
+
import {
|
|
24
|
+
ASSISTANT_ROOT,
|
|
25
|
+
} from "../domain/assistant.js";
|
|
22
26
|
import {
|
|
23
27
|
createLocalDeploymentPlan,
|
|
24
28
|
validateLocalTarget,
|
|
@@ -78,6 +82,7 @@ const defaultServices = {
|
|
|
78
82
|
discoverN8n,
|
|
79
83
|
discoverNetworks,
|
|
80
84
|
installSidecar,
|
|
85
|
+
installAssistant,
|
|
81
86
|
attestLocalCodexInstallation,
|
|
82
87
|
getLocalDockerStatus,
|
|
83
88
|
getProjectMeta,
|
|
@@ -289,6 +294,95 @@ function requireConnection(state) {
|
|
|
289
294
|
return state.connection;
|
|
290
295
|
}
|
|
291
296
|
|
|
297
|
+
function rejectActiveVpsMutation(state) {
|
|
298
|
+
if (state.closing) {
|
|
299
|
+
throw Object.assign(new Error("The local wizard is closing."), {
|
|
300
|
+
statusCode: 409,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (state.vpsMutationInFlight) {
|
|
304
|
+
throw Object.assign(
|
|
305
|
+
new Error("A VPS installation is already in progress. Wait for it to finish before trying another installation."),
|
|
306
|
+
{ statusCode: 409 },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function acquireVpsMutationLock(state) {
|
|
312
|
+
rejectActiveVpsMutation(state);
|
|
313
|
+
const lock = Symbol("vps-mutation");
|
|
314
|
+
let resolveCompletion;
|
|
315
|
+
state.vpsMutationInFlight = true;
|
|
316
|
+
state.vpsMutationLock = lock;
|
|
317
|
+
state.vpsMutationCompletion = new Promise((resolve) => {
|
|
318
|
+
resolveCompletion = resolve;
|
|
319
|
+
});
|
|
320
|
+
return () => {
|
|
321
|
+
if (state.vpsMutationLock === lock) {
|
|
322
|
+
state.vpsMutationInFlight = false;
|
|
323
|
+
state.vpsMutationLock = null;
|
|
324
|
+
state.vpsMutationCompletion = null;
|
|
325
|
+
resolveCompletion();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function requireReviewedVpsPlan(plan, body, label) {
|
|
331
|
+
if (
|
|
332
|
+
!plan ||
|
|
333
|
+
plan.containerName !== body.containerName ||
|
|
334
|
+
plan.networkName !== body.networkName
|
|
335
|
+
) {
|
|
336
|
+
throw new Error(`Review a fresh ${label} plan before installing.`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function requireAssistantSearxngSelection(value) {
|
|
341
|
+
if (typeof value !== "boolean") {
|
|
342
|
+
throw new Error("Choose whether to include optional SearXNG web search.");
|
|
343
|
+
}
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function requireEnabledInstanceAi(state, containerName) {
|
|
348
|
+
const instanceAi = state.networksByContainer.get(containerName)?.instanceAi;
|
|
349
|
+
if (instanceAi?.status === "enabled") return instanceAi;
|
|
350
|
+
if (instanceAi?.status === "missing" || instanceAi?.status === "configured") {
|
|
351
|
+
throw new Error(
|
|
352
|
+
"The selected n8n container needs N8N_ENABLED_MODULES to include instance-ai. Update its existing deployment separately; restart n8n outside this wizard only if you later authorize that action.",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
throw new Error("The selected n8n container's AI Assistant prerequisite could not be verified.");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function requireReviewedAssistantPlan(state, plan, body) {
|
|
359
|
+
requireReviewedVpsPlan(plan, body, "AI Assistant");
|
|
360
|
+
if (plan.includeSearxng !== body.includeSearxng) {
|
|
361
|
+
throw new Error("Review a fresh AI Assistant plan for the selected web-search option.");
|
|
362
|
+
}
|
|
363
|
+
const instanceAi = requireEnabledInstanceAi(state, body.containerName);
|
|
364
|
+
if (plan.instanceAi?.status !== instanceAi.status) {
|
|
365
|
+
throw new Error("Review a fresh AI Assistant plan after prerequisite discovery.");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function requireFreshAssistantInstallState(plan, networks, body) {
|
|
370
|
+
if (!Array.isArray(networks?.networks) || !networks.networks.includes(body.networkName)) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
"The selected Docker network changed after plan review. Review a fresh AI Assistant plan before installing.",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const instanceAi = networks.instanceAi;
|
|
376
|
+
if (instanceAi?.status !== "enabled") {
|
|
377
|
+
throw new Error(
|
|
378
|
+
"The selected n8n container needs N8N_ENABLED_MODULES to include instance-ai. Update its existing deployment separately; restart n8n outside this wizard only if you later authorize that action.",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (plan.instanceAi?.status !== instanceAi.status) {
|
|
382
|
+
throw new Error("Review a fresh AI Assistant plan after prerequisite discovery.");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
292
386
|
function requireDiscoveredContainer(state, containerName) {
|
|
293
387
|
const container = state.discovery?.containers.find(
|
|
294
388
|
(candidate) => candidate.name === containerName,
|
|
@@ -310,7 +404,11 @@ function requireDiscoveredNetwork(state, containerName, networkName) {
|
|
|
310
404
|
|
|
311
405
|
function safeErrorMessage(error) {
|
|
312
406
|
const message =
|
|
313
|
-
typeof error?.
|
|
407
|
+
typeof error?.safeMessage === "string"
|
|
408
|
+
? error.safeMessage
|
|
409
|
+
: typeof error?.message === "string"
|
|
410
|
+
? error.message
|
|
411
|
+
: "Request failed.";
|
|
314
412
|
if (
|
|
315
413
|
message.length > 240 ||
|
|
316
414
|
/[\r\n]/u.test(message) ||
|
|
@@ -356,6 +454,9 @@ async function loadDefaultUiFiles() {
|
|
|
356
454
|
readFile(new URL("../ui/app.js", import.meta.url), "utf8"),
|
|
357
455
|
readFile(new URL("../ui/local.js", import.meta.url), "utf8"),
|
|
358
456
|
readFile(new URL("../ui/oauth-popup.js", import.meta.url), "utf8"),
|
|
457
|
+
readFile(new URL("../ui/assistant.html", import.meta.url), "utf8"),
|
|
458
|
+
readFile(new URL("../ui/assistant.js", import.meta.url), "utf8"),
|
|
459
|
+
readFile(new URL("../ui/assistant.css", import.meta.url), "utf8"),
|
|
359
460
|
readFile(new URL("../ui/theme.js", import.meta.url), "utf8"),
|
|
360
461
|
readFile(new URL("../ui/time.js", import.meta.url), "utf8"),
|
|
361
462
|
readFile(new URL("../ui/styles.css", import.meta.url), "utf8"),
|
|
@@ -363,6 +464,8 @@ async function loadDefaultUiFiles() {
|
|
|
363
464
|
readFile(new URL("../ui/icons/monitor.svg", import.meta.url), "utf8"),
|
|
364
465
|
readFile(new URL("../ui/icons/sun.svg", import.meta.url), "utf8"),
|
|
365
466
|
readFile(new URL("../ui/icons/moon.svg", import.meta.url), "utf8"),
|
|
467
|
+
readFile(new URL("../ui/relmio-icon.png", import.meta.url)),
|
|
468
|
+
readFile(new URL("../ui/relmio-icon-rounded.svg", import.meta.url), "utf8"),
|
|
366
469
|
]);
|
|
367
470
|
|
|
368
471
|
return {
|
|
@@ -371,13 +474,18 @@ async function loadDefaultUiFiles() {
|
|
|
371
474
|
"/app.js": files[2],
|
|
372
475
|
"/local.js": files[3],
|
|
373
476
|
"/oauth-popup.js": files[4],
|
|
374
|
-
"/
|
|
375
|
-
"/
|
|
376
|
-
"/
|
|
377
|
-
"/
|
|
378
|
-
"/
|
|
379
|
-
"/
|
|
380
|
-
"/
|
|
477
|
+
"/assistant": files[5],
|
|
478
|
+
"/assistant.js": files[6],
|
|
479
|
+
"/assistant.css": files[7],
|
|
480
|
+
"/theme.js": files[8],
|
|
481
|
+
"/time.js": files[9],
|
|
482
|
+
"/styles.css": files[10],
|
|
483
|
+
"/local.css": files[11],
|
|
484
|
+
"/icons/monitor.svg": files[12],
|
|
485
|
+
"/icons/sun.svg": files[13],
|
|
486
|
+
"/icons/moon.svg": files[14],
|
|
487
|
+
"/relmio-icon.png": files[15],
|
|
488
|
+
"/relmio-icon-rounded.svg": files[16],
|
|
381
489
|
};
|
|
382
490
|
}
|
|
383
491
|
|
|
@@ -1085,6 +1193,7 @@ async function handleApi(request, response, path, state) {
|
|
|
1085
1193
|
}
|
|
1086
1194
|
|
|
1087
1195
|
if (path === "/api/ssh/fingerprint") {
|
|
1196
|
+
rejectActiveVpsMutation(state);
|
|
1088
1197
|
enforceRateLimit(state, path);
|
|
1089
1198
|
const host = validateHostname(body.host);
|
|
1090
1199
|
const port = validatePort(body.port);
|
|
@@ -1092,12 +1201,14 @@ async function handleApi(request, response, path, state) {
|
|
|
1092
1201
|
host,
|
|
1093
1202
|
port,
|
|
1094
1203
|
});
|
|
1204
|
+
rejectActiveVpsMutation(state);
|
|
1095
1205
|
state.scannedHost = { host, port, fingerprint };
|
|
1096
1206
|
sendJson(response, 200, { fingerprint });
|
|
1097
1207
|
return;
|
|
1098
1208
|
}
|
|
1099
1209
|
|
|
1100
1210
|
if (path === "/api/ssh/connect") {
|
|
1211
|
+
rejectActiveVpsMutation(state);
|
|
1101
1212
|
enforceRateLimit(state, path);
|
|
1102
1213
|
const host = validateHostname(body.host);
|
|
1103
1214
|
const port = validatePort(body.port);
|
|
@@ -1116,8 +1227,9 @@ async function handleApi(request, response, path, state) {
|
|
|
1116
1227
|
state.connection?.close();
|
|
1117
1228
|
state.connection = null;
|
|
1118
1229
|
|
|
1230
|
+
let candidateConnection = null;
|
|
1119
1231
|
try {
|
|
1120
|
-
|
|
1232
|
+
candidateConnection = await state.services.connectVerified({
|
|
1121
1233
|
host,
|
|
1122
1234
|
port,
|
|
1123
1235
|
username: body.username,
|
|
@@ -1125,43 +1237,67 @@ async function handleApi(request, response, path, state) {
|
|
|
1125
1237
|
agent: body.useAgent ? process.env.SSH_AUTH_SOCK : undefined,
|
|
1126
1238
|
expectedFingerprint: scannedHost.fingerprint,
|
|
1127
1239
|
});
|
|
1240
|
+
rejectActiveVpsMutation(state);
|
|
1241
|
+
state.connection = candidateConnection;
|
|
1242
|
+
candidateConnection = null;
|
|
1128
1243
|
} finally {
|
|
1129
1244
|
body.password = undefined;
|
|
1245
|
+
try {
|
|
1246
|
+
candidateConnection?.close();
|
|
1247
|
+
} catch {
|
|
1248
|
+
// A stale connection must never become the shared VPS connection.
|
|
1249
|
+
}
|
|
1130
1250
|
}
|
|
1131
1251
|
|
|
1132
1252
|
state.scannedHost = null;
|
|
1133
1253
|
state.discovery = null;
|
|
1134
1254
|
state.networksByContainer.clear();
|
|
1255
|
+
state.sidecarPlan = null;
|
|
1256
|
+
state.assistantPlan = null;
|
|
1135
1257
|
sendJson(response, 200, { connected: true });
|
|
1136
1258
|
return;
|
|
1137
1259
|
}
|
|
1138
1260
|
|
|
1139
1261
|
if (path === "/api/discover") {
|
|
1262
|
+
rejectActiveVpsMutation(state);
|
|
1140
1263
|
const connection = requireConnection(state);
|
|
1141
|
-
|
|
1264
|
+
const discovery = await state.services.discoverN8n(connection);
|
|
1265
|
+
rejectActiveVpsMutation(state);
|
|
1266
|
+
state.discovery = discovery;
|
|
1142
1267
|
state.networksByContainer.clear();
|
|
1268
|
+
state.sidecarPlan = null;
|
|
1269
|
+
state.assistantPlan = null;
|
|
1143
1270
|
sendJson(response, 200, state.discovery);
|
|
1144
1271
|
return;
|
|
1145
1272
|
}
|
|
1146
1273
|
|
|
1147
1274
|
if (path === "/api/networks") {
|
|
1275
|
+
rejectActiveVpsMutation(state);
|
|
1148
1276
|
const connection = requireConnection(state);
|
|
1149
1277
|
requireDiscoveredContainer(state, body.containerName);
|
|
1150
1278
|
const networks = await state.services.discoverNetworks(
|
|
1151
1279
|
connection,
|
|
1152
1280
|
body.containerName,
|
|
1153
1281
|
);
|
|
1282
|
+
rejectActiveVpsMutation(state);
|
|
1154
1283
|
state.networksByContainer.set(body.containerName, networks);
|
|
1284
|
+
state.sidecarPlan = null;
|
|
1285
|
+
state.assistantPlan = null;
|
|
1155
1286
|
sendJson(response, 200, networks);
|
|
1156
1287
|
return;
|
|
1157
1288
|
}
|
|
1158
1289
|
|
|
1159
1290
|
if (path === "/api/plan") {
|
|
1291
|
+
rejectActiveVpsMutation(state);
|
|
1160
1292
|
requireDiscoveredNetwork(
|
|
1161
1293
|
state,
|
|
1162
1294
|
body.containerName,
|
|
1163
1295
|
body.networkName,
|
|
1164
1296
|
);
|
|
1297
|
+
state.sidecarPlan = {
|
|
1298
|
+
containerName: body.containerName,
|
|
1299
|
+
networkName: body.networkName,
|
|
1300
|
+
};
|
|
1165
1301
|
sendJson(response, 200, {
|
|
1166
1302
|
installDirectory: "/docker/n8n-openai-oauth",
|
|
1167
1303
|
sidecarProject: "n8n-openai-oauth",
|
|
@@ -1174,16 +1310,93 @@ async function handleApi(request, response, path, state) {
|
|
|
1174
1310
|
return;
|
|
1175
1311
|
}
|
|
1176
1312
|
|
|
1177
|
-
if (path === "/api/
|
|
1313
|
+
if (path === "/api/assistant/plan") {
|
|
1314
|
+
rejectActiveVpsMutation(state);
|
|
1315
|
+
const includeSearxng = requireAssistantSearxngSelection(body.includeSearxng);
|
|
1316
|
+
requireDiscoveredNetwork(
|
|
1317
|
+
state,
|
|
1318
|
+
body.containerName,
|
|
1319
|
+
body.networkName,
|
|
1320
|
+
);
|
|
1321
|
+
const instanceAi = requireEnabledInstanceAi(state, body.containerName);
|
|
1322
|
+
state.assistantPlan = {
|
|
1323
|
+
containerName: body.containerName,
|
|
1324
|
+
networkName: body.networkName,
|
|
1325
|
+
includeSearxng,
|
|
1326
|
+
instanceAi,
|
|
1327
|
+
};
|
|
1328
|
+
sendJson(response, 200, {
|
|
1329
|
+
installDirectory: ASSISTANT_ROOT,
|
|
1330
|
+
companionProject: "generated ownership-bound project after confirmation",
|
|
1331
|
+
sandboxUrl: "generated after verified installation",
|
|
1332
|
+
includeSearxng,
|
|
1333
|
+
webSearch: includeSearxng ? "enabled" : "disabled",
|
|
1334
|
+
...(includeSearxng ? { searxngUrl: "generated after verified installation" } : {}),
|
|
1335
|
+
networkName: body.networkName,
|
|
1336
|
+
instanceAi,
|
|
1337
|
+
existingN8nChanges: [],
|
|
1338
|
+
existingN8nRestarts: 0,
|
|
1339
|
+
publishedPorts: [],
|
|
1340
|
+
privilegedRunner: true,
|
|
1341
|
+
preview: true,
|
|
1342
|
+
});
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (path === "/api/assistant/install") {
|
|
1347
|
+
rejectActiveVpsMutation(state);
|
|
1348
|
+
enforceRateLimit(state, path);
|
|
1349
|
+
const includeSearxng = requireAssistantSearxngSelection(body.includeSearxng);
|
|
1350
|
+
requireDiscoveredNetwork(
|
|
1351
|
+
state,
|
|
1352
|
+
body.containerName,
|
|
1353
|
+
body.networkName,
|
|
1354
|
+
);
|
|
1355
|
+
const reviewedPlan = state.assistantPlan;
|
|
1356
|
+
requireReviewedAssistantPlan(state, reviewedPlan, body);
|
|
1178
1357
|
const connection = requireConnection(state);
|
|
1179
|
-
|
|
1358
|
+
state.assistantPlan = null;
|
|
1359
|
+
const releaseVpsMutationLock = acquireVpsMutationLock(state);
|
|
1180
1360
|
try {
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
state,
|
|
1361
|
+
const freshNetworks = await state.services.discoverNetworks(
|
|
1362
|
+
connection,
|
|
1184
1363
|
body.containerName,
|
|
1185
|
-
body.networkName,
|
|
1186
1364
|
);
|
|
1365
|
+
requireFreshAssistantInstallState(reviewedPlan, freshNetworks, body);
|
|
1366
|
+
const result = await state.services.installAssistant({
|
|
1367
|
+
remote: connection,
|
|
1368
|
+
networkName: body.networkName,
|
|
1369
|
+
confirmed: body.confirmed,
|
|
1370
|
+
includeSearxng,
|
|
1371
|
+
});
|
|
1372
|
+
sendJson(response, 200, result);
|
|
1373
|
+
} finally {
|
|
1374
|
+
try {
|
|
1375
|
+
connection.close();
|
|
1376
|
+
if (state.connection === connection) {
|
|
1377
|
+
state.connection = null;
|
|
1378
|
+
}
|
|
1379
|
+
} finally {
|
|
1380
|
+
releaseVpsMutationLock();
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (path === "/api/install") {
|
|
1387
|
+
rejectActiveVpsMutation(state);
|
|
1388
|
+
enforceRateLimit(state, path);
|
|
1389
|
+
requireDiscoveredNetwork(
|
|
1390
|
+
state,
|
|
1391
|
+
body.containerName,
|
|
1392
|
+
body.networkName,
|
|
1393
|
+
);
|
|
1394
|
+
requireReviewedVpsPlan(state.sidecarPlan, body, "sidecar");
|
|
1395
|
+
const connection = requireConnection(state);
|
|
1396
|
+
state.sidecarPlan = null;
|
|
1397
|
+
const releaseVpsMutationLock = acquireVpsMutationLock(state);
|
|
1398
|
+
let result;
|
|
1399
|
+
try {
|
|
1187
1400
|
const authStatus = await state.services.getAuthStatus();
|
|
1188
1401
|
if (!authStatus.exists) {
|
|
1189
1402
|
throw new Error("Sign in with ChatGPT before installing.");
|
|
@@ -1198,9 +1411,13 @@ async function handleApi(request, response, path, state) {
|
|
|
1198
1411
|
confirmed: body.confirmed,
|
|
1199
1412
|
});
|
|
1200
1413
|
} finally {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
state.connection
|
|
1414
|
+
try {
|
|
1415
|
+
connection.close();
|
|
1416
|
+
if (state.connection === connection) {
|
|
1417
|
+
state.connection = null;
|
|
1418
|
+
}
|
|
1419
|
+
} finally {
|
|
1420
|
+
releaseVpsMutationLock();
|
|
1204
1421
|
}
|
|
1205
1422
|
}
|
|
1206
1423
|
|
|
@@ -1209,8 +1426,11 @@ async function handleApi(request, response, path, state) {
|
|
|
1209
1426
|
}
|
|
1210
1427
|
|
|
1211
1428
|
if (path === "/api/disconnect") {
|
|
1429
|
+
rejectActiveVpsMutation(state);
|
|
1212
1430
|
state.connection?.close();
|
|
1213
1431
|
state.connection = null;
|
|
1432
|
+
state.sidecarPlan = null;
|
|
1433
|
+
state.assistantPlan = null;
|
|
1214
1434
|
sendJson(response, 200, { disconnected: true });
|
|
1215
1435
|
return;
|
|
1216
1436
|
}
|
|
@@ -1223,6 +1443,10 @@ function createRequestHandler(state) {
|
|
|
1223
1443
|
setSecurityHeaders(response);
|
|
1224
1444
|
|
|
1225
1445
|
try {
|
|
1446
|
+
if (state.closing) {
|
|
1447
|
+
sendJson(response, 503, { error: "The local wizard is closing." });
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1226
1450
|
const url = new URL(request.url, state.origin);
|
|
1227
1451
|
const path = url.pathname;
|
|
1228
1452
|
|
|
@@ -1241,9 +1465,11 @@ function createRequestHandler(state) {
|
|
|
1241
1465
|
? "text/javascript; charset=utf-8"
|
|
1242
1466
|
: path.endsWith(".svg")
|
|
1243
1467
|
? "image/svg+xml; charset=utf-8"
|
|
1244
|
-
: path.endsWith(".
|
|
1245
|
-
? "
|
|
1246
|
-
: "
|
|
1468
|
+
: path.endsWith(".png")
|
|
1469
|
+
? "image/png"
|
|
1470
|
+
: path.endsWith(".css")
|
|
1471
|
+
? "text/css; charset=utf-8"
|
|
1472
|
+
: "text/html; charset=utf-8";
|
|
1247
1473
|
const contents = state.uiFiles[path];
|
|
1248
1474
|
response.writeHead(200, {
|
|
1249
1475
|
"Content-Type": contentType,
|
|
@@ -1288,6 +1514,11 @@ export async function startWizardServer({
|
|
|
1288
1514
|
scannedHost: null,
|
|
1289
1515
|
discovery: null,
|
|
1290
1516
|
networksByContainer: new Map(),
|
|
1517
|
+
sidecarPlan: null,
|
|
1518
|
+
assistantPlan: null,
|
|
1519
|
+
vpsMutationInFlight: false,
|
|
1520
|
+
vpsMutationLock: null,
|
|
1521
|
+
vpsMutationCompletion: null,
|
|
1291
1522
|
oauthLogin: null,
|
|
1292
1523
|
oauthRetryBlocked: false,
|
|
1293
1524
|
oauthStartupError: null,
|
|
@@ -1323,6 +1554,7 @@ export async function startWizardServer({
|
|
|
1323
1554
|
origin: state.origin,
|
|
1324
1555
|
async close() {
|
|
1325
1556
|
state.closing = true;
|
|
1557
|
+
const serverClose = new Promise((resolve) => server.close(resolve));
|
|
1326
1558
|
state.localChatTest.dispose?.();
|
|
1327
1559
|
await waitForBoundedResult(
|
|
1328
1560
|
state.oauthLoginStartPromise,
|
|
@@ -1346,12 +1578,15 @@ export async function startWizardServer({
|
|
|
1346
1578
|
codexLogin?.completion,
|
|
1347
1579
|
state.oauthShutdownWaitMs,
|
|
1348
1580
|
);
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
await waitForBoundedResult(
|
|
1352
|
-
new Promise((resolve) => server.close(resolve)),
|
|
1581
|
+
const vpsMutationFinished = await waitForBoundedResult(
|
|
1582
|
+
state.vpsMutationCompletion,
|
|
1353
1583
|
state.oauthShutdownWaitMs,
|
|
1354
1584
|
);
|
|
1585
|
+
if (vpsMutationFinished) {
|
|
1586
|
+
state.connection?.close();
|
|
1587
|
+
state.connection = null;
|
|
1588
|
+
}
|
|
1589
|
+
await waitForBoundedResult(serverClose, state.oauthShutdownWaitMs);
|
|
1355
1590
|
},
|
|
1356
1591
|
};
|
|
1357
1592
|
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
<svg
|
|
2
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
-
viewBox="0 0 64 64"
|
|
4
|
-
role="img"
|
|
5
|
-
aria-labelledby="relmio-mark-title"
|
|
6
|
-
>
|
|
7
|
-
<title id="relmio-mark-title">Relmio mark</title>
|
|
8
|
-
<path fill="#137c74" d="M8 13 28 5v38c0 4-2 7-6 9L8 59Z" />
|
|
9
|
-
<path fill="#12211f" d="M36 5l11 5c6 3 9 8 9 15v9c0 3 1 5 3 7l3 3c3 3 3 8 0 11s-8 3-11 0L40 44c-3-3-4-6-4-10Z" />
|
|
10
|
-
</svg>
|