relmio 0.8.1 → 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 +28 -0
- package/README.md +47 -1
- package/docs/ai-assistant.md +230 -0
- package/docs/images/brand/relmio-logo-rounded.svg +25 -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 +1 -1
- package/src/ui/local.html +1 -1
- package/src/ui/relmio-icon-rounded.svg +25 -0
- package/src/web/server.js +255 -24
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"),
|
|
@@ -364,6 +465,7 @@ async function loadDefaultUiFiles() {
|
|
|
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"),
|
|
366
467
|
readFile(new URL("../ui/relmio-icon.png", import.meta.url)),
|
|
468
|
+
readFile(new URL("../ui/relmio-icon-rounded.svg", import.meta.url), "utf8"),
|
|
367
469
|
]);
|
|
368
470
|
|
|
369
471
|
return {
|
|
@@ -372,14 +474,18 @@ async function loadDefaultUiFiles() {
|
|
|
372
474
|
"/app.js": files[2],
|
|
373
475
|
"/local.js": files[3],
|
|
374
476
|
"/oauth-popup.js": files[4],
|
|
375
|
-
"/
|
|
376
|
-
"/
|
|
377
|
-
"/
|
|
378
|
-
"/
|
|
379
|
-
"/
|
|
380
|
-
"/
|
|
381
|
-
"/
|
|
382
|
-
"/
|
|
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],
|
|
383
489
|
};
|
|
384
490
|
}
|
|
385
491
|
|
|
@@ -1087,6 +1193,7 @@ async function handleApi(request, response, path, state) {
|
|
|
1087
1193
|
}
|
|
1088
1194
|
|
|
1089
1195
|
if (path === "/api/ssh/fingerprint") {
|
|
1196
|
+
rejectActiveVpsMutation(state);
|
|
1090
1197
|
enforceRateLimit(state, path);
|
|
1091
1198
|
const host = validateHostname(body.host);
|
|
1092
1199
|
const port = validatePort(body.port);
|
|
@@ -1094,12 +1201,14 @@ async function handleApi(request, response, path, state) {
|
|
|
1094
1201
|
host,
|
|
1095
1202
|
port,
|
|
1096
1203
|
});
|
|
1204
|
+
rejectActiveVpsMutation(state);
|
|
1097
1205
|
state.scannedHost = { host, port, fingerprint };
|
|
1098
1206
|
sendJson(response, 200, { fingerprint });
|
|
1099
1207
|
return;
|
|
1100
1208
|
}
|
|
1101
1209
|
|
|
1102
1210
|
if (path === "/api/ssh/connect") {
|
|
1211
|
+
rejectActiveVpsMutation(state);
|
|
1103
1212
|
enforceRateLimit(state, path);
|
|
1104
1213
|
const host = validateHostname(body.host);
|
|
1105
1214
|
const port = validatePort(body.port);
|
|
@@ -1118,8 +1227,9 @@ async function handleApi(request, response, path, state) {
|
|
|
1118
1227
|
state.connection?.close();
|
|
1119
1228
|
state.connection = null;
|
|
1120
1229
|
|
|
1230
|
+
let candidateConnection = null;
|
|
1121
1231
|
try {
|
|
1122
|
-
|
|
1232
|
+
candidateConnection = await state.services.connectVerified({
|
|
1123
1233
|
host,
|
|
1124
1234
|
port,
|
|
1125
1235
|
username: body.username,
|
|
@@ -1127,43 +1237,67 @@ async function handleApi(request, response, path, state) {
|
|
|
1127
1237
|
agent: body.useAgent ? process.env.SSH_AUTH_SOCK : undefined,
|
|
1128
1238
|
expectedFingerprint: scannedHost.fingerprint,
|
|
1129
1239
|
});
|
|
1240
|
+
rejectActiveVpsMutation(state);
|
|
1241
|
+
state.connection = candidateConnection;
|
|
1242
|
+
candidateConnection = null;
|
|
1130
1243
|
} finally {
|
|
1131
1244
|
body.password = undefined;
|
|
1245
|
+
try {
|
|
1246
|
+
candidateConnection?.close();
|
|
1247
|
+
} catch {
|
|
1248
|
+
// A stale connection must never become the shared VPS connection.
|
|
1249
|
+
}
|
|
1132
1250
|
}
|
|
1133
1251
|
|
|
1134
1252
|
state.scannedHost = null;
|
|
1135
1253
|
state.discovery = null;
|
|
1136
1254
|
state.networksByContainer.clear();
|
|
1255
|
+
state.sidecarPlan = null;
|
|
1256
|
+
state.assistantPlan = null;
|
|
1137
1257
|
sendJson(response, 200, { connected: true });
|
|
1138
1258
|
return;
|
|
1139
1259
|
}
|
|
1140
1260
|
|
|
1141
1261
|
if (path === "/api/discover") {
|
|
1262
|
+
rejectActiveVpsMutation(state);
|
|
1142
1263
|
const connection = requireConnection(state);
|
|
1143
|
-
|
|
1264
|
+
const discovery = await state.services.discoverN8n(connection);
|
|
1265
|
+
rejectActiveVpsMutation(state);
|
|
1266
|
+
state.discovery = discovery;
|
|
1144
1267
|
state.networksByContainer.clear();
|
|
1268
|
+
state.sidecarPlan = null;
|
|
1269
|
+
state.assistantPlan = null;
|
|
1145
1270
|
sendJson(response, 200, state.discovery);
|
|
1146
1271
|
return;
|
|
1147
1272
|
}
|
|
1148
1273
|
|
|
1149
1274
|
if (path === "/api/networks") {
|
|
1275
|
+
rejectActiveVpsMutation(state);
|
|
1150
1276
|
const connection = requireConnection(state);
|
|
1151
1277
|
requireDiscoveredContainer(state, body.containerName);
|
|
1152
1278
|
const networks = await state.services.discoverNetworks(
|
|
1153
1279
|
connection,
|
|
1154
1280
|
body.containerName,
|
|
1155
1281
|
);
|
|
1282
|
+
rejectActiveVpsMutation(state);
|
|
1156
1283
|
state.networksByContainer.set(body.containerName, networks);
|
|
1284
|
+
state.sidecarPlan = null;
|
|
1285
|
+
state.assistantPlan = null;
|
|
1157
1286
|
sendJson(response, 200, networks);
|
|
1158
1287
|
return;
|
|
1159
1288
|
}
|
|
1160
1289
|
|
|
1161
1290
|
if (path === "/api/plan") {
|
|
1291
|
+
rejectActiveVpsMutation(state);
|
|
1162
1292
|
requireDiscoveredNetwork(
|
|
1163
1293
|
state,
|
|
1164
1294
|
body.containerName,
|
|
1165
1295
|
body.networkName,
|
|
1166
1296
|
);
|
|
1297
|
+
state.sidecarPlan = {
|
|
1298
|
+
containerName: body.containerName,
|
|
1299
|
+
networkName: body.networkName,
|
|
1300
|
+
};
|
|
1167
1301
|
sendJson(response, 200, {
|
|
1168
1302
|
installDirectory: "/docker/n8n-openai-oauth",
|
|
1169
1303
|
sidecarProject: "n8n-openai-oauth",
|
|
@@ -1176,16 +1310,93 @@ async function handleApi(request, response, path, state) {
|
|
|
1176
1310
|
return;
|
|
1177
1311
|
}
|
|
1178
1312
|
|
|
1179
|
-
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);
|
|
1180
1357
|
const connection = requireConnection(state);
|
|
1181
|
-
|
|
1358
|
+
state.assistantPlan = null;
|
|
1359
|
+
const releaseVpsMutationLock = acquireVpsMutationLock(state);
|
|
1182
1360
|
try {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
state,
|
|
1361
|
+
const freshNetworks = await state.services.discoverNetworks(
|
|
1362
|
+
connection,
|
|
1186
1363
|
body.containerName,
|
|
1187
|
-
body.networkName,
|
|
1188
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 {
|
|
1189
1400
|
const authStatus = await state.services.getAuthStatus();
|
|
1190
1401
|
if (!authStatus.exists) {
|
|
1191
1402
|
throw new Error("Sign in with ChatGPT before installing.");
|
|
@@ -1200,9 +1411,13 @@ async function handleApi(request, response, path, state) {
|
|
|
1200
1411
|
confirmed: body.confirmed,
|
|
1201
1412
|
});
|
|
1202
1413
|
} finally {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
state.connection
|
|
1414
|
+
try {
|
|
1415
|
+
connection.close();
|
|
1416
|
+
if (state.connection === connection) {
|
|
1417
|
+
state.connection = null;
|
|
1418
|
+
}
|
|
1419
|
+
} finally {
|
|
1420
|
+
releaseVpsMutationLock();
|
|
1206
1421
|
}
|
|
1207
1422
|
}
|
|
1208
1423
|
|
|
@@ -1211,8 +1426,11 @@ async function handleApi(request, response, path, state) {
|
|
|
1211
1426
|
}
|
|
1212
1427
|
|
|
1213
1428
|
if (path === "/api/disconnect") {
|
|
1429
|
+
rejectActiveVpsMutation(state);
|
|
1214
1430
|
state.connection?.close();
|
|
1215
1431
|
state.connection = null;
|
|
1432
|
+
state.sidecarPlan = null;
|
|
1433
|
+
state.assistantPlan = null;
|
|
1216
1434
|
sendJson(response, 200, { disconnected: true });
|
|
1217
1435
|
return;
|
|
1218
1436
|
}
|
|
@@ -1225,6 +1443,10 @@ function createRequestHandler(state) {
|
|
|
1225
1443
|
setSecurityHeaders(response);
|
|
1226
1444
|
|
|
1227
1445
|
try {
|
|
1446
|
+
if (state.closing) {
|
|
1447
|
+
sendJson(response, 503, { error: "The local wizard is closing." });
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1228
1450
|
const url = new URL(request.url, state.origin);
|
|
1229
1451
|
const path = url.pathname;
|
|
1230
1452
|
|
|
@@ -1292,6 +1514,11 @@ export async function startWizardServer({
|
|
|
1292
1514
|
scannedHost: null,
|
|
1293
1515
|
discovery: null,
|
|
1294
1516
|
networksByContainer: new Map(),
|
|
1517
|
+
sidecarPlan: null,
|
|
1518
|
+
assistantPlan: null,
|
|
1519
|
+
vpsMutationInFlight: false,
|
|
1520
|
+
vpsMutationLock: null,
|
|
1521
|
+
vpsMutationCompletion: null,
|
|
1295
1522
|
oauthLogin: null,
|
|
1296
1523
|
oauthRetryBlocked: false,
|
|
1297
1524
|
oauthStartupError: null,
|
|
@@ -1327,6 +1554,7 @@ export async function startWizardServer({
|
|
|
1327
1554
|
origin: state.origin,
|
|
1328
1555
|
async close() {
|
|
1329
1556
|
state.closing = true;
|
|
1557
|
+
const serverClose = new Promise((resolve) => server.close(resolve));
|
|
1330
1558
|
state.localChatTest.dispose?.();
|
|
1331
1559
|
await waitForBoundedResult(
|
|
1332
1560
|
state.oauthLoginStartPromise,
|
|
@@ -1350,12 +1578,15 @@ export async function startWizardServer({
|
|
|
1350
1578
|
codexLogin?.completion,
|
|
1351
1579
|
state.oauthShutdownWaitMs,
|
|
1352
1580
|
);
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
await waitForBoundedResult(
|
|
1356
|
-
new Promise((resolve) => server.close(resolve)),
|
|
1581
|
+
const vpsMutationFinished = await waitForBoundedResult(
|
|
1582
|
+
state.vpsMutationCompletion,
|
|
1357
1583
|
state.oauthShutdownWaitMs,
|
|
1358
1584
|
);
|
|
1585
|
+
if (vpsMutationFinished) {
|
|
1586
|
+
state.connection?.close();
|
|
1587
|
+
state.connection = null;
|
|
1588
|
+
}
|
|
1589
|
+
await waitForBoundedResult(serverClose, state.oauthShutdownWaitMs);
|
|
1359
1590
|
},
|
|
1360
1591
|
};
|
|
1361
1592
|
}
|