labgate 0.5.33 → 0.5.34
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/README.md +1 -0
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/config.js +22 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/container.js +3 -2
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/explorer-claude.js +1 -0
- package/dist/lib/explorer-claude.js.map +1 -1
- package/dist/lib/init.js +6 -0
- package/dist/lib/init.js.map +1 -1
- package/dist/lib/ui.html +729 -224
- package/dist/lib/ui.js +372 -2
- package/dist/lib/ui.js.map +1 -1
- package/dist/mcp-bundles/dataset-mcp.bundle.mjs +14 -0
- package/dist/mcp-bundles/explorer-mcp.bundle.mjs +19 -0
- package/package.json +1 -1
package/dist/lib/ui.js
CHANGED
|
@@ -60,6 +60,7 @@ const explorer_store_js_1 = require("./explorer-store.js");
|
|
|
60
60
|
const log = __importStar(require("./log.js"));
|
|
61
61
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
62
62
|
const HTML_PATH = (0, path_1.resolve)(__dirname, '..', 'lib', 'ui.html');
|
|
63
|
+
const PACKAGE_JSON_PATH = (0, path_1.resolve)(__dirname, '..', '..', 'package.json');
|
|
63
64
|
const FONTS_DIR = (0, path_1.resolve)(__dirname, '..', '..', 'node_modules', 'geist', 'dist', 'fonts');
|
|
64
65
|
const XTERM_CSS_PATH = (0, path_1.resolve)(__dirname, '..', '..', 'node_modules', '@xterm', 'xterm', 'css', 'xterm.css');
|
|
65
66
|
const XTERM_JS_PATH = (0, path_1.resolve)(__dirname, '..', '..', 'node_modules', '@xterm', 'xterm', 'lib', 'xterm.js');
|
|
@@ -79,6 +80,12 @@ const PODMAN_SETUP_MAX_BUFFER = 16 * 1024 * 1024;
|
|
|
79
80
|
const EXPLORER_TSP_TEMPLATE_DIR = (0, path_1.resolve)(__dirname, '..', '..', 'templates', 'tsp-lab');
|
|
80
81
|
const EXPLORER_TSP_TEMPLATE_SOURCE_REPO = (0, path_1.join)((0, config_js_1.getExplorerRootDir)(), 'templates', 'tsp-lab-source');
|
|
81
82
|
const EXPLORER_ARTIFACT_READ_MAX_BYTES = 2 * 1024 * 1024;
|
|
83
|
+
const UI_VERSION_NPM_PACKAGE = 'labgate';
|
|
84
|
+
const UI_VERSION_CHECK_TIMEOUT_MS = 8_000;
|
|
85
|
+
const UI_VERSION_CHECK_MAX_BUFFER = 256 * 1024;
|
|
86
|
+
const UI_VERSION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
87
|
+
const UI_SELF_UPDATE_TIMEOUT_MS = 20 * 60 * 1000;
|
|
88
|
+
const UI_SELF_UPDATE_MAX_BUFFER = 16 * 1024 * 1024;
|
|
82
89
|
const IRIS_SAMPLE_README = '# Iris Flowers Dataset (Sample)\n' +
|
|
83
90
|
'\n' +
|
|
84
91
|
'Source: https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv\n' +
|
|
@@ -118,6 +125,18 @@ const CLAUDE_HEADLESS_STDERR_LIMIT = 12_000;
|
|
|
118
125
|
const webTerminalInitJobs = new Map();
|
|
119
126
|
const webTerminalImagePullLocks = new Map();
|
|
120
127
|
const webTerminalAgentPrepLocks = new Map();
|
|
128
|
+
const LABGATE_UI_VERSION = readPackageVersion();
|
|
129
|
+
let uiPublishedVersionCache = null;
|
|
130
|
+
let uiPublishedVersionInFlight = null;
|
|
131
|
+
let uiSelfUpdateState = {
|
|
132
|
+
status: 'idle',
|
|
133
|
+
startedAt: null,
|
|
134
|
+
finishedAt: null,
|
|
135
|
+
message: '',
|
|
136
|
+
error: null,
|
|
137
|
+
latestVersion: null,
|
|
138
|
+
};
|
|
139
|
+
let uiSelfUpdatePromise = null;
|
|
121
140
|
function getResultsStore() {
|
|
122
141
|
if (!resultsStore) {
|
|
123
142
|
resultsStore = new results_store_js_1.ResultsStore((0, config_js_1.getResultsDbPath)());
|
|
@@ -187,6 +206,256 @@ function commandErrorDetail(err) {
|
|
|
187
206
|
.map((part) => String(part).trim())
|
|
188
207
|
.join('\n');
|
|
189
208
|
}
|
|
209
|
+
function readPackageVersion() {
|
|
210
|
+
try {
|
|
211
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(PACKAGE_JSON_PATH, 'utf-8'));
|
|
212
|
+
const version = typeof parsed?.version === 'string' ? parsed.version.trim() : '';
|
|
213
|
+
return version || '0.0.0';
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return '0.0.0';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function stripVersionPrefix(raw) {
|
|
220
|
+
return String(raw || '').trim().replace(/^v/i, '');
|
|
221
|
+
}
|
|
222
|
+
function parseSemverTriplet(version) {
|
|
223
|
+
const normalized = stripVersionPrefix(version).split('-', 1)[0];
|
|
224
|
+
const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
225
|
+
if (!match)
|
|
226
|
+
return null;
|
|
227
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
228
|
+
}
|
|
229
|
+
function compareSemverStrings(a, b) {
|
|
230
|
+
const left = parseSemverTriplet(a);
|
|
231
|
+
const right = parseSemverTriplet(b);
|
|
232
|
+
if (!left || !right)
|
|
233
|
+
return null;
|
|
234
|
+
for (let i = 0; i < 3; i += 1) {
|
|
235
|
+
if (left[i] > right[i])
|
|
236
|
+
return 1;
|
|
237
|
+
if (left[i] < right[i])
|
|
238
|
+
return -1;
|
|
239
|
+
}
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
function getUiBuildId() {
|
|
243
|
+
try {
|
|
244
|
+
const st = (0, fs_1.statSync)(HTML_PATH);
|
|
245
|
+
return `${LABGATE_UI_VERSION}:${st.size}:${Math.floor(st.mtimeMs)}`;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return `${LABGATE_UI_VERSION}:unknown`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function normalizeNpmVersionValue(raw) {
|
|
252
|
+
if (typeof raw === 'string') {
|
|
253
|
+
const cleaned = stripVersionPrefix(raw).trim();
|
|
254
|
+
return cleaned || null;
|
|
255
|
+
}
|
|
256
|
+
if (Array.isArray(raw)) {
|
|
257
|
+
const versions = raw
|
|
258
|
+
.map((entry) => normalizeNpmVersionValue(entry))
|
|
259
|
+
.filter((entry) => !!entry);
|
|
260
|
+
if (versions.length === 0)
|
|
261
|
+
return null;
|
|
262
|
+
const sorted = [...versions].sort((a, b) => {
|
|
263
|
+
const cmp = compareSemverStrings(a, b);
|
|
264
|
+
if (cmp !== null)
|
|
265
|
+
return cmp;
|
|
266
|
+
return a.localeCompare(b);
|
|
267
|
+
});
|
|
268
|
+
return sorted[sorted.length - 1] || null;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function parseNpmVersionOutput(rawOutput) {
|
|
273
|
+
const text = String(rawOutput || '').trim();
|
|
274
|
+
if (!text)
|
|
275
|
+
return null;
|
|
276
|
+
try {
|
|
277
|
+
return normalizeNpmVersionValue(JSON.parse(text));
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
const cleaned = stripVersionPrefix(text.replace(/^"+|"+$/g, '').trim());
|
|
281
|
+
return cleaned || null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function summarizeCommandError(err) {
|
|
285
|
+
const detail = commandErrorDetail(err);
|
|
286
|
+
const firstLine = detail
|
|
287
|
+
.split('\n')
|
|
288
|
+
.map((line) => line.trim())
|
|
289
|
+
.find((line) => line.length > 0);
|
|
290
|
+
if (!firstLine)
|
|
291
|
+
return 'Could not reach npm registry.';
|
|
292
|
+
return firstLine.length > 180 ? `${firstLine.slice(0, 177)}...` : firstLine;
|
|
293
|
+
}
|
|
294
|
+
async function fetchPublishedUiVersion() {
|
|
295
|
+
const checkedAt = new Date().toISOString();
|
|
296
|
+
try {
|
|
297
|
+
const result = await execFileAsync('npm', ['view', UI_VERSION_NPM_PACKAGE, 'version', '--json'], {
|
|
298
|
+
timeout: UI_VERSION_CHECK_TIMEOUT_MS,
|
|
299
|
+
maxBuffer: UI_VERSION_CHECK_MAX_BUFFER,
|
|
300
|
+
});
|
|
301
|
+
const latestVersion = parseNpmVersionOutput(String(result?.stdout || ''));
|
|
302
|
+
if (!latestVersion) {
|
|
303
|
+
return {
|
|
304
|
+
latestVersion: null,
|
|
305
|
+
checkedAt,
|
|
306
|
+
error: 'npm returned an unreadable version.',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
latestVersion,
|
|
311
|
+
checkedAt,
|
|
312
|
+
error: null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
return {
|
|
317
|
+
latestVersion: null,
|
|
318
|
+
checkedAt,
|
|
319
|
+
error: summarizeCommandError(err),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function getPublishedUiVersionCached(force = false) {
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
if (!force && uiPublishedVersionCache && (now - uiPublishedVersionCache.fetchedAtMs) < UI_VERSION_CACHE_TTL_MS) {
|
|
326
|
+
return uiPublishedVersionCache.value;
|
|
327
|
+
}
|
|
328
|
+
if (!force && !uiPublishedVersionCache) {
|
|
329
|
+
return {
|
|
330
|
+
latestVersion: null,
|
|
331
|
+
checkedAt: '',
|
|
332
|
+
error: null,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (uiPublishedVersionInFlight)
|
|
336
|
+
return uiPublishedVersionInFlight;
|
|
337
|
+
uiPublishedVersionInFlight = fetchPublishedUiVersion()
|
|
338
|
+
.then((value) => {
|
|
339
|
+
uiPublishedVersionCache = { value, fetchedAtMs: Date.now() };
|
|
340
|
+
return value;
|
|
341
|
+
})
|
|
342
|
+
.finally(() => {
|
|
343
|
+
uiPublishedVersionInFlight = null;
|
|
344
|
+
});
|
|
345
|
+
return uiPublishedVersionInFlight;
|
|
346
|
+
}
|
|
347
|
+
function getActiveContainerSessionCount() {
|
|
348
|
+
const dir = (0, config_js_1.getSessionsDir)();
|
|
349
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
350
|
+
return 0;
|
|
351
|
+
const localHost = (0, os_1.hostname)();
|
|
352
|
+
const files = (0, fs_1.readdirSync)(dir).filter((f) => f.endsWith('.json'));
|
|
353
|
+
let active = 0;
|
|
354
|
+
for (const file of files) {
|
|
355
|
+
try {
|
|
356
|
+
const path = (0, path_1.join)(dir, file);
|
|
357
|
+
const data = JSON.parse((0, fs_1.readFileSync)(path, 'utf-8'));
|
|
358
|
+
if (data.node === localHost) {
|
|
359
|
+
try {
|
|
360
|
+
process.kill(Number(data.pid), 0);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
try {
|
|
364
|
+
(0, fs_1.unlinkSync)(path);
|
|
365
|
+
}
|
|
366
|
+
catch { /* best effort */ }
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
active += 1;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Skip unparseable files.
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return active;
|
|
377
|
+
}
|
|
378
|
+
async function getActiveWebTerminalSessionCount() {
|
|
379
|
+
const localHost = (0, os_1.hostname)();
|
|
380
|
+
const records = (0, web_terminal_js_1.listWebTerminalRecords)();
|
|
381
|
+
let active = 0;
|
|
382
|
+
for (const record of records) {
|
|
383
|
+
if (record.status !== 'running')
|
|
384
|
+
continue;
|
|
385
|
+
if (record.node !== localHost) {
|
|
386
|
+
active += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
let alive = false;
|
|
390
|
+
try {
|
|
391
|
+
alive = await (0, web_terminal_js_1.hasTmuxSession)(record.tmuxSession);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
alive = false;
|
|
395
|
+
}
|
|
396
|
+
if (alive) {
|
|
397
|
+
active += 1;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
(0, web_terminal_js_1.updateWebTerminalRecordStatus)(record.id, { status: 'exited', exitCode: record.exitCode ?? 0, error: null });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return active;
|
|
404
|
+
}
|
|
405
|
+
async function getActiveUiUsage() {
|
|
406
|
+
const containerSessions = getActiveContainerSessionCount();
|
|
407
|
+
const webTerminalSessions = await getActiveWebTerminalSessionCount();
|
|
408
|
+
return {
|
|
409
|
+
containerSessions,
|
|
410
|
+
webTerminalSessions,
|
|
411
|
+
total: containerSessions + webTerminalSessions,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function isUiUpdateInProgress() {
|
|
415
|
+
return !!uiSelfUpdatePromise || uiSelfUpdateState.status === 'running';
|
|
416
|
+
}
|
|
417
|
+
function runUiSelfUpdate() {
|
|
418
|
+
if (uiSelfUpdatePromise)
|
|
419
|
+
return;
|
|
420
|
+
const startedAt = new Date().toISOString();
|
|
421
|
+
uiSelfUpdateState = {
|
|
422
|
+
status: 'running',
|
|
423
|
+
startedAt,
|
|
424
|
+
finishedAt: null,
|
|
425
|
+
message: `Installing ${UI_VERSION_NPM_PACKAGE}@latest...`,
|
|
426
|
+
error: null,
|
|
427
|
+
latestVersion: null,
|
|
428
|
+
};
|
|
429
|
+
uiSelfUpdatePromise = (async () => {
|
|
430
|
+
try {
|
|
431
|
+
await execFileAsync('npm', ['install', '-g', `${UI_VERSION_NPM_PACKAGE}@latest`], {
|
|
432
|
+
timeout: UI_SELF_UPDATE_TIMEOUT_MS,
|
|
433
|
+
maxBuffer: UI_SELF_UPDATE_MAX_BUFFER,
|
|
434
|
+
});
|
|
435
|
+
const published = await getPublishedUiVersionCached(true);
|
|
436
|
+
uiSelfUpdateState = {
|
|
437
|
+
status: 'success',
|
|
438
|
+
startedAt,
|
|
439
|
+
finishedAt: new Date().toISOString(),
|
|
440
|
+
message: 'Update complete. Restart `labgate ui` to load the new version.',
|
|
441
|
+
error: null,
|
|
442
|
+
latestVersion: published.latestVersion,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
uiSelfUpdateState = {
|
|
447
|
+
status: 'error',
|
|
448
|
+
startedAt,
|
|
449
|
+
finishedAt: new Date().toISOString(),
|
|
450
|
+
message: 'Update failed.',
|
|
451
|
+
error: summarizeCommandError(err),
|
|
452
|
+
latestVersion: null,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
})().finally(() => {
|
|
456
|
+
uiSelfUpdatePromise = null;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
190
459
|
function isPodmanNotReadyError(error) {
|
|
191
460
|
return /podman is installed but not ready/i.test(error || '');
|
|
192
461
|
}
|
|
@@ -1086,7 +1355,7 @@ function isClaudeAuthenticationFailure(event, assistantSnapshot, stderrText) {
|
|
|
1086
1355
|
const authRe = /oauth token has expired|authentication_error|failed to authenticate|api error:\s*401/i;
|
|
1087
1356
|
return authRe.test(packed) || authRe.test(message);
|
|
1088
1357
|
}
|
|
1089
|
-
function buildClaudeHeadlessApptainerArgs(config, workdir, prompt, resumeSessionId) {
|
|
1358
|
+
function buildClaudeHeadlessApptainerArgs(config, workdir, prompt, resumeSessionId, runWithAllowedPermissions) {
|
|
1090
1359
|
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
1091
1360
|
const sifPath = (0, path_1.join)((0, config_js_1.getImagesDir)(), (0, container_js_1.imageToSifName)(config.image));
|
|
1092
1361
|
const resume = resumeSessionId.trim();
|
|
@@ -1128,6 +1397,7 @@ function buildClaudeHeadlessApptainerArgs(config, workdir, prompt, resumeSession
|
|
|
1128
1397
|
'--output-format',
|
|
1129
1398
|
'stream-json',
|
|
1130
1399
|
'--include-partial-messages',
|
|
1400
|
+
...(runWithAllowedPermissions ? ['--dangerously-skip-permissions'] : []),
|
|
1131
1401
|
...(resume ? ['--resume', resume] : []),
|
|
1132
1402
|
prompt,
|
|
1133
1403
|
];
|
|
@@ -1380,7 +1650,7 @@ async function startClaudeHeadlessWsRun(ws, record, prompt, resumeSessionId) {
|
|
|
1380
1650
|
});
|
|
1381
1651
|
return () => { };
|
|
1382
1652
|
}
|
|
1383
|
-
const args = buildClaudeHeadlessApptainerArgs(config, record.workdir, trimmedPrompt, resumeSessionId);
|
|
1653
|
+
const args = buildClaudeHeadlessApptainerArgs(config, record.workdir, trimmedPrompt, resumeSessionId, (0, config_js_1.shouldClaudeHeadlessRunWithAllowedPermissions)(config));
|
|
1384
1654
|
const child = (0, child_process_1.spawn)('apptainer', args, {
|
|
1385
1655
|
cwd: record.workdir,
|
|
1386
1656
|
env: process.env,
|
|
@@ -1653,6 +1923,8 @@ async function handlePostConfig(req, res) {
|
|
|
1653
1923
|
obj.audit = incoming.audit;
|
|
1654
1924
|
if (incoming.slurm)
|
|
1655
1925
|
obj.slurm = incoming.slurm;
|
|
1926
|
+
if (incoming.headless)
|
|
1927
|
+
obj.headless = incoming.headless;
|
|
1656
1928
|
const { writeFileSync } = await import('fs');
|
|
1657
1929
|
writeFileSync(configPath, JSON.stringify(obj, null, 2) + '\n', { encoding: 'utf-8', mode: config_js_1.PRIVATE_FILE_MODE });
|
|
1658
1930
|
(0, config_js_1.ensurePrivateFile)(configPath);
|
|
@@ -1665,6 +1937,65 @@ async function handlePostConfig(req, res) {
|
|
|
1665
1937
|
function handleGetConfigPath(_req, res) {
|
|
1666
1938
|
json(res, { path: (0, config_js_1.getConfigPath)() });
|
|
1667
1939
|
}
|
|
1940
|
+
async function handleGetUiVersion(reqUrl, res) {
|
|
1941
|
+
const forceRefresh = reqUrl.searchParams.get('refresh') === '1';
|
|
1942
|
+
const published = await getPublishedUiVersionCached(forceRefresh);
|
|
1943
|
+
const runningVersion = LABGATE_UI_VERSION;
|
|
1944
|
+
const latestVersion = published.latestVersion;
|
|
1945
|
+
let updateAvailable = false;
|
|
1946
|
+
if (latestVersion) {
|
|
1947
|
+
const cmp = compareSemverStrings(latestVersion, runningVersion);
|
|
1948
|
+
updateAvailable = cmp === null
|
|
1949
|
+
? stripVersionPrefix(latestVersion) !== stripVersionPrefix(runningVersion)
|
|
1950
|
+
: cmp > 0;
|
|
1951
|
+
}
|
|
1952
|
+
json(res, {
|
|
1953
|
+
ok: true,
|
|
1954
|
+
runningVersion,
|
|
1955
|
+
uiBuildId: getUiBuildId(),
|
|
1956
|
+
latestVersion,
|
|
1957
|
+
latestCheckedAt: published.checkedAt,
|
|
1958
|
+
updateAvailable,
|
|
1959
|
+
updateCommand: `npm install -g ${UI_VERSION_NPM_PACKAGE}@latest`,
|
|
1960
|
+
restartCommand: 'labgate ui',
|
|
1961
|
+
checkError: published.error,
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
function handleGetUiUpdateStatus(_req, res) {
|
|
1965
|
+
json(res, {
|
|
1966
|
+
ok: true,
|
|
1967
|
+
status: uiSelfUpdateState.status,
|
|
1968
|
+
startedAt: uiSelfUpdateState.startedAt,
|
|
1969
|
+
finishedAt: uiSelfUpdateState.finishedAt,
|
|
1970
|
+
message: uiSelfUpdateState.message,
|
|
1971
|
+
error: uiSelfUpdateState.error,
|
|
1972
|
+
latestVersion: uiSelfUpdateState.latestVersion,
|
|
1973
|
+
inProgress: isUiUpdateInProgress(),
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
async function handlePostUiUpdate(_req, res) {
|
|
1977
|
+
if (isUiUpdateInProgress()) {
|
|
1978
|
+
json(res, { ok: false, code: 'in_progress', error: 'An update is already running.' }, 409);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const usage = await getActiveUiUsage();
|
|
1982
|
+
if (usage.total > 0) {
|
|
1983
|
+
json(res, {
|
|
1984
|
+
ok: false,
|
|
1985
|
+
code: 'in_use',
|
|
1986
|
+
error: 'Stop active sessions before updating LabGate.',
|
|
1987
|
+
activeUsage: usage,
|
|
1988
|
+
}, 409);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
runUiSelfUpdate();
|
|
1992
|
+
json(res, {
|
|
1993
|
+
ok: true,
|
|
1994
|
+
started: true,
|
|
1995
|
+
status: uiSelfUpdateState.status,
|
|
1996
|
+
message: uiSelfUpdateState.message,
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1668
1999
|
const INSTRUCTION_FILE_MAP = {
|
|
1669
2000
|
'agent.md': 'AGENTS.md',
|
|
1670
2001
|
'agents.md': 'AGENTS.md',
|
|
@@ -5571,6 +5902,20 @@ function upgradeBadRequest(socket) {
|
|
|
5571
5902
|
// Best effort.
|
|
5572
5903
|
}
|
|
5573
5904
|
}
|
|
5905
|
+
function upgradeServiceUnavailable(socket) {
|
|
5906
|
+
try {
|
|
5907
|
+
socket.write('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
|
|
5908
|
+
}
|
|
5909
|
+
catch {
|
|
5910
|
+
// Best effort.
|
|
5911
|
+
}
|
|
5912
|
+
try {
|
|
5913
|
+
socket.destroy();
|
|
5914
|
+
}
|
|
5915
|
+
catch {
|
|
5916
|
+
// Best effort.
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5574
5919
|
/**
|
|
5575
5920
|
* Start the settings UI server.
|
|
5576
5921
|
* Returns the HTTP server so callers can close it when done.
|
|
@@ -5790,6 +6135,18 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
5790
6135
|
return;
|
|
5791
6136
|
}
|
|
5792
6137
|
try {
|
|
6138
|
+
if (isUiUpdateInProgress() &&
|
|
6139
|
+
pathname.startsWith('/api/') &&
|
|
6140
|
+
method !== 'GET' &&
|
|
6141
|
+
method !== 'HEAD' &&
|
|
6142
|
+
pathname !== '/api/ui/update') {
|
|
6143
|
+
json(res, {
|
|
6144
|
+
ok: false,
|
|
6145
|
+
code: 'update_in_progress',
|
|
6146
|
+
error: 'LabGate update in progress. Write actions are temporarily locked.',
|
|
6147
|
+
}, 503);
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
5793
6150
|
if (pathname.startsWith('/api/') &&
|
|
5794
6151
|
method !== 'GET' &&
|
|
5795
6152
|
method !== 'HEAD' &&
|
|
@@ -5808,6 +6165,15 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
5808
6165
|
else if (pathname === '/api/config/path' && method === 'GET') {
|
|
5809
6166
|
handleGetConfigPath(req, res);
|
|
5810
6167
|
}
|
|
6168
|
+
else if (pathname === '/api/ui/version' && method === 'GET') {
|
|
6169
|
+
await handleGetUiVersion(reqUrl, res);
|
|
6170
|
+
}
|
|
6171
|
+
else if (pathname === '/api/ui/update/status' && method === 'GET') {
|
|
6172
|
+
handleGetUiUpdateStatus(req, res);
|
|
6173
|
+
}
|
|
6174
|
+
else if (pathname === '/api/ui/update' && method === 'POST') {
|
|
6175
|
+
await handlePostUiUpdate(req, res);
|
|
6176
|
+
}
|
|
5811
6177
|
else if (pathname === '/api/sessions' && method === 'GET') {
|
|
5812
6178
|
await handleGetSessions(req, res);
|
|
5813
6179
|
}
|
|
@@ -6029,6 +6395,10 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
6029
6395
|
upgradeBadRequest(socket);
|
|
6030
6396
|
return;
|
|
6031
6397
|
}
|
|
6398
|
+
if (isUiUpdateInProgress()) {
|
|
6399
|
+
upgradeServiceUnavailable(socket);
|
|
6400
|
+
return;
|
|
6401
|
+
}
|
|
6032
6402
|
if (useTcp) {
|
|
6033
6403
|
const auth = isAuthorizedRequest(req, reqUrl, uiAccessToken);
|
|
6034
6404
|
if (!auth.ok) {
|