@slock-ai/computer 0.0.1-play.20260521182209 → 0.0.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/index.js +969 -247
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ var DeviceAuthClient = class {
|
|
|
24
24
|
});
|
|
25
25
|
if (res.status === 404) {
|
|
26
26
|
throw new Error(
|
|
27
|
-
"device login is not enabled on this server (ask an admin to
|
|
27
|
+
"device login is not enabled on this server (ask an admin to unset SLOCK_DEVICE_LOGIN_ENABLED or set it back to a non-false value; this surface is on by default since PR-G \u2014 upgrade the server if you are on an older build)"
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
if (res.status !== 201) {
|
|
@@ -67,14 +67,14 @@ var ComputerAttachClient = class {
|
|
|
67
67
|
return new URL(p, this.baseUrl).toString();
|
|
68
68
|
}
|
|
69
69
|
/** POST /api/computer/attach — user-authed; issues sk_computer_*. */
|
|
70
|
-
async attach(
|
|
70
|
+
async attach(serverSlug, name) {
|
|
71
71
|
const res = await fetch2(this.url("/api/computer/attach"), {
|
|
72
72
|
method: "POST",
|
|
73
73
|
headers: {
|
|
74
74
|
"Content-Type": "application/json",
|
|
75
75
|
Authorization: `Bearer ${this.accessToken}`
|
|
76
76
|
},
|
|
77
|
-
body: JSON.stringify({
|
|
77
|
+
body: JSON.stringify({ serverSlug, name })
|
|
78
78
|
});
|
|
79
79
|
const body = await res.json().catch(() => null);
|
|
80
80
|
if (res.status === 201 && body && typeof body.apiKey === "string") {
|
|
@@ -82,7 +82,8 @@ var ComputerAttachClient = class {
|
|
|
82
82
|
status: "success",
|
|
83
83
|
apiKey: body.apiKey,
|
|
84
84
|
serverMachineId: String(body.serverMachineId ?? ""),
|
|
85
|
-
serverId: String(body.serverId ??
|
|
85
|
+
serverId: String(body.serverId ?? ""),
|
|
86
|
+
serverSlug: String(body.serverSlug ?? serverSlug),
|
|
86
87
|
resumed: body.resumed === true
|
|
87
88
|
};
|
|
88
89
|
}
|
|
@@ -92,6 +93,45 @@ var ComputerAttachClient = class {
|
|
|
92
93
|
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
93
94
|
return { status: "error", code };
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* POST /api/computer/adopt-legacy — task #39 PR-J1 (RFC v8.2 §5.11).
|
|
98
|
+
* User-authed; the legacy `sk_machine_*` key is the proof of authority
|
|
99
|
+
* over the machine. On success the server marks the machine row migrated
|
|
100
|
+
* and mints a fresh `sk_computer_*`. The raw legacy key MUST NOT be
|
|
101
|
+
* persisted by the caller — only the freshly-minted sk_computer_* lands
|
|
102
|
+
* in attachment.json.
|
|
103
|
+
*/
|
|
104
|
+
async adoptLegacy(legacyApiKey, name) {
|
|
105
|
+
const res = await fetch2(this.url("/api/computer/adopt-legacy"), {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({ legacyApiKey, ...name ? { name } : {} })
|
|
112
|
+
});
|
|
113
|
+
const body = await res.json().catch(() => null);
|
|
114
|
+
if (res.status === 201 && body && typeof body.apiKey === "string") {
|
|
115
|
+
return {
|
|
116
|
+
status: "success",
|
|
117
|
+
apiKey: body.apiKey,
|
|
118
|
+
computerId: String(body.computerId ?? ""),
|
|
119
|
+
machineId: String(body.machineId ?? ""),
|
|
120
|
+
serverId: String(body.serverId ?? ""),
|
|
121
|
+
resumed: body.resumed === true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
125
|
+
if (res.status === 401 && code === "legacy_key_invalid") {
|
|
126
|
+
return { status: "legacy_key_invalid" };
|
|
127
|
+
}
|
|
128
|
+
if (res.status === 409 && code === "legacy_machine_key_migrated") {
|
|
129
|
+
return { status: "legacy_machine_key_migrated" };
|
|
130
|
+
}
|
|
131
|
+
if (res.status === 403) return { status: "not_authorized" };
|
|
132
|
+
if (res.status === 404) return { status: "disabled" };
|
|
133
|
+
return { status: "error", code };
|
|
134
|
+
}
|
|
95
135
|
/**
|
|
96
136
|
* POST /internal/computer/preflight — §9 READ-ONLY, side-effect-free.
|
|
97
137
|
* Authenticated with the freshly-issued sk_computer_* (NOT the user
|
|
@@ -108,7 +148,12 @@ var ComputerAttachClient = class {
|
|
|
108
148
|
body: "{}"
|
|
109
149
|
});
|
|
110
150
|
const body = await res.json().catch(() => null);
|
|
111
|
-
if (res.status === 200 && body && body.ok === true)
|
|
151
|
+
if (res.status === 200 && body && body.ok === true) {
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
serverSlug: typeof body.serverSlug === "string" && body.serverSlug.length > 0 ? body.serverSlug : void 0
|
|
155
|
+
};
|
|
156
|
+
}
|
|
112
157
|
const code = body && typeof body.code === "string" ? body.code : `http_${res.status}`;
|
|
113
158
|
return { ok: false, code };
|
|
114
159
|
}
|
|
@@ -154,6 +199,7 @@ var RunnersClient = class {
|
|
|
154
199
|
};
|
|
155
200
|
|
|
156
201
|
// src/paths.ts
|
|
202
|
+
import { createHash } from "crypto";
|
|
157
203
|
import os from "os";
|
|
158
204
|
import path from "path";
|
|
159
205
|
function resolveSlockHome(env = process.env, homeDir = os.homedir()) {
|
|
@@ -212,6 +258,32 @@ function supervisorLogPath(slockHome) {
|
|
|
212
258
|
function supervisorVersionPath(slockHome) {
|
|
213
259
|
return path.join(computerDir(slockHome), "supervisor-version.json");
|
|
214
260
|
}
|
|
261
|
+
var HOSTNAME_SUFFIXES = [
|
|
262
|
+
".fritz.box",
|
|
263
|
+
".localdomain",
|
|
264
|
+
".local",
|
|
265
|
+
".lan",
|
|
266
|
+
".home"
|
|
267
|
+
];
|
|
268
|
+
function shortHostnameHash(hostname) {
|
|
269
|
+
return createHash("sha256").update(hostname).digest("hex").slice(0, 8);
|
|
270
|
+
}
|
|
271
|
+
function deriveDefaultComputerName(hostname = os.hostname()) {
|
|
272
|
+
const original = hostname.trim();
|
|
273
|
+
let name = original;
|
|
274
|
+
let lower = name.toLowerCase();
|
|
275
|
+
for (; ; ) {
|
|
276
|
+
const suffix = HOSTNAME_SUFFIXES.find((candidate) => lower.endsWith(candidate));
|
|
277
|
+
if (!suffix) break;
|
|
278
|
+
name = name.slice(0, -suffix.length);
|
|
279
|
+
lower = name.toLowerCase();
|
|
280
|
+
}
|
|
281
|
+
name = name.replace(/[\s'"]+/g, "-").replace(/-+$/g, "");
|
|
282
|
+
return name.length > 0 ? name : `slock-computer-${shortHostnameHash(original)}`;
|
|
283
|
+
}
|
|
284
|
+
function adoptionLogPath(slockHome) {
|
|
285
|
+
return path.join(computerDir(slockHome), "adoption.log");
|
|
286
|
+
}
|
|
215
287
|
function channelPath(slockHome) {
|
|
216
288
|
return path.join(computerDir(slockHome), "channel");
|
|
217
289
|
}
|
|
@@ -248,7 +320,8 @@ async function runLogin(opts) {
|
|
|
248
320
|
} catch (err) {
|
|
249
321
|
fail("DEVICE_AUTHORIZE_FAILED", err instanceof Error ? err.message : String(err));
|
|
250
322
|
}
|
|
251
|
-
const
|
|
323
|
+
const verificationUri = grant.verificationUriComplete || grant.verificationUri;
|
|
324
|
+
const verifyUrl = new URL(verificationUri, baseUrl).toString();
|
|
252
325
|
info(`To finish login, open: ${verifyUrl}`);
|
|
253
326
|
info(`and enter the code: ${grant.userCode}`);
|
|
254
327
|
info(`Waiting for approval (expires in ${grant.expiresIn}s)\u2026`);
|
|
@@ -274,21 +347,129 @@ async function runLogin(opts) {
|
|
|
274
347
|
{ mode: 384 }
|
|
275
348
|
);
|
|
276
349
|
info(`Logged in. User session written to ${file}`);
|
|
277
|
-
info(`Next: run \`slock-computer attach <
|
|
350
|
+
info(`Next: run \`slock-computer attach <serverSlug>\` to attach this machine.`);
|
|
278
351
|
return;
|
|
279
352
|
}
|
|
280
353
|
fail("LOGIN_EXPIRED", "Login request expired before approval. Re-run `slock-computer login`.");
|
|
281
354
|
}
|
|
282
355
|
|
|
283
356
|
// src/attach.ts
|
|
284
|
-
import { chmod, mkdir as
|
|
357
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
358
|
+
import { dirname as dirname3 } from "path";
|
|
359
|
+
|
|
360
|
+
// src/serverState.ts
|
|
361
|
+
import { readFile, readdir, writeFile as writeFile2, mkdir as mkdir2, unlink, access, chmod } from "fs/promises";
|
|
285
362
|
import { dirname as dirname2 } from "path";
|
|
363
|
+
import { constants as fsConstants } from "fs";
|
|
364
|
+
function parseAttachment(raw) {
|
|
365
|
+
try {
|
|
366
|
+
const a = JSON.parse(raw);
|
|
367
|
+
if (a.kind === "computer-attachment" && typeof a.serverId === "string" && typeof a.serverMachineId === "string" && typeof a.apiKey === "string" && a.apiKey.length > 0 && typeof a.serverUrl === "string") {
|
|
368
|
+
return {
|
|
369
|
+
kind: "computer-attachment",
|
|
370
|
+
serverId: a.serverId,
|
|
371
|
+
serverSlug: typeof a.serverSlug === "string" && a.serverSlug.length > 0 ? a.serverSlug : void 0,
|
|
372
|
+
serverMachineId: a.serverMachineId,
|
|
373
|
+
apiKey: a.apiKey,
|
|
374
|
+
serverUrl: a.serverUrl,
|
|
375
|
+
attachedAt: typeof a.attachedAt === "string" ? a.attachedAt : void 0
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
async function readServerAttachment(slockHome, serverId) {
|
|
383
|
+
if (!isValidServerId(serverId)) return null;
|
|
384
|
+
try {
|
|
385
|
+
return parseAttachment(await readFile(serverAttachmentPath(slockHome, serverId), "utf8"));
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function writeServerAttachment(slockHome, attachment) {
|
|
391
|
+
if (!isValidServerId(attachment.serverId)) return;
|
|
392
|
+
const path2 = serverAttachmentPath(slockHome, attachment.serverId);
|
|
393
|
+
await mkdir2(dirname2(path2), { recursive: true });
|
|
394
|
+
await writeFile2(path2, JSON.stringify(attachment, null, 2), { mode: 384 });
|
|
395
|
+
await chmod(path2, 384);
|
|
396
|
+
}
|
|
397
|
+
async function listAttachedServerIds(slockHome) {
|
|
398
|
+
let entries;
|
|
399
|
+
try {
|
|
400
|
+
entries = await readdir(serversDir(slockHome));
|
|
401
|
+
} catch {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const ids = [];
|
|
405
|
+
for (const name of entries) {
|
|
406
|
+
if (!isValidServerId(name)) continue;
|
|
407
|
+
if (await readServerAttachment(slockHome, name)) ids.push(name);
|
|
408
|
+
}
|
|
409
|
+
return ids.sort();
|
|
410
|
+
}
|
|
411
|
+
async function listServerAttachments(slockHome) {
|
|
412
|
+
const ids = await listAttachedServerIds(slockHome);
|
|
413
|
+
const out = [];
|
|
414
|
+
for (const id of ids) {
|
|
415
|
+
const a = await readServerAttachment(slockHome, id);
|
|
416
|
+
if (a) out.push(a);
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
function normalizeServerSlug(input) {
|
|
421
|
+
const trimmed = input.trim();
|
|
422
|
+
if (!trimmed) return "";
|
|
423
|
+
return trimmed.startsWith("/") ? trimmed.slice(1) : trimmed;
|
|
424
|
+
}
|
|
425
|
+
function formatServerSlugDisplay(slug) {
|
|
426
|
+
if (!slug) return "(unknown slug; re-run attach or doctor)";
|
|
427
|
+
return slug.startsWith("/") ? slug : `/${slug}`;
|
|
428
|
+
}
|
|
429
|
+
async function resolveAttachedServerSlug(slockHome, serverSlug) {
|
|
430
|
+
const requested = normalizeServerSlug(serverSlug);
|
|
431
|
+
if (!requested) return null;
|
|
432
|
+
const attachments = await listServerAttachments(slockHome);
|
|
433
|
+
return attachments.find((a) => a.serverSlug === requested) ?? null;
|
|
434
|
+
}
|
|
435
|
+
async function setServerManaged(slockHome, serverId) {
|
|
436
|
+
if (!isValidServerId(serverId)) return;
|
|
437
|
+
const flagPath = serverManagedFlagPath(slockHome, serverId);
|
|
438
|
+
await mkdir2(dirname2(flagPath), { recursive: true });
|
|
439
|
+
await writeFile2(flagPath, "", { mode: 384 });
|
|
440
|
+
}
|
|
441
|
+
async function clearServerManaged(slockHome, serverId) {
|
|
442
|
+
if (!isValidServerId(serverId)) return;
|
|
443
|
+
try {
|
|
444
|
+
await unlink(serverManagedFlagPath(slockHome, serverId));
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function isServerManaged(slockHome, serverId) {
|
|
449
|
+
if (!isValidServerId(serverId)) return false;
|
|
450
|
+
try {
|
|
451
|
+
await access(serverManagedFlagPath(slockHome, serverId), fsConstants.F_OK);
|
|
452
|
+
return true;
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function listManagedServerIds(slockHome) {
|
|
458
|
+
const attached = await listAttachedServerIds(slockHome);
|
|
459
|
+
const out = [];
|
|
460
|
+
for (const id of attached) {
|
|
461
|
+
if (await isServerManaged(slockHome, id)) out.push(id);
|
|
462
|
+
}
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/attach.ts
|
|
286
467
|
async function runAttach(opts) {
|
|
287
468
|
const slockHome = resolveSlockHome();
|
|
288
469
|
const sessionFile = userSessionPath(slockHome);
|
|
289
470
|
let session;
|
|
290
471
|
try {
|
|
291
|
-
session = JSON.parse(await
|
|
472
|
+
session = JSON.parse(await readFile2(sessionFile, "utf8"));
|
|
292
473
|
} catch {
|
|
293
474
|
fail(
|
|
294
475
|
"NO_USER_SESSION",
|
|
@@ -306,18 +487,23 @@ async function runAttach(opts) {
|
|
|
306
487
|
fail("MISSING_SERVER_URL", "Set SLOCK_SERVER_URL or pass --server-url <url>.");
|
|
307
488
|
}
|
|
308
489
|
const client = new ComputerAttachClient(baseUrl, session.accessToken);
|
|
309
|
-
|
|
310
|
-
|
|
490
|
+
const slugForServer = normalizeServerSlug(opts.serverSlug);
|
|
491
|
+
if (!slugForServer) {
|
|
492
|
+
fail("ATTACH_NOT_AUTHORIZED", "Server slug must not be empty.");
|
|
493
|
+
}
|
|
494
|
+
const computerName = opts.name?.trim() || deriveDefaultComputerName();
|
|
495
|
+
info(`Attaching this machine to server ${formatServerSlugDisplay(slugForServer)}\u2026`);
|
|
496
|
+
const attached = await client.attach(slugForServer, computerName);
|
|
311
497
|
if (attached.status === "disabled") {
|
|
312
498
|
fail(
|
|
313
499
|
"ATTACH_DISABLED",
|
|
314
|
-
"Computer attach is not enabled on this server (ask an admin to
|
|
500
|
+
"Computer attach is not enabled on this server (ask an admin to unset SLOCK_DEVICE_LOGIN_ENABLED or set it back to a non-false value; this surface is on by default since PR-G \u2014 upgrade the server if you are on an older build)."
|
|
315
501
|
);
|
|
316
502
|
}
|
|
317
503
|
if (attached.status === "not_authorized") {
|
|
318
504
|
fail(
|
|
319
505
|
"ATTACH_NOT_AUTHORIZED",
|
|
320
|
-
"Not authorized to attach to that server. Check the server
|
|
506
|
+
"Not authorized to attach to that server. Check the server slug and that you're a member."
|
|
321
507
|
);
|
|
322
508
|
}
|
|
323
509
|
if (attached.status === "error") {
|
|
@@ -327,6 +513,12 @@ async function runAttach(opts) {
|
|
|
327
513
|
"Your user session is no longer valid. Re-run `slock-computer login`."
|
|
328
514
|
);
|
|
329
515
|
}
|
|
516
|
+
if (attached.code === "COMPUTER_NAME_COLLISION") {
|
|
517
|
+
fail(
|
|
518
|
+
"COMPUTER_NAME_COLLISION",
|
|
519
|
+
`A Computer named ${JSON.stringify(computerName)} already exists on that server. Re-run with --name <name>.`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
330
522
|
fail("ATTACH_FAILED", `Attach failed (${attached.code}). Re-run after checking --server-url / server version.`);
|
|
331
523
|
}
|
|
332
524
|
info(attached.resumed ? "Resumed existing attachment; running preflight\u2026" : "Attachment issued; running preflight\u2026");
|
|
@@ -338,13 +530,14 @@ async function runAttach(opts) {
|
|
|
338
530
|
);
|
|
339
531
|
}
|
|
340
532
|
const file = serverAttachmentPath(slockHome, attached.serverId);
|
|
341
|
-
await
|
|
342
|
-
await
|
|
533
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
534
|
+
await writeFile3(
|
|
343
535
|
file,
|
|
344
536
|
JSON.stringify(
|
|
345
537
|
{
|
|
346
538
|
kind: "computer-attachment",
|
|
347
539
|
serverId: attached.serverId,
|
|
540
|
+
serverSlug: attached.serverSlug,
|
|
348
541
|
serverMachineId: attached.serverMachineId,
|
|
349
542
|
apiKey: attached.apiKey,
|
|
350
543
|
serverUrl: baseUrl,
|
|
@@ -355,9 +548,9 @@ async function runAttach(opts) {
|
|
|
355
548
|
),
|
|
356
549
|
{ mode: 384 }
|
|
357
550
|
);
|
|
358
|
-
await
|
|
551
|
+
await chmod2(file, 384);
|
|
359
552
|
info(`Attached. Computer state written to ${file}`);
|
|
360
|
-
info(` server: ${attached.
|
|
553
|
+
info(` server: ${formatServerSlugDisplay(attached.serverSlug)}`);
|
|
361
554
|
info(` serverMachine: ${attached.serverMachineId}`);
|
|
362
555
|
if (opts.run === false) {
|
|
363
556
|
info("Next: run `slock-computer start` to run it in the background.");
|
|
@@ -366,102 +559,357 @@ async function runAttach(opts) {
|
|
|
366
559
|
}
|
|
367
560
|
}
|
|
368
561
|
|
|
369
|
-
// src/
|
|
370
|
-
import { readFile as
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
import {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
kind: "computer-attachment",
|
|
382
|
-
serverId: a.serverId,
|
|
383
|
-
serverMachineId: a.serverMachineId,
|
|
384
|
-
apiKey: a.apiKey,
|
|
385
|
-
serverUrl: a.serverUrl,
|
|
386
|
-
attachedAt: typeof a.attachedAt === "string" ? a.attachedAt : void 0
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
} catch {
|
|
562
|
+
// src/adopt.ts
|
|
563
|
+
import { chmod as chmod3, mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, appendFile, stat } from "fs/promises";
|
|
564
|
+
import { createHash as createHash2 } from "crypto";
|
|
565
|
+
import { dirname as dirname4, join } from "path";
|
|
566
|
+
import { setTimeout as delay } from "timers/promises";
|
|
567
|
+
async function resolveLegacyKey(inputs, env) {
|
|
568
|
+
const sources = [];
|
|
569
|
+
if (typeof inputs.legacyApiKey === "string" && inputs.legacyApiKey.length > 0) {
|
|
570
|
+
sources.push({
|
|
571
|
+
mode: "legacy_key_argv",
|
|
572
|
+
load: async () => inputs.legacyApiKey.trim()
|
|
573
|
+
});
|
|
390
574
|
}
|
|
391
|
-
|
|
575
|
+
if (typeof inputs.legacyApiKeyFile === "string" && inputs.legacyApiKeyFile.length > 0) {
|
|
576
|
+
sources.push({
|
|
577
|
+
mode: "legacy_key_file",
|
|
578
|
+
load: async () => {
|
|
579
|
+
const contents = await readFile3(inputs.legacyApiKeyFile, "utf8");
|
|
580
|
+
return contents.trim();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (inputs.legacyApiKeyStdin) {
|
|
585
|
+
sources.push({
|
|
586
|
+
mode: "legacy_key_stdin",
|
|
587
|
+
load: async () => readAllStdin()
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
const envKey = env.SLOCK_LEGACY_API_KEY;
|
|
591
|
+
if (typeof envKey === "string" && envKey.trim().length > 0) {
|
|
592
|
+
sources.push({
|
|
593
|
+
mode: "legacy_key_env",
|
|
594
|
+
load: async () => envKey.trim()
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (sources.length === 0) {
|
|
598
|
+
fail(
|
|
599
|
+
"LEGACY_KEY_REQUIRED",
|
|
600
|
+
"No legacy api key provided. Pass exactly one of: --legacy-api-key <key>, --legacy-api-key-file <path>, --legacy-api-key-stdin, or SLOCK_LEGACY_API_KEY."
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (sources.length > 1) {
|
|
604
|
+
const modes = sources.map((s) => s.mode).join(", ");
|
|
605
|
+
fail(
|
|
606
|
+
"LEGACY_KEY_MULTIPLE_SOURCES",
|
|
607
|
+
`Multiple legacy api key sources provided (${modes}). Choose exactly one.`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
const chosen = sources[0];
|
|
611
|
+
const rawKey = await chosen.load();
|
|
612
|
+
if (chosen.mode === "legacy_key_env") {
|
|
613
|
+
delete env.SLOCK_LEGACY_API_KEY;
|
|
614
|
+
}
|
|
615
|
+
if (!rawKey.startsWith("sk_machine_") && !rawKey.startsWith("sk_daemon_")) {
|
|
616
|
+
fail(
|
|
617
|
+
"LEGACY_KEY_INVALID",
|
|
618
|
+
"Provided key does not look like a legacy machine key (expected `sk_machine_*` or `sk_daemon_*`)."
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
rawKey,
|
|
623
|
+
mode: chosen.mode,
|
|
624
|
+
redactedPrefix: rawKey.slice(0, 8)
|
|
625
|
+
};
|
|
392
626
|
}
|
|
393
|
-
async function
|
|
394
|
-
|
|
627
|
+
async function readAllStdin() {
|
|
628
|
+
return new Promise((resolve, reject) => {
|
|
629
|
+
const chunks = [];
|
|
630
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
631
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8").trim()));
|
|
632
|
+
process.stdin.on("error", reject);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
var LEGACY_STOP_WAIT_MS = 1e4;
|
|
636
|
+
var LEGACY_STOP_POLL_MS = 200;
|
|
637
|
+
function legacyLockOwnerPath(slockHome, rawKey) {
|
|
638
|
+
const fingerprint = createHash2("sha256").update(rawKey).digest("hex").slice(0, 16);
|
|
639
|
+
return join(slockHome, "machines", `machine-${fingerprint}`, "daemon.lock", "owner.json");
|
|
640
|
+
}
|
|
641
|
+
function isProcessAlive(pid) {
|
|
642
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
395
643
|
try {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
644
|
+
process.kill(pid, 0);
|
|
645
|
+
return true;
|
|
646
|
+
} catch (err) {
|
|
647
|
+
const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
|
|
648
|
+
return code !== "ESRCH";
|
|
399
649
|
}
|
|
400
650
|
}
|
|
401
|
-
async function
|
|
402
|
-
let
|
|
651
|
+
async function stopLegacyDaemonByOwnerFile(ownerFile) {
|
|
652
|
+
let raw;
|
|
403
653
|
try {
|
|
404
|
-
|
|
654
|
+
raw = await readFile3(ownerFile, "utf8");
|
|
405
655
|
} catch {
|
|
406
|
-
return
|
|
656
|
+
return { attempted: false, outcome: "absent" };
|
|
407
657
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (
|
|
658
|
+
let pid;
|
|
659
|
+
try {
|
|
660
|
+
const owner = JSON.parse(raw);
|
|
661
|
+
if (typeof owner.pid !== "number") {
|
|
662
|
+
return { attempted: false, outcome: "error", reason: "owner_json_missing_pid" };
|
|
663
|
+
}
|
|
664
|
+
pid = owner.pid;
|
|
665
|
+
} catch {
|
|
666
|
+
return { attempted: false, outcome: "error", reason: "owner_json_unparseable" };
|
|
412
667
|
}
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
async function listServerAttachments(slockHome) {
|
|
416
|
-
const ids = await listAttachedServerIds(slockHome);
|
|
417
|
-
const out = [];
|
|
418
|
-
for (const id of ids) {
|
|
419
|
-
const a = await readServerAttachment(slockHome, id);
|
|
420
|
-
if (a) out.push(a);
|
|
668
|
+
if (!isProcessAlive(pid)) {
|
|
669
|
+
return { attempted: false, pid, outcome: "already_dead" };
|
|
421
670
|
}
|
|
422
|
-
return out;
|
|
423
|
-
}
|
|
424
|
-
async function setServerManaged(slockHome, serverId) {
|
|
425
|
-
if (!isValidServerId(serverId)) return;
|
|
426
|
-
const flagPath = serverManagedFlagPath(slockHome, serverId);
|
|
427
|
-
await mkdir3(dirname3(flagPath), { recursive: true });
|
|
428
|
-
await writeFile3(flagPath, "", { mode: 384 });
|
|
429
|
-
}
|
|
430
|
-
async function clearServerManaged(slockHome, serverId) {
|
|
431
|
-
if (!isValidServerId(serverId)) return;
|
|
432
671
|
try {
|
|
433
|
-
|
|
434
|
-
} catch {
|
|
672
|
+
process.kill(pid, "SIGTERM");
|
|
673
|
+
} catch (err) {
|
|
674
|
+
const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
|
|
675
|
+
if (code === "EPERM") return { attempted: true, pid, outcome: "denied", reason: "eperm" };
|
|
676
|
+
return { attempted: true, pid, outcome: "error", reason: code ?? "signal_failed" };
|
|
677
|
+
}
|
|
678
|
+
const deadline = Date.now() + LEGACY_STOP_WAIT_MS;
|
|
679
|
+
while (Date.now() < deadline) {
|
|
680
|
+
if (!isProcessAlive(pid)) return { attempted: true, pid, outcome: "stopped" };
|
|
681
|
+
await delay(LEGACY_STOP_POLL_MS);
|
|
435
682
|
}
|
|
683
|
+
return { attempted: true, pid, outcome: "timed_out" };
|
|
436
684
|
}
|
|
437
|
-
async function
|
|
438
|
-
|
|
685
|
+
async function runAdoptLegacy(inputs) {
|
|
686
|
+
const slockHome = resolveSlockHome();
|
|
687
|
+
const sessionFile = userSessionPath(slockHome);
|
|
688
|
+
let session;
|
|
439
689
|
try {
|
|
440
|
-
await
|
|
441
|
-
return true;
|
|
690
|
+
session = JSON.parse(await readFile3(sessionFile, "utf8"));
|
|
442
691
|
} catch {
|
|
443
|
-
|
|
692
|
+
fail(
|
|
693
|
+
"NO_USER_SESSION",
|
|
694
|
+
`No user session at ${sessionFile}. Run \`slock-computer login\` first.`
|
|
695
|
+
);
|
|
444
696
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
697
|
+
if (session.kind !== "user-session" || typeof session.accessToken !== "string" || !session.accessToken) {
|
|
698
|
+
fail(
|
|
699
|
+
"INVALID_USER_SESSION",
|
|
700
|
+
`User session at ${sessionFile} is invalid. Re-run \`slock-computer login\`.`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
const baseUrl = (inputs.serverUrl ?? session.serverUrl ?? process.env.SLOCK_SERVER_URL ?? "").trim();
|
|
704
|
+
if (!baseUrl) {
|
|
705
|
+
fail("MISSING_SERVER_URL", "Set SLOCK_SERVER_URL or pass --server-url <url>.");
|
|
706
|
+
}
|
|
707
|
+
const slugForServer = normalizeServerSlug(inputs.serverSlug);
|
|
708
|
+
if (!slugForServer) {
|
|
709
|
+
fail("ADOPT_NOT_AUTHORIZED", "Server slug must not be empty.");
|
|
710
|
+
}
|
|
711
|
+
const legacy = await resolveLegacyKey(inputs, process.env);
|
|
712
|
+
info(
|
|
713
|
+
`Adopting legacy daemon for ${formatServerSlugDisplay(slugForServer)} via ${legacy.mode}\u2026`
|
|
714
|
+
);
|
|
715
|
+
const client = new ComputerAttachClient(baseUrl, session.accessToken);
|
|
716
|
+
const adoptStartedAt = /* @__PURE__ */ new Date();
|
|
717
|
+
const legacyOwnerFile = legacyLockOwnerPath(slockHome, legacy.rawKey);
|
|
718
|
+
const result = await client.adoptLegacy(legacy.rawKey, inputs.name);
|
|
719
|
+
legacy.rawKey = void 0;
|
|
720
|
+
if (result.status === "disabled") {
|
|
721
|
+
await appendAdoptionLog(slockHome, {
|
|
722
|
+
mode: legacy.mode,
|
|
723
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
724
|
+
startedAt: adoptStartedAt,
|
|
725
|
+
outcome: "failed",
|
|
726
|
+
failureReason: "computer_adopt_disabled"
|
|
727
|
+
});
|
|
728
|
+
fail(
|
|
729
|
+
"ADOPT_DISABLED",
|
|
730
|
+
"Computer legacy adoption is not enabled on this server. Upgrade the server or set SLOCK_DEVICE_LOGIN_ENABLED."
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
if (result.status === "legacy_key_invalid") {
|
|
734
|
+
await appendAdoptionLog(slockHome, {
|
|
735
|
+
mode: legacy.mode,
|
|
736
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
737
|
+
startedAt: adoptStartedAt,
|
|
738
|
+
outcome: "failed",
|
|
739
|
+
failureReason: "legacy_key_invalid"
|
|
740
|
+
});
|
|
741
|
+
fail(
|
|
742
|
+
"LEGACY_KEY_INVALID",
|
|
743
|
+
"Server rejected the legacy api key (unknown / wrong server / malformed). Verify the key and --server-url."
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (result.status === "legacy_machine_key_migrated") {
|
|
747
|
+
await appendAdoptionLog(slockHome, {
|
|
748
|
+
mode: legacy.mode,
|
|
749
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
750
|
+
startedAt: adoptStartedAt,
|
|
751
|
+
outcome: "failed",
|
|
752
|
+
failureReason: "legacy_machine_key_migrated"
|
|
753
|
+
});
|
|
754
|
+
fail(
|
|
755
|
+
"LEGACY_MACHINE_KEY_MIGRATED",
|
|
756
|
+
"This machine has already been adopted; the legacy key is no longer accepted. Use `slock-computer attach` to add another Computer attachment."
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (result.status === "not_authorized") {
|
|
760
|
+
await appendAdoptionLog(slockHome, {
|
|
761
|
+
mode: legacy.mode,
|
|
762
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
763
|
+
startedAt: adoptStartedAt,
|
|
764
|
+
outcome: "failed",
|
|
765
|
+
failureReason: "not_authorized"
|
|
766
|
+
});
|
|
767
|
+
fail(
|
|
768
|
+
"ADOPT_NOT_AUTHORIZED",
|
|
769
|
+
"Not authorized to adopt this machine on this server. Check that you are a current member."
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
if (result.status === "error") {
|
|
773
|
+
await appendAdoptionLog(slockHome, {
|
|
774
|
+
mode: legacy.mode,
|
|
775
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
776
|
+
startedAt: adoptStartedAt,
|
|
777
|
+
outcome: "failed",
|
|
778
|
+
failureReason: result.code
|
|
779
|
+
});
|
|
780
|
+
fail("ADOPT_FAILED", `Adoption failed (${result.code}).`);
|
|
781
|
+
}
|
|
782
|
+
info(result.resumed ? "Adopted (resumed prior attachment); running preflight\u2026" : "Adopted; running preflight\u2026");
|
|
783
|
+
const pre = await client.preflight(result.apiKey);
|
|
784
|
+
if (!pre.ok) {
|
|
785
|
+
await appendAdoptionLog(slockHome, {
|
|
786
|
+
mode: legacy.mode,
|
|
787
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
788
|
+
startedAt: adoptStartedAt,
|
|
789
|
+
outcome: "failed",
|
|
790
|
+
failureReason: `preflight_${pre.code}`
|
|
791
|
+
});
|
|
792
|
+
fail(
|
|
793
|
+
"PREFLIGHT_FAILED",
|
|
794
|
+
`Server preflight failed (${pre.code}); local state not written. Upgrade the server or retry.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const stop = await stopLegacyDaemonByOwnerFile(legacyOwnerFile);
|
|
798
|
+
if (stop.outcome === "timed_out" || stop.outcome === "denied" || stop.outcome === "error") {
|
|
799
|
+
await appendAdoptionLog(slockHome, {
|
|
800
|
+
mode: legacy.mode,
|
|
801
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
802
|
+
startedAt: adoptStartedAt,
|
|
803
|
+
outcome: "failed",
|
|
804
|
+
failureReason: `legacy_stop_${stop.outcome}`,
|
|
805
|
+
computerId: result.computerId,
|
|
806
|
+
machineId: result.machineId,
|
|
807
|
+
serverId: result.serverId,
|
|
808
|
+
legacyStop: stop
|
|
809
|
+
});
|
|
810
|
+
const detail = stop.outcome === "timed_out" ? `pid ${stop.pid} did not exit within ${LEGACY_STOP_WAIT_MS}ms` : stop.outcome === "denied" ? `pid ${stop.pid} cannot be stopped (permission denied)` : `stop attempt error (${stop.reason ?? "unknown"})`;
|
|
811
|
+
fail(
|
|
812
|
+
"LEGACY_DAEMON_STOP_FAILED",
|
|
813
|
+
`Adoption succeeded server-side but the legacy daemon could not be stopped: ${detail}. Stop it manually and re-run \`slock-computer adopt-legacy\`, or run \`slock-computer attach\` after confirming the legacy process is gone. No local Computer state was written.`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
const file = serverAttachmentPath(slockHome, result.serverId);
|
|
817
|
+
await mkdir4(dirname4(file), { recursive: true });
|
|
818
|
+
await writeFile4(
|
|
819
|
+
file,
|
|
820
|
+
JSON.stringify(
|
|
821
|
+
{
|
|
822
|
+
kind: "computer-attachment",
|
|
823
|
+
serverId: result.serverId,
|
|
824
|
+
serverSlug: slugForServer,
|
|
825
|
+
serverMachineId: result.computerId,
|
|
826
|
+
apiKey: result.apiKey,
|
|
827
|
+
serverUrl: baseUrl,
|
|
828
|
+
attachedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
829
|
+
adoptedFromLegacy: true,
|
|
830
|
+
legacyMachineId: result.machineId
|
|
831
|
+
},
|
|
832
|
+
null,
|
|
833
|
+
2
|
|
834
|
+
),
|
|
835
|
+
{ mode: 384 }
|
|
836
|
+
);
|
|
837
|
+
await chmod3(file, 384);
|
|
838
|
+
await appendAdoptionLog(slockHome, {
|
|
839
|
+
mode: legacy.mode,
|
|
840
|
+
redactedPrefix: legacy.redactedPrefix,
|
|
841
|
+
startedAt: adoptStartedAt,
|
|
842
|
+
outcome: "succeeded",
|
|
843
|
+
computerId: result.computerId,
|
|
844
|
+
machineId: result.machineId,
|
|
845
|
+
serverId: result.serverId,
|
|
846
|
+
legacyStop: stop
|
|
847
|
+
});
|
|
848
|
+
info(`Adopted. Computer state written to ${file}`);
|
|
849
|
+
info(` server: ${formatServerSlugDisplay(slugForServer)}`);
|
|
850
|
+
info(` serverMachine: ${result.computerId}`);
|
|
851
|
+
info(` legacyMachine: ${result.machineId}`);
|
|
852
|
+
switch (stop.outcome) {
|
|
853
|
+
case "absent":
|
|
854
|
+
info(" legacy daemon: not detected on this Computer (no local lock file)");
|
|
855
|
+
break;
|
|
856
|
+
case "already_dead":
|
|
857
|
+
info(` legacy daemon: already stopped (pid ${stop.pid} not running)`);
|
|
858
|
+
break;
|
|
859
|
+
case "stopped":
|
|
860
|
+
info(` legacy daemon: stopped (pid ${stop.pid}, SIGTERM)`);
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
info(`Next: run \`slock-computer start\` to bring this server online under the Computer supervisor.`);
|
|
864
|
+
}
|
|
865
|
+
async function appendAdoptionLog(slockHome, line) {
|
|
866
|
+
try {
|
|
867
|
+
await mkdir4(computerDir(slockHome), { recursive: true });
|
|
868
|
+
const fields = [
|
|
869
|
+
`ts=${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
870
|
+
`started_at=${line.startedAt.toISOString()}`,
|
|
871
|
+
`outcome=${line.outcome}`,
|
|
872
|
+
`credential_bridge_mode=${line.mode}`,
|
|
873
|
+
`legacy_key_prefix=${line.redactedPrefix}`
|
|
874
|
+
];
|
|
875
|
+
if (line.failureReason) fields.push(`failure_reason=${line.failureReason}`);
|
|
876
|
+
if (line.computerId) fields.push(`computer_id=${line.computerId}`);
|
|
877
|
+
if (line.machineId) fields.push(`legacy_machine_id=${line.machineId}`);
|
|
878
|
+
if (line.serverId) fields.push(`server_id=${line.serverId}`);
|
|
879
|
+
if (line.legacyStop) {
|
|
880
|
+
fields.push(`legacy_stop_outcome=${line.legacyStop.outcome}`);
|
|
881
|
+
if (typeof line.legacyStop.pid === "number") {
|
|
882
|
+
fields.push(`legacy_stop_pid=${line.legacyStop.pid}`);
|
|
883
|
+
}
|
|
884
|
+
if (line.legacyStop.reason) {
|
|
885
|
+
fields.push(`legacy_stop_reason=${line.legacyStop.reason}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const path2 = adoptionLogPath(slockHome);
|
|
889
|
+
await appendFile(path2, fields.join(" ") + "\n", { mode: 384 });
|
|
890
|
+
try {
|
|
891
|
+
const st = await stat(path2);
|
|
892
|
+
if ((st.mode & 63) !== 0) await chmod3(path2, 384);
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
} catch {
|
|
451
896
|
}
|
|
452
|
-
return out;
|
|
453
897
|
}
|
|
454
898
|
|
|
899
|
+
// src/setup.ts
|
|
900
|
+
import { readdir as readdir3, readFile as readFile6 } from "fs/promises";
|
|
901
|
+
import { join as join3 } from "path";
|
|
902
|
+
|
|
455
903
|
// src/supervisor.ts
|
|
456
904
|
import { spawn as spawn2 } from "child_process";
|
|
457
|
-
import { mkdir as
|
|
458
|
-
import { dirname as
|
|
905
|
+
import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile6, open, unlink as unlink4, rename as rename2 } from "fs/promises";
|
|
906
|
+
import { dirname as dirname7, join as joinPath } from "path";
|
|
459
907
|
import { fileURLToPath } from "url";
|
|
460
908
|
|
|
461
909
|
// src/cleanup.ts
|
|
462
|
-
import { readdir as readdir2, stat, unlink as unlink2, rm, rmdir, rename, mkdir as
|
|
910
|
+
import { readdir as readdir2, stat as stat2, unlink as unlink2, rm, rmdir, rename, mkdir as mkdir5 } from "fs/promises";
|
|
463
911
|
import { spawn } from "child_process";
|
|
464
|
-
import { join } from "path";
|
|
912
|
+
import { join as join2 } from "path";
|
|
465
913
|
function emptyCleanupReport() {
|
|
466
914
|
return {
|
|
467
915
|
stalePidfiles: [],
|
|
@@ -475,7 +923,7 @@ function emptyCleanupReport() {
|
|
|
475
923
|
async function cleanupStalePidfile(pidfilePath) {
|
|
476
924
|
const pid = await readPidfileAt(pidfilePath);
|
|
477
925
|
if (pid === null) return false;
|
|
478
|
-
if (
|
|
926
|
+
if (isProcessAlive2(pid)) return false;
|
|
479
927
|
try {
|
|
480
928
|
await unlink2(pidfilePath);
|
|
481
929
|
return true;
|
|
@@ -556,17 +1004,17 @@ async function cleanupOrphanProcesses(slockHome, psSpawn) {
|
|
|
556
1004
|
return signaled;
|
|
557
1005
|
}
|
|
558
1006
|
function quarantineDir(slockHome) {
|
|
559
|
-
return
|
|
1007
|
+
return join2(computerDir(slockHome), ".quarantine");
|
|
560
1008
|
}
|
|
561
1009
|
async function quarantineServerSubtree(slockHome, serverId) {
|
|
562
|
-
const src =
|
|
1010
|
+
const src = join2(serversDir(slockHome), serverId);
|
|
563
1011
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
564
|
-
const dest =
|
|
565
|
-
await
|
|
1012
|
+
const dest = join2(quarantineDir(slockHome), `${stamp}-${serverId}`);
|
|
1013
|
+
await mkdir5(dirname5(dest), { recursive: true });
|
|
566
1014
|
await rename(src, dest);
|
|
567
1015
|
return dest;
|
|
568
1016
|
}
|
|
569
|
-
function
|
|
1017
|
+
function dirname5(p) {
|
|
570
1018
|
const idx = p.lastIndexOf("/");
|
|
571
1019
|
return idx > 0 ? p.slice(0, idx) : "/";
|
|
572
1020
|
}
|
|
@@ -600,13 +1048,13 @@ var TMP_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
|
600
1048
|
async function cleanupTmpFiles(slockHome) {
|
|
601
1049
|
const removed = [];
|
|
602
1050
|
const cdir = computerDir(slockHome);
|
|
603
|
-
const stagingDir =
|
|
1051
|
+
const stagingDir = join2(cdir, "upgrade-staging");
|
|
604
1052
|
try {
|
|
605
1053
|
const versions = await readdir2(stagingDir);
|
|
606
1054
|
for (const v of versions) {
|
|
607
|
-
const vdir =
|
|
1055
|
+
const vdir = join2(stagingDir, v);
|
|
608
1056
|
try {
|
|
609
|
-
const s = await
|
|
1057
|
+
const s = await stat2(vdir);
|
|
610
1058
|
if (Date.now() - s.mtimeMs > TMP_MAX_AGE_MS) {
|
|
611
1059
|
await rm(vdir, { recursive: true, force: true });
|
|
612
1060
|
removed.push(vdir);
|
|
@@ -621,9 +1069,9 @@ async function cleanupTmpFiles(slockHome) {
|
|
|
621
1069
|
}
|
|
622
1070
|
} catch {
|
|
623
1071
|
}
|
|
624
|
-
const snap =
|
|
1072
|
+
const snap = join2(cdir, "upgrade-snapshot.json");
|
|
625
1073
|
try {
|
|
626
|
-
const s = await
|
|
1074
|
+
const s = await stat2(snap);
|
|
627
1075
|
if (Date.now() - s.mtimeMs > TMP_MAX_AGE_MS) {
|
|
628
1076
|
await unlink2(snap);
|
|
629
1077
|
removed.push(snap);
|
|
@@ -633,9 +1081,9 @@ async function cleanupTmpFiles(slockHome) {
|
|
|
633
1081
|
return removed;
|
|
634
1082
|
}
|
|
635
1083
|
async function cleanupStaleLock(slockHome) {
|
|
636
|
-
const lockDir =
|
|
1084
|
+
const lockDir = join2(computerDir(slockHome), ".lock");
|
|
637
1085
|
try {
|
|
638
|
-
const s = await
|
|
1086
|
+
const s = await stat2(lockDir);
|
|
639
1087
|
if (Date.now() - s.mtimeMs > 60 * 1e3) {
|
|
640
1088
|
await rm(lockDir, { recursive: true, force: true });
|
|
641
1089
|
return [lockDir];
|
|
@@ -645,9 +1093,9 @@ async function cleanupStaleLock(slockHome) {
|
|
|
645
1093
|
return [];
|
|
646
1094
|
}
|
|
647
1095
|
async function forceReleaseLock(slockHome) {
|
|
648
|
-
const lockDir =
|
|
1096
|
+
const lockDir = join2(computerDir(slockHome), ".lock");
|
|
649
1097
|
try {
|
|
650
|
-
await
|
|
1098
|
+
await stat2(lockDir);
|
|
651
1099
|
await rm(lockDir, { recursive: true, force: true });
|
|
652
1100
|
return [lockDir];
|
|
653
1101
|
} catch {
|
|
@@ -668,14 +1116,14 @@ async function runFullCleanup(slockHome, options = {}) {
|
|
|
668
1116
|
}
|
|
669
1117
|
|
|
670
1118
|
// src/health.ts
|
|
671
|
-
import { readFile as
|
|
672
|
-
import { dirname as
|
|
1119
|
+
import { readFile as readFile4, writeFile as writeFile5, unlink as unlink3, mkdir as mkdir6 } from "fs/promises";
|
|
1120
|
+
import { dirname as dirname6 } from "path";
|
|
673
1121
|
var CRASH_WINDOW_MS = 6e4;
|
|
674
1122
|
var DEGRADED_THRESHOLD = 3;
|
|
675
1123
|
async function readHealthFile(slockHome, serverId) {
|
|
676
1124
|
if (!isValidServerId(serverId)) return { crashes: [] };
|
|
677
1125
|
try {
|
|
678
|
-
const raw = await
|
|
1126
|
+
const raw = await readFile4(serverHealthPath(slockHome, serverId), "utf8");
|
|
679
1127
|
const parsed = JSON.parse(raw);
|
|
680
1128
|
if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
|
|
681
1129
|
return parsed;
|
|
@@ -686,8 +1134,8 @@ async function readHealthFile(slockHome, serverId) {
|
|
|
686
1134
|
}
|
|
687
1135
|
async function writeHealthFile(slockHome, serverId, file) {
|
|
688
1136
|
const path2 = serverHealthPath(slockHome, serverId);
|
|
689
|
-
await
|
|
690
|
-
await
|
|
1137
|
+
await mkdir6(dirname6(path2), { recursive: true });
|
|
1138
|
+
await writeFile5(path2, JSON.stringify(file), { mode: 384 });
|
|
691
1139
|
}
|
|
692
1140
|
async function recordCrash(slockHome, serverId, exitCode, signal, nowMs = Date.now()) {
|
|
693
1141
|
if (!isValidServerId(serverId)) return;
|
|
@@ -713,9 +1161,26 @@ async function readCrashHistory(slockHome, serverId, nowMs = Date.now()) {
|
|
|
713
1161
|
});
|
|
714
1162
|
}
|
|
715
1163
|
async function isDegraded(slockHome, serverId, nowMs = Date.now()) {
|
|
716
|
-
const
|
|
1164
|
+
const file = await readHealthFile(slockHome, serverId);
|
|
1165
|
+
if (file.fatalConfig) return true;
|
|
1166
|
+
const cutoffMs = nowMs - CRASH_WINDOW_MS;
|
|
1167
|
+
const recent = file.crashes.filter((c) => {
|
|
1168
|
+
const t = new Date(c.at).getTime();
|
|
1169
|
+
return Number.isFinite(t) && t >= cutoffMs;
|
|
1170
|
+
});
|
|
717
1171
|
return recent.length >= DEGRADED_THRESHOLD;
|
|
718
1172
|
}
|
|
1173
|
+
async function markFatalConfig(slockHome, serverId, exitCode, signal, nowMs = Date.now()) {
|
|
1174
|
+
if (!isValidServerId(serverId)) return;
|
|
1175
|
+
const file = await readHealthFile(slockHome, serverId);
|
|
1176
|
+
file.fatalConfig = {
|
|
1177
|
+
at: new Date(nowMs).toISOString(),
|
|
1178
|
+
exitCode,
|
|
1179
|
+
signal,
|
|
1180
|
+
reason: "ex_config"
|
|
1181
|
+
};
|
|
1182
|
+
await writeHealthFile(slockHome, serverId, file);
|
|
1183
|
+
}
|
|
719
1184
|
async function resetHealth(slockHome, serverId) {
|
|
720
1185
|
if (!isValidServerId(serverId)) return;
|
|
721
1186
|
try {
|
|
@@ -727,14 +1192,14 @@ async function resetHealth(slockHome, serverId) {
|
|
|
727
1192
|
// src/supervisor.ts
|
|
728
1193
|
async function readPidfileAt(pidfilePath) {
|
|
729
1194
|
try {
|
|
730
|
-
const raw = (await
|
|
1195
|
+
const raw = (await readFile5(pidfilePath, "utf8")).trim();
|
|
731
1196
|
const pid = Number.parseInt(raw, 10);
|
|
732
1197
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
733
1198
|
} catch {
|
|
734
1199
|
return null;
|
|
735
1200
|
}
|
|
736
1201
|
}
|
|
737
|
-
function
|
|
1202
|
+
function isProcessAlive2(pid) {
|
|
738
1203
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
739
1204
|
try {
|
|
740
1205
|
process.kill(pid, 0);
|
|
@@ -744,8 +1209,8 @@ function isProcessAlive(pid) {
|
|
|
744
1209
|
}
|
|
745
1210
|
}
|
|
746
1211
|
async function writePidfileAt(pidfilePath, pid) {
|
|
747
|
-
await
|
|
748
|
-
await
|
|
1212
|
+
await mkdir7(dirname7(pidfilePath), { recursive: true });
|
|
1213
|
+
await writeFile6(pidfilePath, String(pid), { mode: 384 });
|
|
749
1214
|
}
|
|
750
1215
|
async function clearPidfileAt(pidfilePath) {
|
|
751
1216
|
try {
|
|
@@ -759,7 +1224,7 @@ function buildResidentSpawn(mode, serverId, selfEntry = process.argv[1] ?? "", e
|
|
|
759
1224
|
}
|
|
760
1225
|
var PARENT_LOCK_HELD_ENV_VAR = "SLOCK_COMPUTER_PARENT_MUTATION_LOCK_HELD";
|
|
761
1226
|
async function spawnDetachedSupervisor(slockHome) {
|
|
762
|
-
await
|
|
1227
|
+
await mkdir7(computerDir(slockHome), { recursive: true });
|
|
763
1228
|
const supLogFd = await open(supervisorLogPath(slockHome), "a");
|
|
764
1229
|
const { command, args } = buildResidentSpawn("__supervise", null);
|
|
765
1230
|
const child = spawn2(command, args, {
|
|
@@ -783,10 +1248,35 @@ async function spawnDetachedSupervisor(slockHome) {
|
|
|
783
1248
|
await writePidfileAt(supervisorPidPath(slockHome), pid);
|
|
784
1249
|
return pid;
|
|
785
1250
|
}
|
|
1251
|
+
var EX_CONFIG_EXIT_CODE = 78;
|
|
786
1252
|
var defaultCoreFactory = async (creds) => {
|
|
787
|
-
|
|
788
|
-
|
|
1253
|
+
let coreMod;
|
|
1254
|
+
try {
|
|
1255
|
+
coreMod = await import("@slock-ai/daemon/core");
|
|
1256
|
+
} catch (err) {
|
|
1257
|
+
const code = err?.code;
|
|
1258
|
+
const isModuleMissing = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || err instanceof Error && /Cannot find (module|package)/i.test(err.message);
|
|
1259
|
+
if (isModuleMissing) {
|
|
1260
|
+
process.stderr.write(
|
|
1261
|
+
`slock-computer: per-server daemon failed to load \`@slock-ai/daemon/core\`.
|
|
1262
|
+
Reason: ${err instanceof Error ? err.message : String(err)}
|
|
1263
|
+
This usually means a source-run workspace without a built daemon dist.
|
|
1264
|
+
Fix: run \`pnpm --filter @slock-ai/daemon build\` once, then \`slock-computer doctor <server> --reset-health && slock-computer start\`.
|
|
1265
|
+
(Packaged/npx installs ship the dist already \u2014 this only affects local source-run setups.)
|
|
1266
|
+
`
|
|
1267
|
+
);
|
|
1268
|
+
process.exit(EX_CONFIG_EXIT_CODE);
|
|
1269
|
+
}
|
|
1270
|
+
throw err;
|
|
1271
|
+
}
|
|
1272
|
+
return new coreMod.DaemonCore({ serverUrl: creds.serverUrl, apiKey: creds.apiKey, localTrace: true });
|
|
789
1273
|
};
|
|
1274
|
+
function classifyChildExit(code, signal) {
|
|
1275
|
+
if (code === EX_CONFIG_EXIT_CODE) return "config-error";
|
|
1276
|
+
if (signal === "SIGTERM" || signal === "SIGINT") return "graceful";
|
|
1277
|
+
if (code === 0) return "graceful";
|
|
1278
|
+
return "crash";
|
|
1279
|
+
}
|
|
790
1280
|
async function runResident(serverId, deps = {}) {
|
|
791
1281
|
assertValidServerId(serverId);
|
|
792
1282
|
const slockHome = resolveSlockHome();
|
|
@@ -852,10 +1342,10 @@ async function runSupervisorStartupRecovery(slockHome) {
|
|
|
852
1342
|
}
|
|
853
1343
|
async function resolveSupervisorIdentity() {
|
|
854
1344
|
const here = fileURLToPath(import.meta.url);
|
|
855
|
-
const installRoot =
|
|
1345
|
+
const installRoot = dirname7(dirname7(here));
|
|
856
1346
|
let version = null;
|
|
857
1347
|
try {
|
|
858
|
-
const raw = await
|
|
1348
|
+
const raw = await readFile5(joinPath(installRoot, "package.json"), "utf8");
|
|
859
1349
|
const parsed = JSON.parse(raw);
|
|
860
1350
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
861
1351
|
version = parsed.version;
|
|
@@ -875,7 +1365,7 @@ async function writeSupervisorVersionEvidence(slockHome) {
|
|
|
875
1365
|
};
|
|
876
1366
|
const dest = supervisorVersionPath(slockHome);
|
|
877
1367
|
const tmp = `${dest}.tmp`;
|
|
878
|
-
await
|
|
1368
|
+
await writeFile6(tmp, JSON.stringify(payload) + "\n", { mode: 384 });
|
|
879
1369
|
await rename2(tmp, dest);
|
|
880
1370
|
} catch (err) {
|
|
881
1371
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -887,7 +1377,7 @@ async function writeSupervisorVersionEvidence(slockHome) {
|
|
|
887
1377
|
}
|
|
888
1378
|
async function readSupervisorVersionEvidence(slockHome) {
|
|
889
1379
|
try {
|
|
890
|
-
const raw = await
|
|
1380
|
+
const raw = await readFile5(supervisorVersionPath(slockHome), "utf8");
|
|
891
1381
|
const parsed = JSON.parse(raw);
|
|
892
1382
|
if (typeof parsed.installRoot !== "string" || typeof parsed.pid !== "number" || typeof parsed.writtenAt !== "string" || parsed.version !== null && typeof parsed.version !== "string") {
|
|
893
1383
|
return null;
|
|
@@ -904,7 +1394,7 @@ async function readSupervisorVersionEvidence(slockHome) {
|
|
|
904
1394
|
}
|
|
905
1395
|
async function runSupervise() {
|
|
906
1396
|
const slockHome = resolveSlockHome();
|
|
907
|
-
await
|
|
1397
|
+
await mkdir7(computerDir(slockHome), { recursive: true });
|
|
908
1398
|
await runSupervisorStartupRecovery(slockHome);
|
|
909
1399
|
await writePidfileAt(supervisorPidPath(slockHome), process.pid);
|
|
910
1400
|
await writeSupervisorVersionEvidence(slockHome);
|
|
@@ -912,7 +1402,7 @@ async function runSupervise() {
|
|
|
912
1402
|
const { [PARENT_LOCK_HELD_ENV_VAR]: _parentLockMarker, ...childEnv } = process.env;
|
|
913
1403
|
const spawnChild = async (serverId) => {
|
|
914
1404
|
const logPath = serverDaemonLogPath(slockHome, serverId);
|
|
915
|
-
await
|
|
1405
|
+
await mkdir7(dirname7(logPath), { recursive: true });
|
|
916
1406
|
const logFd = await open(logPath, "a");
|
|
917
1407
|
const { command, args } = buildResidentSpawn("__run", serverId);
|
|
918
1408
|
const child = spawn2(command, args, {
|
|
@@ -930,6 +1420,25 @@ async function runSupervise() {
|
|
|
930
1420
|
children.delete(serverId);
|
|
931
1421
|
await clearPidfileAt(serverDaemonPidPath(slockHome, serverId));
|
|
932
1422
|
if (handle.stopping) return;
|
|
1423
|
+
const classification = classifyChildExit(code, signal);
|
|
1424
|
+
if (classification === "config-error") {
|
|
1425
|
+
try {
|
|
1426
|
+
await markFatalConfig(slockHome, serverId, code, signal);
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
process.stderr.write(
|
|
1430
|
+
`Supervisor: server ${serverId} child exited with EX_CONFIG (${EX_CONFIG_EXIT_CODE}); marked degraded, NOT auto-restarting. See ${serverDaemonLogPath(slockHome, serverId)} for the actionable error.
|
|
1431
|
+
`
|
|
1432
|
+
);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (classification === "graceful") {
|
|
1436
|
+
await new Promise((r) => setTimeout(r, CHILD_RESTART_BACKOFF_MS));
|
|
1437
|
+
if (await readServerAttachment(slockHome, serverId) && !shuttingDown) {
|
|
1438
|
+
await spawnChild(serverId);
|
|
1439
|
+
}
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
933
1442
|
try {
|
|
934
1443
|
await recordCrash(slockHome, serverId, code, signal);
|
|
935
1444
|
} catch {
|
|
@@ -1001,10 +1510,10 @@ async function runStart(opts = {}, _deps = {}) {
|
|
|
1001
1510
|
await setServerManaged(slockHome, id);
|
|
1002
1511
|
}
|
|
1003
1512
|
const existing = await readPidfileAt(supervisorPidPath(slockHome));
|
|
1004
|
-
if (existing &&
|
|
1513
|
+
if (existing && isProcessAlive2(existing)) {
|
|
1005
1514
|
info(`Supervisor already running (pid ${existing}).`);
|
|
1006
1515
|
info(
|
|
1007
|
-
opts.serverId ? `Marked server ${opts.serverId} as managed; its daemon will be ensured on next reconcile tick.` : `Marked all ${attached.length} attached server(s) as managed; their daemons will be ensured on next reconcile tick.`
|
|
1516
|
+
opts.serverId ? `Marked server ${opts.serverLabel ?? opts.serverId} as managed; its daemon will be ensured on next reconcile tick.` : `Marked all ${attached.length} attached server(s) as managed; their daemons will be ensured on next reconcile tick.`
|
|
1008
1517
|
);
|
|
1009
1518
|
return;
|
|
1010
1519
|
}
|
|
@@ -1029,12 +1538,12 @@ async function runStart(opts = {}, _deps = {}) {
|
|
|
1029
1538
|
info(`Per-server daemon logs: ~/.slock/computer/servers/<serverId>/daemon.log`);
|
|
1030
1539
|
info(`Check state with \`slock-computer status\`.`);
|
|
1031
1540
|
}
|
|
1032
|
-
async function runDetach(serverId) {
|
|
1541
|
+
async function runDetach(serverId, serverLabel = serverId) {
|
|
1033
1542
|
assertValidServerId(serverId);
|
|
1034
1543
|
const slockHome = resolveSlockHome();
|
|
1035
1544
|
const a = await readServerAttachment(slockHome, serverId);
|
|
1036
1545
|
if (!a) {
|
|
1037
|
-
fail("NOT_ATTACHED", `Not attached to server ${
|
|
1546
|
+
fail("NOT_ATTACHED", `Not attached to server ${serverLabel} (nothing to detach).`);
|
|
1038
1547
|
}
|
|
1039
1548
|
await bestEffortServerRevoke(a.serverUrl, a.apiKey, serverId);
|
|
1040
1549
|
await clearServerManaged(slockHome, serverId);
|
|
@@ -1044,7 +1553,7 @@ async function runDetach(serverId) {
|
|
|
1044
1553
|
serverDaemonLogPath(slockHome, serverId)
|
|
1045
1554
|
];
|
|
1046
1555
|
for (const p of subtree) await clearPidfileAt(p);
|
|
1047
|
-
info(`Detached from server ${
|
|
1556
|
+
info(`Detached from server ${serverLabel}.`);
|
|
1048
1557
|
info(`The supervisor (if running) will stop that server's daemon on its next reconcile tick.`);
|
|
1049
1558
|
}
|
|
1050
1559
|
async function bestEffortServerRevoke(serverUrl, apiKey, serverId) {
|
|
@@ -1078,10 +1587,134 @@ async function bestEffortServerRevoke(serverUrl, apiKey, serverId) {
|
|
|
1078
1587
|
}
|
|
1079
1588
|
}
|
|
1080
1589
|
|
|
1590
|
+
// src/setup.ts
|
|
1591
|
+
async function hasValidUserSession(slockHome) {
|
|
1592
|
+
try {
|
|
1593
|
+
const parsed = JSON.parse(await readFile6(userSessionPath(slockHome), "utf8"));
|
|
1594
|
+
return parsed.kind === "user-session" && typeof parsed.accessToken === "string" && parsed.accessToken.length > 0;
|
|
1595
|
+
} catch {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
async function hasLiveLegacyDaemon(slockHome) {
|
|
1600
|
+
let machineDirs;
|
|
1601
|
+
try {
|
|
1602
|
+
machineDirs = await readdir3(join3(slockHome, "machines"));
|
|
1603
|
+
} catch {
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
for (const name of machineDirs) {
|
|
1607
|
+
if (!name.startsWith("machine-")) continue;
|
|
1608
|
+
try {
|
|
1609
|
+
const raw = await readFile6(join3(slockHome, "machines", name, "daemon.lock", "owner.json"), "utf8");
|
|
1610
|
+
const owner = JSON.parse(raw);
|
|
1611
|
+
if (typeof owner.pid === "number" && isProcessAlive2(owner.pid)) return true;
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
function hasExplicitLegacyKeyInput(opts) {
|
|
1618
|
+
return typeof opts.legacyApiKey === "string" && opts.legacyApiKey.length > 0 || typeof opts.legacyApiKeyFile === "string" && opts.legacyApiKeyFile.length > 0 || opts.legacyApiKeyStdin === true;
|
|
1619
|
+
}
|
|
1620
|
+
function hasAmbientLegacyKeyInput() {
|
|
1621
|
+
return typeof process.env.SLOCK_LEGACY_API_KEY === "string" && process.env.SLOCK_LEGACY_API_KEY.trim().length > 0;
|
|
1622
|
+
}
|
|
1623
|
+
async function runSetup(opts, deps = {}) {
|
|
1624
|
+
const slockHome = resolveSlockHome();
|
|
1625
|
+
const isTty = deps.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1626
|
+
const login = deps.runLogin ?? runLogin;
|
|
1627
|
+
const attach = deps.runAttach ?? runAttach;
|
|
1628
|
+
const adopt = deps.runAdoptLegacy ?? runAdoptLegacy;
|
|
1629
|
+
const start = deps.runStart ?? runStart;
|
|
1630
|
+
const legacyDaemonCheck = deps.hasLiveLegacyDaemon ?? hasLiveLegacyDaemon;
|
|
1631
|
+
if (!isTty && !opts.yes) {
|
|
1632
|
+
fail(
|
|
1633
|
+
"NON_INTERACTIVE_SETUP_REQUIRES_FLAGS",
|
|
1634
|
+
"Non-interactive setup requires --yes after you have confirmed the login/attach/adopt/start actions. Run `slock-computer login`, `slock-computer attach`, and `slock-computer start` separately for fully explicit automation."
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
if (!opts.adoptLegacy && hasExplicitLegacyKeyInput(opts)) {
|
|
1638
|
+
fail(
|
|
1639
|
+
"LEGACY_KEY_OUTSIDE_ADOPT",
|
|
1640
|
+
"`--legacy-api-key*` options are only accepted with `slock-computer setup --adopt-legacy` or `slock-computer adopt-legacy`."
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const label = formatServerSlugDisplay(opts.serverSlug);
|
|
1644
|
+
info(`Setting up Slock Computer for ${label}\u2026`);
|
|
1645
|
+
if (!await hasValidUserSession(slockHome)) {
|
|
1646
|
+
if (!isTty) {
|
|
1647
|
+
fail(
|
|
1648
|
+
"NON_INTERACTIVE_SETUP_REQUIRES_FLAGS",
|
|
1649
|
+
"No user session is available and setup is running non-interactively. Run `slock-computer login` in a terminal first, then re-run setup with --yes."
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
await login({ serverUrl: opts.serverUrl });
|
|
1653
|
+
} else {
|
|
1654
|
+
info("User session: already logged in.");
|
|
1655
|
+
}
|
|
1656
|
+
let attachment = await resolveAttachedServerSlug(slockHome, opts.serverSlug);
|
|
1657
|
+
if (attachment) {
|
|
1658
|
+
info(`Attachment: already attached to ${label}.`);
|
|
1659
|
+
} else {
|
|
1660
|
+
const liveLegacy = await legacyDaemonCheck(slockHome);
|
|
1661
|
+
const explicitLegacyKey = hasExplicitLegacyKeyInput(opts);
|
|
1662
|
+
const hasLegacyKey = explicitLegacyKey || opts.adoptLegacy === true && hasAmbientLegacyKeyInput();
|
|
1663
|
+
if (!opts.adoptLegacy && liveLegacy) {
|
|
1664
|
+
fail(
|
|
1665
|
+
"LEGACY_DAEMON_RUNNING",
|
|
1666
|
+
"A legacy daemon appears to be running in this SLOCK_HOME. Re-run with `--adopt-legacy` and a legacy key, or stop the legacy daemon before fresh setup."
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
if (opts.adoptLegacy && hasLegacyKey) {
|
|
1670
|
+
await adopt({
|
|
1671
|
+
serverSlug: opts.serverSlug,
|
|
1672
|
+
serverUrl: opts.serverUrl,
|
|
1673
|
+
name: opts.name,
|
|
1674
|
+
legacyApiKey: opts.legacyApiKey,
|
|
1675
|
+
legacyApiKeyFile: opts.legacyApiKeyFile,
|
|
1676
|
+
legacyApiKeyStdin: opts.legacyApiKeyStdin
|
|
1677
|
+
});
|
|
1678
|
+
} else if (opts.adoptLegacy && liveLegacy) {
|
|
1679
|
+
fail(
|
|
1680
|
+
"LEGACY_DAEMON_RUNNING",
|
|
1681
|
+
"A legacy daemon is running, but no legacy key was provided. Provide --legacy-api-key-file/--legacy-api-key/--legacy-api-key-stdin, or stop the legacy daemon before falling back to a fresh attach."
|
|
1682
|
+
);
|
|
1683
|
+
} else {
|
|
1684
|
+
if (opts.adoptLegacy) {
|
|
1685
|
+
info("Legacy key: not provided; falling back to fresh device-login attach.");
|
|
1686
|
+
}
|
|
1687
|
+
await attach({
|
|
1688
|
+
serverSlug: opts.serverSlug,
|
|
1689
|
+
serverUrl: opts.serverUrl,
|
|
1690
|
+
name: opts.name,
|
|
1691
|
+
run: false
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
attachment = await resolveAttachedServerSlug(slockHome, opts.serverSlug);
|
|
1695
|
+
if (!attachment) {
|
|
1696
|
+
fail(
|
|
1697
|
+
"SETUP_ATTACHMENT_MISSING",
|
|
1698
|
+
`Setup did not produce a local attachment for ${label}. Re-run \`slock-computer attach ${label}\` or \`slock-computer adopt-legacy ${label}\`.`
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (opts.start === false) {
|
|
1703
|
+
info(`Start: skipped (--no-start). Run \`slock-computer start ${label}\` when ready.`);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
await start({
|
|
1707
|
+
serverId: attachment.serverId,
|
|
1708
|
+
serverLabel: opts.serverSlug,
|
|
1709
|
+
foreground: opts.foreground
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1081
1713
|
// src/status.ts
|
|
1714
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1082
1715
|
async function readJsonSafe(path2) {
|
|
1083
1716
|
try {
|
|
1084
|
-
return JSON.parse(await
|
|
1717
|
+
return JSON.parse(await readFile7(path2, "utf8"));
|
|
1085
1718
|
} catch {
|
|
1086
1719
|
return null;
|
|
1087
1720
|
}
|
|
@@ -1091,7 +1724,7 @@ function str(v) {
|
|
|
1091
1724
|
}
|
|
1092
1725
|
async function pidStatus(pidfile) {
|
|
1093
1726
|
const pid = await readPidfileAt(pidfile);
|
|
1094
|
-
return pid !== null &&
|
|
1727
|
+
return pid !== null && isProcessAlive2(pid) ? { running: true, pid } : { running: false };
|
|
1095
1728
|
}
|
|
1096
1729
|
async function deriveHealth(slockHome, serverId, daemon) {
|
|
1097
1730
|
if (!daemon.running) return "offline";
|
|
@@ -1102,15 +1735,20 @@ async function buildStatusReport() {
|
|
|
1102
1735
|
const slockHome = resolveSlockHome();
|
|
1103
1736
|
const session = await readJsonSafe(userSessionPath(slockHome));
|
|
1104
1737
|
const attachments = await listServerAttachments(slockHome);
|
|
1105
|
-
const supervisor =
|
|
1738
|
+
const supervisor = {
|
|
1739
|
+
...await pidStatus(supervisorPidPath(slockHome)),
|
|
1740
|
+
logPath: supervisorLogPath(slockHome)
|
|
1741
|
+
};
|
|
1106
1742
|
const servers = [];
|
|
1107
1743
|
for (const a of attachments) {
|
|
1108
1744
|
const daemon = await pidStatus(serverDaemonPidPath(slockHome, a.serverId));
|
|
1109
1745
|
servers.push({
|
|
1110
1746
|
serverId: a.serverId,
|
|
1747
|
+
serverSlug: a.serverSlug ?? null,
|
|
1111
1748
|
serverMachineId: a.serverMachineId,
|
|
1112
1749
|
serverUrl: a.serverUrl,
|
|
1113
1750
|
attachedAt: a.attachedAt ?? null,
|
|
1751
|
+
daemonLogPath: serverDaemonLogPath(slockHome, a.serverId),
|
|
1114
1752
|
daemon,
|
|
1115
1753
|
health: await deriveHealth(slockHome, a.serverId, daemon)
|
|
1116
1754
|
});
|
|
@@ -1143,17 +1781,19 @@ async function runStatus(opts) {
|
|
|
1143
1781
|
info(
|
|
1144
1782
|
`Supervisor: ${report.supervisor.running ? `running (pid ${report.supervisor.pid})` : "stopped \u2014 run `slock-computer start`"}`
|
|
1145
1783
|
);
|
|
1784
|
+
info(`Supervisor log: ${report.supervisor.logPath}`);
|
|
1146
1785
|
info("");
|
|
1147
1786
|
if (report.servers.length === 0) {
|
|
1148
|
-
info("Attachments: none \u2014 run `slock-computer attach
|
|
1787
|
+
info("Attachments: none \u2014 run `slock-computer attach /<serverSlug>` (e.g. `/myserver`).");
|
|
1149
1788
|
} else {
|
|
1150
1789
|
info("Attachments:");
|
|
1151
|
-
info(` ${pad("SERVER",
|
|
1790
|
+
info(` ${pad("SERVER", 24)}${pad("HEALTH", 12)}${pad("DAEMON", 24)}${pad("MACHINE", 38)}URL`);
|
|
1152
1791
|
for (const s of report.servers) {
|
|
1153
1792
|
const dcol = s.daemon.running ? `running (pid ${s.daemon.pid})` : "stopped";
|
|
1154
1793
|
info(
|
|
1155
|
-
` ${pad(s.
|
|
1794
|
+
` ${pad(formatServerSlugDisplay(s.serverSlug), 24)}${pad(s.health, 12)}${pad(dcol, 24)}${pad(s.serverMachineId, 38)}${s.serverUrl}`
|
|
1156
1795
|
);
|
|
1796
|
+
info(` Daemon log: ${s.daemonLogPath}`);
|
|
1157
1797
|
}
|
|
1158
1798
|
if (report.servers.some((s) => s.health === "degraded")) {
|
|
1159
1799
|
info("");
|
|
@@ -1161,7 +1801,7 @@ async function runStatus(opts) {
|
|
|
1161
1801
|
" Note: one or more servers are `degraded` (repeated crashes; auto-restart paused)."
|
|
1162
1802
|
);
|
|
1163
1803
|
info(
|
|
1164
|
-
" Run `slock-computer doctor
|
|
1804
|
+
" Run `slock-computer doctor /<serverSlug> --reset-health` (e.g. `/myserver`) after fixing the underlying issue."
|
|
1165
1805
|
);
|
|
1166
1806
|
}
|
|
1167
1807
|
}
|
|
@@ -1169,36 +1809,60 @@ async function runStatus(opts) {
|
|
|
1169
1809
|
}
|
|
1170
1810
|
|
|
1171
1811
|
// src/targetServer.ts
|
|
1812
|
+
function attachmentLabel(a) {
|
|
1813
|
+
return formatServerSlugDisplay(a.serverSlug);
|
|
1814
|
+
}
|
|
1815
|
+
async function refreshAttachmentSlug(home, attachment) {
|
|
1816
|
+
try {
|
|
1817
|
+
const client = new ComputerAttachClient(attachment.serverUrl, "");
|
|
1818
|
+
const result = await client.preflight(attachment.apiKey);
|
|
1819
|
+
if (!result.ok || !result.serverSlug || result.serverSlug === attachment.serverSlug) {
|
|
1820
|
+
return attachment;
|
|
1821
|
+
}
|
|
1822
|
+
const updated = { ...attachment, serverSlug: result.serverSlug };
|
|
1823
|
+
await writeServerAttachment(home, updated);
|
|
1824
|
+
return updated;
|
|
1825
|
+
} catch {
|
|
1826
|
+
return attachment;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
async function listAttachmentsWithFreshSlugs(home) {
|
|
1830
|
+
const attachments = await listServerAttachments(home);
|
|
1831
|
+
return await Promise.all(attachments.map((a) => refreshAttachmentSlug(home, a)));
|
|
1832
|
+
}
|
|
1172
1833
|
async function resolveTargetServerId(opts) {
|
|
1173
1834
|
const home = resolveSlockHome();
|
|
1174
|
-
|
|
1175
|
-
if (
|
|
1176
|
-
fail("NO_ATTACHMENT", "No server attachments yet. Run `slock-computer attach
|
|
1835
|
+
let attachments = await listServerAttachments(home);
|
|
1836
|
+
if (attachments.length === 0) {
|
|
1837
|
+
fail("NO_ATTACHMENT", "No server attachments yet. Run `slock-computer attach /<serverSlug>` (e.g. `/myserver`) first.");
|
|
1177
1838
|
}
|
|
1178
|
-
const requested = (opts.server ?? "")
|
|
1839
|
+
const requested = normalizeServerSlug(opts.server ?? "");
|
|
1179
1840
|
if (requested) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1841
|
+
let found = attachments.find((a) => a.serverSlug === requested);
|
|
1842
|
+
if (!found) {
|
|
1843
|
+
attachments = await listAttachmentsWithFreshSlugs(home);
|
|
1844
|
+
found = attachments.find((a) => a.serverSlug === requested);
|
|
1182
1845
|
}
|
|
1183
|
-
if (!
|
|
1846
|
+
if (!found) {
|
|
1184
1847
|
fail(
|
|
1185
1848
|
"NOT_ATTACHED",
|
|
1186
|
-
`
|
|
1849
|
+
`Server slug ${formatServerSlugDisplay(requested)} is not attached. Attached server slugs: ${attachments.map(attachmentLabel).join(", ")}.`
|
|
1187
1850
|
);
|
|
1188
1851
|
}
|
|
1189
|
-
return
|
|
1852
|
+
return found.serverId;
|
|
1190
1853
|
}
|
|
1191
|
-
if (
|
|
1854
|
+
if (attachments.length === 1) return attachments[0].serverId;
|
|
1192
1855
|
fail(
|
|
1193
1856
|
"AMBIGUOUS_SERVER",
|
|
1194
|
-
`Multiple servers attached (${
|
|
1857
|
+
`Multiple servers attached (${attachments.map(attachmentLabel).join(", ")}). Pass \`--server /<serverSlug>\` (e.g. \`--server ${attachmentLabel(attachments[0])}\`) to choose one.`
|
|
1195
1858
|
);
|
|
1196
1859
|
}
|
|
1197
1860
|
async function resolveTargetAttachment(opts) {
|
|
1198
1861
|
const serverId = await resolveTargetServerId(opts);
|
|
1199
1862
|
const a = await readServerAttachment(resolveSlockHome(), serverId);
|
|
1200
1863
|
if (!a) {
|
|
1201
|
-
|
|
1864
|
+
const label = opts.server ?? serverId;
|
|
1865
|
+
fail("INVALID_ATTACHMENT", `Attachment for ${label} is missing/invalid. Re-run \`slock-computer attach ${label}\`.`);
|
|
1202
1866
|
}
|
|
1203
1867
|
return a;
|
|
1204
1868
|
}
|
|
@@ -1208,22 +1872,23 @@ async function runRunnersList(opts) {
|
|
|
1208
1872
|
const a = await resolveTargetAttachment({ server: opts.server });
|
|
1209
1873
|
const client = new RunnersClient(a.serverUrl, a.apiKey);
|
|
1210
1874
|
const result = await client.list();
|
|
1875
|
+
const label = a.serverSlug ? formatServerSlugDisplay(a.serverSlug) : a.serverId;
|
|
1211
1876
|
if (result.status === "unauthorized") {
|
|
1212
1877
|
fail(
|
|
1213
1878
|
"RUNNERS_UNAUTHORIZED",
|
|
1214
|
-
`The Computer credential for ${
|
|
1879
|
+
`The Computer credential for ${label} was rejected. Re-run \`slock-computer attach ${label}\`.`
|
|
1215
1880
|
);
|
|
1216
1881
|
}
|
|
1217
1882
|
if (result.status === "error") {
|
|
1218
1883
|
fail(
|
|
1219
1884
|
"RUNNERS_LIST_FAILED",
|
|
1220
|
-
`Could not list runners on ${
|
|
1885
|
+
`Could not list runners on ${label} (${result.code}). Check --server-url / server version.`
|
|
1221
1886
|
);
|
|
1222
1887
|
}
|
|
1223
1888
|
if (opts.json) {
|
|
1224
1889
|
info(
|
|
1225
1890
|
JSON.stringify(
|
|
1226
|
-
{ server: a.serverId, whitelist: result.whitelist, runners: result.runners },
|
|
1891
|
+
{ server: label, serverId: a.serverId, whitelist: result.whitelist, runners: result.runners },
|
|
1227
1892
|
null,
|
|
1228
1893
|
2
|
|
1229
1894
|
)
|
|
@@ -1231,11 +1896,11 @@ async function runRunnersList(opts) {
|
|
|
1231
1896
|
return;
|
|
1232
1897
|
}
|
|
1233
1898
|
if (result.runners.length === 0) {
|
|
1234
|
-
info(`No runners on server ${
|
|
1899
|
+
info(`No runners on server ${label}.`);
|
|
1235
1900
|
return;
|
|
1236
1901
|
}
|
|
1237
1902
|
info("");
|
|
1238
|
-
info(`Server ${
|
|
1903
|
+
info(`Server ${label}:`);
|
|
1239
1904
|
info(" AGENT STATUS RUNTIME MODEL NAME");
|
|
1240
1905
|
for (const r of result.runners) {
|
|
1241
1906
|
info(
|
|
@@ -1246,27 +1911,28 @@ async function runRunnersList(opts) {
|
|
|
1246
1911
|
}
|
|
1247
1912
|
async function runRunnersStop(agentId, opts = {}) {
|
|
1248
1913
|
if (!agentId || agentId.trim().length === 0) {
|
|
1249
|
-
fail("AGENT_ID_REQUIRED", "Usage: slock-computer runners stop <agentId> [--server
|
|
1914
|
+
fail("AGENT_ID_REQUIRED", "Usage: slock-computer runners stop <agentId> [--server /<serverSlug>]");
|
|
1250
1915
|
}
|
|
1251
1916
|
const a = await resolveTargetAttachment({ server: opts.server });
|
|
1252
1917
|
const client = new RunnersClient(a.serverUrl, a.apiKey);
|
|
1253
1918
|
const result = await client.stop(agentId);
|
|
1919
|
+
const label = a.serverSlug ? formatServerSlugDisplay(a.serverSlug) : a.serverId;
|
|
1254
1920
|
if (result.status === "unauthorized") {
|
|
1255
1921
|
fail(
|
|
1256
1922
|
"RUNNERS_UNAUTHORIZED",
|
|
1257
|
-
`The Computer credential for ${
|
|
1923
|
+
`The Computer credential for ${label} was rejected. Re-run \`slock-computer attach ${label}\`.`
|
|
1258
1924
|
);
|
|
1259
1925
|
}
|
|
1260
1926
|
if (result.status === "not_found") {
|
|
1261
|
-
fail("RUNNER_NOT_FOUND", `No runner ${agentId} on server ${
|
|
1927
|
+
fail("RUNNER_NOT_FOUND", `No runner ${agentId} on server ${label}.`);
|
|
1262
1928
|
}
|
|
1263
1929
|
if (result.status === "error") {
|
|
1264
1930
|
fail(
|
|
1265
1931
|
"RUNNER_STOP_FAILED",
|
|
1266
|
-
`Could not stop runner on ${
|
|
1932
|
+
`Could not stop runner on ${label} (${result.code}). Check --server-url / server version.`
|
|
1267
1933
|
);
|
|
1268
1934
|
}
|
|
1269
|
-
info(`Stop signalled for runner ${agentId} on server ${
|
|
1935
|
+
info(`Stop signalled for runner ${agentId} on server ${label}.`);
|
|
1270
1936
|
}
|
|
1271
1937
|
|
|
1272
1938
|
// src/doctor.ts
|
|
@@ -1303,13 +1969,14 @@ async function runDoctorChecks() {
|
|
|
1303
1969
|
checks.push({
|
|
1304
1970
|
name: "attachments",
|
|
1305
1971
|
ok: false,
|
|
1306
|
-
detail: "no attachments \u2014 run `slock-computer attach
|
|
1972
|
+
detail: "no attachments \u2014 run `slock-computer attach /<serverSlug>` (e.g. `/myserver`)"
|
|
1307
1973
|
});
|
|
1308
1974
|
return checks;
|
|
1309
1975
|
}
|
|
1310
1976
|
for (const a of attachments) {
|
|
1977
|
+
const label = a.serverSlug ? formatServerSlugDisplay(a.serverSlug) : a.serverId;
|
|
1311
1978
|
checks.push({
|
|
1312
|
-
name: `attach ${
|
|
1979
|
+
name: `attach ${label}`,
|
|
1313
1980
|
ok: true,
|
|
1314
1981
|
detail: `machine ${a.serverMachineId} @ ${a.serverUrl}`
|
|
1315
1982
|
});
|
|
@@ -1317,15 +1984,15 @@ async function runDoctorChecks() {
|
|
|
1317
1984
|
const client = new ComputerAttachClient(a.serverUrl, "");
|
|
1318
1985
|
const r = await client.preflight(a.apiKey);
|
|
1319
1986
|
checks.push(
|
|
1320
|
-
r.ok ? { name: `preflight ${
|
|
1321
|
-
name: `preflight ${
|
|
1987
|
+
r.ok ? { name: `preflight ${label}`, ok: true, detail: "surface aligned (\xA79 ok)" } : {
|
|
1988
|
+
name: `preflight ${label}`,
|
|
1322
1989
|
ok: false,
|
|
1323
1990
|
detail: `preflight rejected (${r.code}) \u2014 upgrade the server or re-attach`
|
|
1324
1991
|
}
|
|
1325
1992
|
);
|
|
1326
1993
|
} catch (err) {
|
|
1327
1994
|
checks.push({
|
|
1328
|
-
name: `preflight ${
|
|
1995
|
+
name: `preflight ${label}`,
|
|
1329
1996
|
ok: false,
|
|
1330
1997
|
detail: `server unreachable (${err instanceof Error ? err.message : String(err)}) \u2014 check serverUrl / network`
|
|
1331
1998
|
});
|
|
@@ -1337,7 +2004,7 @@ async function runDoctor(opts) {
|
|
|
1337
2004
|
const slockHome = resolveSlockHome();
|
|
1338
2005
|
if (opts.resetHealth && opts.serverId) {
|
|
1339
2006
|
await resetHealth(slockHome, opts.serverId);
|
|
1340
|
-
info(`Reset health state for server ${opts.serverId}.`);
|
|
2007
|
+
info(`Reset health state for server ${opts.serverLabel ?? opts.serverId}.`);
|
|
1341
2008
|
info(`Supervisor will resume auto-restart on next daemon exit.`);
|
|
1342
2009
|
}
|
|
1343
2010
|
const checks = await runDoctorChecks();
|
|
@@ -1366,14 +2033,14 @@ async function runDoctor(opts) {
|
|
|
1366
2033
|
info(allOk ? "All checks passed." : "Some checks failed \u2014 see the actionable hints above.");
|
|
1367
2034
|
if (crashes.length > 0) {
|
|
1368
2035
|
info("");
|
|
1369
|
-
info(`Recent crashes for server ${opts.serverId} (within 60s window):`);
|
|
2036
|
+
info(`Recent crashes for server ${opts.serverLabel ?? opts.serverId} (within 60s window):`);
|
|
1370
2037
|
for (const c of crashes) {
|
|
1371
2038
|
const sig = c.signal ? ` signal=${c.signal}` : "";
|
|
1372
2039
|
const code = c.exitCode !== null ? ` exitCode=${c.exitCode}` : "";
|
|
1373
2040
|
info(` - ${c.at}${code}${sig}`);
|
|
1374
2041
|
}
|
|
1375
2042
|
info(` \u2192 Once you fix the underlying issue, run`);
|
|
1376
|
-
info(` \`slock-computer doctor ${opts.serverId} --reset-health\` to resume auto-restart.`);
|
|
2043
|
+
info(` \`slock-computer doctor ${opts.serverLabel ?? opts.serverId} --reset-health\` to resume auto-restart.`);
|
|
1377
2044
|
}
|
|
1378
2045
|
if (cleanupReport) {
|
|
1379
2046
|
info("");
|
|
@@ -1407,14 +2074,14 @@ async function runDoctor(opts) {
|
|
|
1407
2074
|
}
|
|
1408
2075
|
|
|
1409
2076
|
// src/logs.ts
|
|
1410
|
-
import { readFile as
|
|
2077
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1411
2078
|
var DEFAULT_LINES = 200;
|
|
1412
2079
|
async function runLogs(opts) {
|
|
1413
2080
|
const home = resolveSlockHome();
|
|
1414
2081
|
const file = opts.supervisor ? supervisorLogPath(home) : serverDaemonLogPath(home, await resolveTargetServerId({ server: opts.server }));
|
|
1415
2082
|
let content;
|
|
1416
2083
|
try {
|
|
1417
|
-
content = await
|
|
2084
|
+
content = await readFile8(file, "utf8");
|
|
1418
2085
|
} catch {
|
|
1419
2086
|
fail(
|
|
1420
2087
|
"NO_DAEMON_LOG",
|
|
@@ -1430,14 +2097,14 @@ async function runLogs(opts) {
|
|
|
1430
2097
|
|
|
1431
2098
|
// src/concurrency.ts
|
|
1432
2099
|
import lockfile from "proper-lockfile";
|
|
1433
|
-
import { mkdir as
|
|
1434
|
-
import { join as
|
|
2100
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
2101
|
+
import { join as join4 } from "path";
|
|
1435
2102
|
var STALE_LOCK_THRESHOLD_MS = 6e4;
|
|
1436
2103
|
async function withMutationLock(fn) {
|
|
1437
2104
|
const slockHome = resolveSlockHome();
|
|
1438
2105
|
const lockTarget = computerDir(slockHome);
|
|
1439
|
-
await
|
|
1440
|
-
const lockfilePath =
|
|
2106
|
+
await mkdir8(lockTarget, { recursive: true });
|
|
2107
|
+
const lockfilePath = join4(lockTarget, ".lock");
|
|
1441
2108
|
let release = null;
|
|
1442
2109
|
try {
|
|
1443
2110
|
release = await lockfile.lock(lockTarget, {
|
|
@@ -1474,8 +2141,8 @@ async function withMutationLock(fn) {
|
|
|
1474
2141
|
}
|
|
1475
2142
|
|
|
1476
2143
|
// src/channel.ts
|
|
1477
|
-
import { readFile as
|
|
1478
|
-
import { dirname as
|
|
2144
|
+
import { readFile as readFile9, writeFile as writeFile7, mkdir as mkdir9 } from "fs/promises";
|
|
2145
|
+
import { dirname as dirname8 } from "path";
|
|
1479
2146
|
var DEFAULT_CHANNEL = "latest";
|
|
1480
2147
|
var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
1481
2148
|
function parseChannel(raw) {
|
|
@@ -1490,7 +2157,7 @@ function parseChannel(raw) {
|
|
|
1490
2157
|
}
|
|
1491
2158
|
async function readChannel(slockHome) {
|
|
1492
2159
|
try {
|
|
1493
|
-
const raw = await
|
|
2160
|
+
const raw = await readFile9(channelPath(slockHome), "utf8");
|
|
1494
2161
|
const parsed = parseChannel(raw);
|
|
1495
2162
|
if (parsed !== null) return parsed;
|
|
1496
2163
|
} catch {
|
|
@@ -1499,8 +2166,8 @@ async function readChannel(slockHome) {
|
|
|
1499
2166
|
}
|
|
1500
2167
|
async function writeChannel(slockHome, channel2) {
|
|
1501
2168
|
const p = channelPath(slockHome);
|
|
1502
|
-
await
|
|
1503
|
-
await
|
|
2169
|
+
await mkdir9(dirname8(p), { recursive: true });
|
|
2170
|
+
await writeFile7(p, `${channel2}
|
|
1504
2171
|
`, { mode: 384 });
|
|
1505
2172
|
}
|
|
1506
2173
|
async function runChannelShow(slockHome) {
|
|
@@ -1522,21 +2189,21 @@ async function runChannelSet(slockHome, raw) {
|
|
|
1522
2189
|
}
|
|
1523
2190
|
|
|
1524
2191
|
// src/upgradeCli.ts
|
|
1525
|
-
import { readFile as
|
|
2192
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1526
2193
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1527
|
-
import { dirname as
|
|
2194
|
+
import { dirname as dirname9, join as join7 } from "path";
|
|
1528
2195
|
|
|
1529
2196
|
// src/upgrade.ts
|
|
1530
2197
|
import { spawn as spawn4 } from "child_process";
|
|
1531
|
-
import { mkdir as
|
|
1532
|
-
import { join as
|
|
1533
|
-
import { createHash } from "crypto";
|
|
2198
|
+
import { mkdir as mkdir10, readFile as readFile11, writeFile as writeFile8, rm as rm2, rename as rename3 } from "fs/promises";
|
|
2199
|
+
import { join as join6 } from "path";
|
|
2200
|
+
import { createHash as createHash3 } from "crypto";
|
|
1534
2201
|
|
|
1535
2202
|
// src/preflightDepDrift.ts
|
|
1536
|
-
import { readFile as
|
|
2203
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1537
2204
|
import { spawn as spawn3 } from "child_process";
|
|
1538
2205
|
import { createRequire } from "module";
|
|
1539
|
-
import { join as
|
|
2206
|
+
import { join as join5 } from "path";
|
|
1540
2207
|
import { pathToFileURL } from "url";
|
|
1541
2208
|
var DAEMON_PACKAGE_NAME = "@slock-ai/daemon";
|
|
1542
2209
|
async function preflightDepDriftCheck(currentBinaryDir, stagedTarballPath, deps = {}) {
|
|
@@ -1704,13 +2371,13 @@ async function defaultReadTarballPackageJson(tarballPath) {
|
|
|
1704
2371
|
return JSON.parse(raw);
|
|
1705
2372
|
}
|
|
1706
2373
|
async function defaultReadCurrentPackageJson(currentBinaryDir) {
|
|
1707
|
-
const raw = await
|
|
2374
|
+
const raw = await readFile10(join5(currentBinaryDir, "package.json"), "utf8");
|
|
1708
2375
|
return JSON.parse(raw);
|
|
1709
2376
|
}
|
|
1710
2377
|
async function defaultReadInstalledDaemonVersion(currentBinaryDir) {
|
|
1711
2378
|
let searchPaths;
|
|
1712
2379
|
try {
|
|
1713
|
-
const anchor = pathToFileURL(
|
|
2380
|
+
const anchor = pathToFileURL(join5(currentBinaryDir, "package.json"));
|
|
1714
2381
|
const req = createRequire(anchor);
|
|
1715
2382
|
searchPaths = req.resolve.paths(DAEMON_PACKAGE_NAME);
|
|
1716
2383
|
} catch {
|
|
@@ -1719,9 +2386,9 @@ async function defaultReadInstalledDaemonVersion(currentBinaryDir) {
|
|
|
1719
2386
|
if (!searchPaths || searchPaths.length === 0) return null;
|
|
1720
2387
|
const subPath = DAEMON_PACKAGE_NAME.split("/");
|
|
1721
2388
|
for (const base of searchPaths) {
|
|
1722
|
-
const candidate =
|
|
2389
|
+
const candidate = join5(base, ...subPath, "package.json");
|
|
1723
2390
|
try {
|
|
1724
|
-
const raw = await
|
|
2391
|
+
const raw = await readFile10(candidate, "utf8");
|
|
1725
2392
|
const parsed = JSON.parse(raw);
|
|
1726
2393
|
if (parsed.name !== DAEMON_PACKAGE_NAME) continue;
|
|
1727
2394
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
@@ -1800,13 +2467,13 @@ function satisfiesTilde(ver, base) {
|
|
|
1800
2467
|
|
|
1801
2468
|
// src/upgrade.ts
|
|
1802
2469
|
function upgradeStagingDir(slockHome, version) {
|
|
1803
|
-
return
|
|
2470
|
+
return join6(computerDir(slockHome), "upgrade-staging", version);
|
|
1804
2471
|
}
|
|
1805
2472
|
async function stagePhase(slockHome, version, deps = {}) {
|
|
1806
2473
|
const npmPack = deps.npmPack ?? defaultNpmPack;
|
|
1807
|
-
const fsReadFile = deps.fsReadFile ??
|
|
2474
|
+
const fsReadFile = deps.fsReadFile ?? readFile11;
|
|
1808
2475
|
const stagedPath = upgradeStagingDir(slockHome, version);
|
|
1809
|
-
await
|
|
2476
|
+
await mkdir10(stagedPath, { recursive: true });
|
|
1810
2477
|
const packageRef = `@slock-ai/computer@${version}`;
|
|
1811
2478
|
const result = await npmPack(stagedPath, packageRef);
|
|
1812
2479
|
if (result.exitCode !== 0) {
|
|
@@ -1825,7 +2492,7 @@ async function stagePhase(slockHome, version, deps = {}) {
|
|
|
1825
2492
|
err.code = "UPGRADE_DOWNLOAD_FAILED";
|
|
1826
2493
|
throw err;
|
|
1827
2494
|
}
|
|
1828
|
-
const tarballSha1 =
|
|
2495
|
+
const tarballSha1 = createHash3("sha1").update(tarballBytes).digest("hex");
|
|
1829
2496
|
return { stagedPath, version, tarballSha1 };
|
|
1830
2497
|
}
|
|
1831
2498
|
function defaultNpmPack(cwd, packageRef) {
|
|
@@ -1857,7 +2524,7 @@ function defaultNpmPack(cwd, packageRef) {
|
|
|
1857
2524
|
resolve({ tarballPath: "", exitCode: 1, stderr: "npm pack returned no filename" });
|
|
1858
2525
|
return;
|
|
1859
2526
|
}
|
|
1860
|
-
resolve({ tarballPath:
|
|
2527
|
+
resolve({ tarballPath: join6(cwd, filename), exitCode: 0, stderr: stderrBuf });
|
|
1861
2528
|
} catch (e) {
|
|
1862
2529
|
resolve({ tarballPath: "", exitCode: 1, stderr: `parse npm pack output: ${e}` });
|
|
1863
2530
|
}
|
|
@@ -1912,13 +2579,13 @@ async function cleanupStaged(slockHome, version) {
|
|
|
1912
2579
|
}
|
|
1913
2580
|
}
|
|
1914
2581
|
function upgradeSnapshotPath(slockHome) {
|
|
1915
|
-
return
|
|
2582
|
+
return join6(computerDir(slockHome), "upgrade-snapshot.json");
|
|
1916
2583
|
}
|
|
1917
2584
|
async function snapshotPhase(slockHome, snap) {
|
|
1918
2585
|
const path2 = upgradeSnapshotPath(slockHome);
|
|
1919
2586
|
const tmp = `${path2}.tmp`;
|
|
1920
|
-
await
|
|
1921
|
-
await
|
|
2587
|
+
await mkdir10(computerDir(slockHome), { recursive: true });
|
|
2588
|
+
await writeFile8(tmp, JSON.stringify(snap, null, 2), { mode: 384 });
|
|
1922
2589
|
await rename3(tmp, path2);
|
|
1923
2590
|
}
|
|
1924
2591
|
async function clearUpgradeSnapshot(slockHome) {
|
|
@@ -1930,7 +2597,7 @@ async function clearUpgradeSnapshot(slockHome) {
|
|
|
1930
2597
|
}
|
|
1931
2598
|
async function extractTarball(tarballPath, destDir, deps = {}) {
|
|
1932
2599
|
const tarSpawn = deps.tarSpawn ?? defaultTarSpawn;
|
|
1933
|
-
await
|
|
2600
|
+
await mkdir10(destDir, { recursive: true });
|
|
1934
2601
|
const result = await tarSpawn(tarballPath, destDir);
|
|
1935
2602
|
if (result.exitCode !== 0) {
|
|
1936
2603
|
const err = new Error(
|
|
@@ -1939,7 +2606,7 @@ async function extractTarball(tarballPath, destDir, deps = {}) {
|
|
|
1939
2606
|
err.code = "UPGRADE_EXTRACT_FAILED";
|
|
1940
2607
|
throw err;
|
|
1941
2608
|
}
|
|
1942
|
-
return { extractedPackageDir:
|
|
2609
|
+
return { extractedPackageDir: join6(destDir, "package") };
|
|
1943
2610
|
}
|
|
1944
2611
|
function defaultTarSpawn(tarballPath, destDir) {
|
|
1945
2612
|
return new Promise((resolve) => {
|
|
@@ -1995,7 +2662,7 @@ async function rollbackSwap(currentBinaryDir, deps = {}) {
|
|
|
1995
2662
|
}
|
|
1996
2663
|
async function rollbackRestart(slockHome, currentBinaryDir, deps = {}) {
|
|
1997
2664
|
const readSupervisorPid = deps.readSupervisorPid ?? (() => defaultReadSupervisorPid(slockHome));
|
|
1998
|
-
const
|
|
2665
|
+
const isProcessAlive3 = deps.isProcessAlive ?? defaultIsProcessAlive;
|
|
1999
2666
|
const killSupervisor = deps.killSupervisor ?? defaultKillSupervisor;
|
|
2000
2667
|
const forceKillSupervisor = deps.forceKillSupervisor ?? defaultForceKillSupervisor;
|
|
2001
2668
|
const waitForExit = deps.waitForExit ?? defaultWaitForExit;
|
|
@@ -2006,7 +2673,7 @@ async function rollbackRestart(slockHome, currentBinaryDir, deps = {}) {
|
|
|
2006
2673
|
let supervisorStopped = false;
|
|
2007
2674
|
try {
|
|
2008
2675
|
const pid = await readSupervisorPid();
|
|
2009
|
-
if (pid === null || !
|
|
2676
|
+
if (pid === null || !isProcessAlive3(pid)) {
|
|
2010
2677
|
supervisorStopped = true;
|
|
2011
2678
|
} else {
|
|
2012
2679
|
await killSupervisor(pid);
|
|
@@ -2149,7 +2816,7 @@ async function restartPhase(slockHome, deps = {}) {
|
|
|
2149
2816
|
}
|
|
2150
2817
|
async function defaultReadSupervisorPid(slockHome) {
|
|
2151
2818
|
try {
|
|
2152
|
-
const raw = (await
|
|
2819
|
+
const raw = (await readFile11(supervisorPidPath(slockHome), "utf8")).trim();
|
|
2153
2820
|
const pid = Number.parseInt(raw, 10);
|
|
2154
2821
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2155
2822
|
} catch {
|
|
@@ -2290,7 +2957,7 @@ async function runUpgrade(slockHome, opts) {
|
|
|
2290
2957
|
try {
|
|
2291
2958
|
const ex = await extractTarball(
|
|
2292
2959
|
stagedTarballPath,
|
|
2293
|
-
|
|
2960
|
+
join6(staged.stagedPath, "extracted"),
|
|
2294
2961
|
{ tarSpawn: deps.tarSpawn }
|
|
2295
2962
|
);
|
|
2296
2963
|
extractedPackageDir = ex.extractedPackageDir;
|
|
@@ -2398,7 +3065,7 @@ async function runUpgrade(slockHome, opts) {
|
|
|
2398
3065
|
async function rollingDaemonHealthCheck(slockHome, deps = {}) {
|
|
2399
3066
|
const list = deps.listManagedServerIds ?? listManagedServerIds;
|
|
2400
3067
|
const readDaemonPid = deps.readDaemonPid ?? defaultReadDaemonPid;
|
|
2401
|
-
const
|
|
3068
|
+
const isProcessAlive3 = deps.isProcessAlive ?? defaultIsProcessAlive;
|
|
2402
3069
|
const perDaemonTimeoutMs = deps.perDaemonTimeoutMs ?? 3e4;
|
|
2403
3070
|
const pollIntervalMs = deps.pollIntervalMs ?? 500;
|
|
2404
3071
|
const managed = await list(slockHome);
|
|
@@ -2411,7 +3078,7 @@ async function rollingDaemonHealthCheck(slockHome, deps = {}) {
|
|
|
2411
3078
|
const pid = await readDaemonPid(slockHome, serverId);
|
|
2412
3079
|
if (pid === null) {
|
|
2413
3080
|
lastReason = "pidfile_missing";
|
|
2414
|
-
} else if (!
|
|
3081
|
+
} else if (!isProcessAlive3(pid)) {
|
|
2415
3082
|
lastReason = "process_dead";
|
|
2416
3083
|
} else {
|
|
2417
3084
|
healthy = true;
|
|
@@ -2429,7 +3096,7 @@ async function rollingDaemonHealthCheck(slockHome, deps = {}) {
|
|
|
2429
3096
|
}
|
|
2430
3097
|
async function defaultReadDaemonPid(slockHome, serverId) {
|
|
2431
3098
|
try {
|
|
2432
|
-
const raw = (await
|
|
3099
|
+
const raw = (await readFile11(serverDaemonPidPath(slockHome, serverId), "utf8")).trim();
|
|
2433
3100
|
const pid = Number.parseInt(raw, 10);
|
|
2434
3101
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2435
3102
|
} catch {
|
|
@@ -2446,13 +3113,13 @@ function defaultIsProcessAlive(pid) {
|
|
|
2446
3113
|
}
|
|
2447
3114
|
}
|
|
2448
3115
|
async function locateStagedTarball(stagedPath) {
|
|
2449
|
-
const { readdir:
|
|
2450
|
-
const entries = await
|
|
3116
|
+
const { readdir: readdir5 } = await import("fs/promises");
|
|
3117
|
+
const entries = await readdir5(stagedPath);
|
|
2451
3118
|
const tgz = entries.find((e) => e.endsWith(".tgz"));
|
|
2452
3119
|
if (!tgz) {
|
|
2453
3120
|
throw new Error(`no .tgz tarball found in staged dir ${stagedPath}`);
|
|
2454
3121
|
}
|
|
2455
|
-
return
|
|
3122
|
+
return join6(stagedPath, tgz);
|
|
2456
3123
|
}
|
|
2457
3124
|
|
|
2458
3125
|
// src/upgradeCli.ts
|
|
@@ -2614,12 +3281,12 @@ async function defaultFetchDistTags() {
|
|
|
2614
3281
|
}
|
|
2615
3282
|
function defaultCurrentBinaryDir() {
|
|
2616
3283
|
const here = fileURLToPath2(import.meta.url);
|
|
2617
|
-
return
|
|
3284
|
+
return dirname9(dirname9(here));
|
|
2618
3285
|
}
|
|
2619
3286
|
async function defaultCurrentVersion() {
|
|
2620
|
-
const pkgPath =
|
|
3287
|
+
const pkgPath = join7(defaultCurrentBinaryDir(), "package.json");
|
|
2621
3288
|
try {
|
|
2622
|
-
const raw = await
|
|
3289
|
+
const raw = await readFile12(pkgPath, "utf8");
|
|
2623
3290
|
const parsed = JSON.parse(raw);
|
|
2624
3291
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
2625
3292
|
return parsed.version;
|
|
@@ -2630,9 +3297,9 @@ async function defaultCurrentVersion() {
|
|
|
2630
3297
|
}
|
|
2631
3298
|
|
|
2632
3299
|
// src/upgradeTestHarness.ts
|
|
2633
|
-
import { mkdir as
|
|
2634
|
-
import { join as
|
|
2635
|
-
import { createHash as
|
|
3300
|
+
import { mkdir as mkdir11, readdir as readdir4, stat as stat3, writeFile as writeFile9 } from "fs/promises";
|
|
3301
|
+
import { join as join8 } from "path";
|
|
3302
|
+
import { createHash as createHash4 } from "crypto";
|
|
2636
3303
|
var PHASES = /* @__PURE__ */ new Set([
|
|
2637
3304
|
"stage",
|
|
2638
3305
|
"verify",
|
|
@@ -2664,7 +3331,7 @@ function buildSimulatedDeps(slockHome, opts) {
|
|
|
2664
3331
|
const targetVersion = opts.targetVersion ?? "0.99.0";
|
|
2665
3332
|
const fromVersion = opts.fromVersion ?? "0.0.0-test";
|
|
2666
3333
|
const tarballBytes = Buffer.from(`harness-tarball-${targetVersion}`, "utf8");
|
|
2667
|
-
const expectedSha =
|
|
3334
|
+
const expectedSha = createHash4("sha1").update(tarballBytes).digest("hex");
|
|
2668
3335
|
const sleepMs = opts.simulateSleepWakeMs ?? 0;
|
|
2669
3336
|
const maybeSleep = async () => {
|
|
2670
3337
|
if (sleepMs > 0) {
|
|
@@ -2678,7 +3345,7 @@ function buildSimulatedDeps(slockHome, opts) {
|
|
|
2678
3345
|
targetVersion,
|
|
2679
3346
|
fromVersion,
|
|
2680
3347
|
channel: "latest",
|
|
2681
|
-
currentBinaryDir:
|
|
3348
|
+
currentBinaryDir: join8(slockHome, "harness-current"),
|
|
2682
3349
|
drainMode: opts.drain,
|
|
2683
3350
|
deps: {
|
|
2684
3351
|
npmPack: async (cwd) => {
|
|
@@ -2689,8 +3356,8 @@ function buildSimulatedDeps(slockHome, opts) {
|
|
|
2689
3356
|
return { tarballPath: "", exitCode: 1, stderr: "simulated stage failure" };
|
|
2690
3357
|
}
|
|
2691
3358
|
const filename = `slock-ai-computer-${targetVersion}.tgz`;
|
|
2692
|
-
await
|
|
2693
|
-
return { tarballPath:
|
|
3359
|
+
await writeFile9(join8(cwd, filename), tarballBytes);
|
|
3360
|
+
return { tarballPath: join8(cwd, filename), exitCode: 0, stderr: "" };
|
|
2694
3361
|
},
|
|
2695
3362
|
fetchAdvertisedHash: async () => {
|
|
2696
3363
|
if (opts.simulateNetworkLossAt === "verify") return null;
|
|
@@ -2701,8 +3368,8 @@ function buildSimulatedDeps(slockHome, opts) {
|
|
|
2701
3368
|
if (opts.simulateFail === "extract") {
|
|
2702
3369
|
return { exitCode: 1, stderr: "simulated extract failure" };
|
|
2703
3370
|
}
|
|
2704
|
-
await
|
|
2705
|
-
await
|
|
3371
|
+
await mkdir11(join8(destDir, "package"), { recursive: true });
|
|
3372
|
+
await writeFile9(join8(destDir, "package", "marker.txt"), `NEW@${targetVersion}`);
|
|
2706
3373
|
return { exitCode: 0, stderr: "" };
|
|
2707
3374
|
},
|
|
2708
3375
|
fsRename: opts.simulateFail === "swap" ? async () => {
|
|
@@ -2761,12 +3428,12 @@ function buildSimulatedDeps(slockHome, opts) {
|
|
|
2761
3428
|
};
|
|
2762
3429
|
}
|
|
2763
3430
|
async function arrangeSnapshotFailure(slockHome) {
|
|
2764
|
-
const snapshotPath =
|
|
2765
|
-
await
|
|
3431
|
+
const snapshotPath = join8(slockHome, "computer", "upgrade-snapshot.json");
|
|
3432
|
+
await mkdir11(snapshotPath, { recursive: true });
|
|
2766
3433
|
}
|
|
2767
3434
|
async function pathInfo(path2) {
|
|
2768
3435
|
try {
|
|
2769
|
-
const s = await
|
|
3436
|
+
const s = await stat3(path2);
|
|
2770
3437
|
if (s.isDirectory()) return { kind: "dir", size: 0 };
|
|
2771
3438
|
if (s.isFile()) return { kind: "file", size: s.size };
|
|
2772
3439
|
return null;
|
|
@@ -2780,14 +3447,14 @@ async function captureStateSnapshot(slockHome, version, currentBinaryDir) {
|
|
|
2780
3447
|
let stagingEntries = null;
|
|
2781
3448
|
if (stagingExists?.kind === "dir") {
|
|
2782
3449
|
try {
|
|
2783
|
-
stagingEntries = (await
|
|
3450
|
+
stagingEntries = (await readdir4(stagingPath)).sort();
|
|
2784
3451
|
} catch {
|
|
2785
3452
|
stagingEntries = null;
|
|
2786
3453
|
}
|
|
2787
3454
|
}
|
|
2788
3455
|
let servers = [];
|
|
2789
3456
|
try {
|
|
2790
|
-
const ids = await
|
|
3457
|
+
const ids = await readdir4(serversDir(slockHome));
|
|
2791
3458
|
servers = await Promise.all(
|
|
2792
3459
|
ids.map(async (serverId) => ({
|
|
2793
3460
|
serverId,
|
|
@@ -2802,7 +3469,7 @@ async function captureStateSnapshot(slockHome, version, currentBinaryDir) {
|
|
|
2802
3469
|
stagingDir: stagingExists,
|
|
2803
3470
|
stagingEntries,
|
|
2804
3471
|
supervisorPid: await pathInfo(supervisorPidPath(slockHome)),
|
|
2805
|
-
channelFile: await pathInfo(
|
|
3472
|
+
channelFile: await pathInfo(join8(computerDir(slockHome), "channel")),
|
|
2806
3473
|
computerDir: await pathInfo(computerDir(slockHome)),
|
|
2807
3474
|
servers,
|
|
2808
3475
|
prevBinary: await pathInfo(currentBinaryDir + ".prev"),
|
|
@@ -2817,12 +3484,12 @@ async function runUpgradeTestHarness(slockHome, opts, writer = (s) => process.st
|
|
|
2817
3484
|
process.exitCode = 1;
|
|
2818
3485
|
return;
|
|
2819
3486
|
}
|
|
2820
|
-
await
|
|
3487
|
+
await mkdir11(slockHome, { recursive: true });
|
|
2821
3488
|
if (opts.simulateFail === "snapshot") {
|
|
2822
3489
|
await arrangeSnapshotFailure(slockHome);
|
|
2823
3490
|
}
|
|
2824
3491
|
const { opts: upgradeOpts } = buildSimulatedDeps(slockHome, opts);
|
|
2825
|
-
await
|
|
3492
|
+
await mkdir11(upgradeOpts.currentBinaryDir, { recursive: true });
|
|
2826
3493
|
let outcome;
|
|
2827
3494
|
try {
|
|
2828
3495
|
outcome = await runUpgrade(slockHome, upgradeOpts);
|
|
@@ -2859,9 +3526,9 @@ async function runUpgradeTestHarness(slockHome, opts, writer = (s) => process.st
|
|
|
2859
3526
|
}
|
|
2860
3527
|
|
|
2861
3528
|
// src/upgradeInstallSmoke.ts
|
|
2862
|
-
import { copyFile, mkdir as
|
|
2863
|
-
import { createHash as
|
|
2864
|
-
import { dirname as
|
|
3529
|
+
import { copyFile, mkdir as mkdir12, readFile as readFile13 } from "fs/promises";
|
|
3530
|
+
import { createHash as createHash5 } from "crypto";
|
|
3531
|
+
import { dirname as dirname10, isAbsolute, join as join9, resolve as pathResolve } from "path";
|
|
2865
3532
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2866
3533
|
async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
|
|
2867
3534
|
if (typeof opts.packageTarball !== "string" || opts.packageTarball.trim().length === 0) {
|
|
@@ -2870,19 +3537,19 @@ async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
|
|
|
2870
3537
|
const tarballPath = isAbsolute(opts.packageTarball) ? opts.packageTarball : pathResolve(opts.packageTarball);
|
|
2871
3538
|
let tarballBytes;
|
|
2872
3539
|
try {
|
|
2873
|
-
tarballBytes = await
|
|
3540
|
+
tarballBytes = await readFile13(tarballPath);
|
|
2874
3541
|
} catch (e) {
|
|
2875
3542
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2876
3543
|
throw new Error(`__upgrade-install-smoke: cannot read --package-tarball ${tarballPath}: ${msg}`);
|
|
2877
3544
|
}
|
|
2878
|
-
const tarballSha1 =
|
|
3545
|
+
const tarballSha1 = createHash5("sha1").update(tarballBytes).digest("hex");
|
|
2879
3546
|
const currentBinaryDir = opts.currentBinaryDir ?? (deps.currentBinaryDir ?? defaultCurrentBinaryDirLocal)();
|
|
2880
3547
|
const fromVersion = opts.fromVersion ?? await (deps.currentVersion ?? defaultCurrentVersionLocal)();
|
|
2881
3548
|
const channel2 = opts.channel ?? "latest";
|
|
2882
3549
|
const spawnFreshSupervisor = deps.spawnFreshSupervisor ?? (async (h) => {
|
|
2883
3550
|
await spawnDetachedSupervisor(h);
|
|
2884
3551
|
});
|
|
2885
|
-
await
|
|
3552
|
+
await mkdir12(slockHome, { recursive: true });
|
|
2886
3553
|
const outcome = await runUpgrade(slockHome, {
|
|
2887
3554
|
targetVersion: opts.targetVersion,
|
|
2888
3555
|
fromVersion,
|
|
@@ -2895,7 +3562,7 @@ async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
|
|
|
2895
3562
|
// other dep falls back to its production default.
|
|
2896
3563
|
npmPack: async (cwd) => {
|
|
2897
3564
|
const filename = `slock-ai-computer-${opts.targetVersion}.tgz`;
|
|
2898
|
-
const stagedTarball =
|
|
3565
|
+
const stagedTarball = join9(cwd, filename);
|
|
2899
3566
|
await copyFile(tarballPath, stagedTarball);
|
|
2900
3567
|
return { tarballPath: stagedTarball, exitCode: 0, stderr: "" };
|
|
2901
3568
|
},
|
|
@@ -2935,12 +3602,12 @@ async function runUpgradeInstallSmokeCli(slockHome, opts, writer = (s) => proces
|
|
|
2935
3602
|
}
|
|
2936
3603
|
function defaultCurrentBinaryDirLocal() {
|
|
2937
3604
|
const here = fileURLToPath3(import.meta.url);
|
|
2938
|
-
return
|
|
3605
|
+
return dirname10(dirname10(here));
|
|
2939
3606
|
}
|
|
2940
3607
|
async function defaultCurrentVersionLocal() {
|
|
2941
|
-
const pkgPath =
|
|
3608
|
+
const pkgPath = join9(defaultCurrentBinaryDirLocal(), "package.json");
|
|
2942
3609
|
try {
|
|
2943
|
-
const raw = await
|
|
3610
|
+
const raw = await readFile13(pkgPath, "utf8");
|
|
2944
3611
|
const parsed = JSON.parse(raw);
|
|
2945
3612
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
2946
3613
|
return parsed.version;
|
|
@@ -2971,43 +3638,83 @@ program.name("slock-computer").description("Slock Computer \u2014 local-machine
|
|
|
2971
3638
|
program.command("login").description("Log in via device-code (one user identity per Computer / SLOCK_HOME).").option("--server-url <url>", "Slock API base URL; defaults to SLOCK_SERVER_URL").action(withCliExit(async (opts) => {
|
|
2972
3639
|
await runLogin({ serverUrl: opts.serverUrl });
|
|
2973
3640
|
}));
|
|
2974
|
-
program.command("attach").argument("<
|
|
3641
|
+
program.command("attach").argument("<serverSlug>", "target Slock server slug (canonical form `/myserver`; bare `myserver` accepted)").description("Attach this Computer to one Slock server (add-not-replace; multi-server OK).").option("--server-url <url>", "Slock API base URL; defaults to SLOCK_SERVER_URL").option("--name <name>", "Computer display name; defaults to a sanitized hostname").option("--no-run", "only authorize + write local state; do not start the supervisor").option("--foreground", "run the supervisor in this terminal instead of the background").action(withCliExit(async (serverSlug, opts) => {
|
|
2975
3642
|
await withMutationLock(
|
|
2976
|
-
() => runAttach({
|
|
3643
|
+
() => runAttach({ serverSlug, serverUrl: opts.serverUrl, name: opts.name, run: opts.run, foreground: opts.foreground })
|
|
2977
3644
|
);
|
|
2978
3645
|
}));
|
|
2979
|
-
program.command("
|
|
2980
|
-
|
|
3646
|
+
program.command("setup").argument("<serverSlug>", "target Slock server slug (canonical form `/myserver`; bare `myserver` accepted)").description("Set up this Computer for one server: login if needed, attach/adopt if needed, then start.").option("--server-url <url>", "Slock API base URL; defaults to SLOCK_SERVER_URL or the saved user session").option("--name <name>", "Computer display name for a new attachment/adoption; defaults to a sanitized hostname").option("--adopt-legacy", "attempt one-time migration from a legacy daemon before fresh attach fallback").option("--legacy-api-key <key>", "legacy key for --adopt-legacy (migration-only; prefer file/stdin)").option("--legacy-api-key-file <path>", "path to a file containing the legacy key for --adopt-legacy").option("--legacy-api-key-stdin", "read the legacy key from stdin for --adopt-legacy").option("--no-start", "stop after login + attach/adopt; do not start the supervisor").option("--foreground", "run the supervisor in this terminal instead of the background").option("-y, --yes", "allow non-interactive setup after confirming the planned actions").action(
|
|
3647
|
+
withCliExit(async (serverSlug, opts) => {
|
|
3648
|
+
await withMutationLock(
|
|
3649
|
+
() => runSetup({
|
|
3650
|
+
serverSlug,
|
|
3651
|
+
serverUrl: opts.serverUrl,
|
|
3652
|
+
name: opts.name,
|
|
3653
|
+
adoptLegacy: opts.adoptLegacy,
|
|
3654
|
+
legacyApiKey: opts.legacyApiKey,
|
|
3655
|
+
legacyApiKeyFile: opts.legacyApiKeyFile,
|
|
3656
|
+
legacyApiKeyStdin: opts.legacyApiKeyStdin,
|
|
3657
|
+
start: opts.start,
|
|
3658
|
+
foreground: opts.foreground,
|
|
3659
|
+
yes: opts.yes
|
|
3660
|
+
})
|
|
3661
|
+
);
|
|
3662
|
+
})
|
|
3663
|
+
);
|
|
3664
|
+
program.command("adopt-legacy").argument("<serverSlug>", "target Slock server slug (the legacy machine's server)").description(
|
|
3665
|
+
"One-time migration: trade a legacy sk_machine_* / sk_daemon_* key for a fresh Computer attachment (sk_computer_*)."
|
|
3666
|
+
).option("--server-url <url>", "Slock API base URL; defaults to SLOCK_SERVER_URL").option("--name <name>", "name to record on the new Computer attachment (default: existing machine name)").option("--legacy-api-key <key>", "raw legacy key on argv (insecure on shared shells; prefer --legacy-api-key-file or stdin)").option("--legacy-api-key-file <path>", "path to a 0600 file containing the legacy key (one line)").option("--legacy-api-key-stdin", "read the legacy key from stdin").action(
|
|
3667
|
+
withCliExit(async (serverSlug, opts) => {
|
|
3668
|
+
await withMutationLock(
|
|
3669
|
+
() => runAdoptLegacy({
|
|
3670
|
+
serverSlug,
|
|
3671
|
+
serverUrl: opts.serverUrl,
|
|
3672
|
+
name: opts.name,
|
|
3673
|
+
legacyApiKey: opts.legacyApiKey,
|
|
3674
|
+
legacyApiKeyFile: opts.legacyApiKeyFile,
|
|
3675
|
+
legacyApiKeyStdin: opts.legacyApiKeyStdin
|
|
3676
|
+
})
|
|
3677
|
+
);
|
|
3678
|
+
})
|
|
3679
|
+
);
|
|
3680
|
+
program.command("detach").argument("<serverSlug>", "server slug to detach from this Computer (canonical form `/myserver`)").description("Remove ONE server's local attachment; never touches user-session or other servers.").action(withCliExit(async (serverSlug) => {
|
|
3681
|
+
await withMutationLock(async () => runDetach(await resolveTargetServerId({ server: serverSlug }), serverSlug));
|
|
2981
3682
|
}));
|
|
2982
3683
|
program.command("status").description("Show this Computer's aggregate state (login + supervisor + per-server daemons).").option("--json", "emit the machine-readable report").action(withCliExit(async (opts) => {
|
|
2983
3684
|
await runStatus({ json: opts.json });
|
|
2984
3685
|
}));
|
|
2985
|
-
program.command("start").argument("[
|
|
3686
|
+
program.command("start").argument("[serverSlug]", "optional: verify this server is attached and ensure its daemon is reconciled (default: ensure all attached)").description("Start/ensure the Computer supervisor (manages all per-server daemons).").option("--foreground", "stay in this terminal instead of detaching").action(withCliExit(async (serverSlug, opts) => {
|
|
2986
3687
|
await withMutationLock(
|
|
2987
|
-
() => runStart({
|
|
3688
|
+
async () => runStart({
|
|
3689
|
+
foreground: opts.foreground,
|
|
3690
|
+
serverId: serverSlug ? await resolveTargetServerId({ server: serverSlug }) : null,
|
|
3691
|
+
serverLabel: serverSlug ?? null
|
|
3692
|
+
})
|
|
2988
3693
|
);
|
|
2989
3694
|
}));
|
|
2990
|
-
program.command("doctor").argument("[
|
|
3695
|
+
program.command("doctor").argument("[serverSlug]", "optional: scope detail (recent crashes) to one server").description("Diagnose login + per-server attachments + per-server preflight (no secrets).").option("--json", "emit the machine-readable report").option("--cleanup", "after diagnosis, run the local residue cleanup pass").option("--fix", "alias for --cleanup (same behavior)").option("--reset-health", "clear <serverSlug>'s crash history so supervisor resumes auto-restart").action(
|
|
2991
3696
|
withCliExit(
|
|
2992
|
-
async (
|
|
3697
|
+
async (serverSlug, opts) => {
|
|
3698
|
+
const serverId = serverSlug ? await resolveTargetServerId({ server: serverSlug }) : void 0;
|
|
2993
3699
|
await runDoctor({
|
|
2994
3700
|
json: opts.json,
|
|
2995
3701
|
cleanup: opts.cleanup,
|
|
2996
3702
|
fix: opts.fix,
|
|
2997
3703
|
serverId,
|
|
3704
|
+
serverLabel: serverSlug,
|
|
2998
3705
|
resetHealth: opts.resetHealth
|
|
2999
3706
|
});
|
|
3000
3707
|
}
|
|
3001
3708
|
)
|
|
3002
3709
|
);
|
|
3003
|
-
program.command("logs").description("Tail one server's daemon log (or the supervisor log); secrets redacted.").option("--lines <n>", "trailing lines to show (default 200)", (v) => Number.parseInt(v, 10)).option("--server <
|
|
3710
|
+
program.command("logs").description("Tail one server's daemon log (or the supervisor log); secrets redacted.").option("--lines <n>", "trailing lines to show (default 200)", (v) => Number.parseInt(v, 10)).option("--server <slug>", "select target server slug (required when \u22652 attached)").option("--supervisor", "tail the global supervisor log instead of a per-server daemon log").action(withCliExit(async (opts) => {
|
|
3004
3711
|
await runLogs({ lines: opts.lines, server: opts.server ?? null, supervisor: !!opts.supervisor });
|
|
3005
3712
|
}));
|
|
3006
3713
|
var runners = program.command("runners").description("Computer runner control plane (per-server scoped; \xA712 whitelist server-side).");
|
|
3007
|
-
runners.command("list").description("List runners on one attached server.").option("--json", "emit the machine-readable list").option("--server <
|
|
3714
|
+
runners.command("list").description("List runners on one attached server.").option("--json", "emit the machine-readable list").option("--server <slug>", "select target server slug (required when \u22652 attached)").action(withCliExit(async (opts) => {
|
|
3008
3715
|
await runRunnersList({ json: opts.json, server: opts.server ?? null });
|
|
3009
3716
|
}));
|
|
3010
|
-
runners.command("stop").argument("<agentId>", "id of the runner to stop").option("--server <
|
|
3717
|
+
runners.command("stop").argument("<agentId>", "id of the runner to stop").option("--server <slug>", "select target server slug (required when \u22652 attached)").description("Stop a runner (server-mediated; reuses the orchestrator's agent stop).").action(withCliExit(async (agentId, opts) => {
|
|
3011
3718
|
await withMutationLock(() => runRunnersStop(agentId, { server: opts.server ?? null }));
|
|
3012
3719
|
}));
|
|
3013
3720
|
var channel = program.command("channel").description("Show or set the Computer release channel (latest | alpha | pinned:<semver>).");
|
|
@@ -3094,6 +3801,21 @@ program.command("__upgrade-install-smoke", { hidden: true }).requiredOption("--p
|
|
|
3094
3801
|
);
|
|
3095
3802
|
})
|
|
3096
3803
|
);
|
|
3804
|
+
{
|
|
3805
|
+
const argv = process.argv.slice(2);
|
|
3806
|
+
const isAdopt = argv[0] === "adopt-legacy";
|
|
3807
|
+
const isSetup = argv[0] === "setup";
|
|
3808
|
+
const strayLegacyFlag = argv.find(
|
|
3809
|
+
(a) => a === "--legacy-api-key" || a.startsWith("--legacy-api-key=") || a === "--legacy-api-key-file" || a.startsWith("--legacy-api-key-file=") || a === "--legacy-api-key-stdin"
|
|
3810
|
+
);
|
|
3811
|
+
if (strayLegacyFlag && !isAdopt && !isSetup) {
|
|
3812
|
+
process.stderr.write(
|
|
3813
|
+
`slock-computer: LEGACY_KEY_OUTSIDE_ADOPT: ${strayLegacyFlag} is only accepted by \`slock-computer adopt-legacy\` or \`slock-computer setup --adopt-legacy\`. Remove it or run an adopt flow.
|
|
3814
|
+
`
|
|
3815
|
+
);
|
|
3816
|
+
process.exit(2);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3097
3819
|
program.parseAsync(process.argv).catch((err) => {
|
|
3098
3820
|
process.stderr.write(`slock-computer: ${err instanceof Error ? err.message : String(err)}
|
|
3099
3821
|
`);
|