@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.
Files changed (2) hide show
  1. package/dist/index.js +969 -247
  2. 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 set SLOCK_DEVICE_LOGIN_ENABLED, or upgrade the server)"
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(serverId, name) {
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({ serverId, name })
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 ?? 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) return { 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 verifyUrl = new URL(grant.verificationUri, baseUrl).toString();
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 <serverId>\` to attach this machine.`);
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 mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
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 readFile(sessionFile, "utf8"));
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
- info(`Attaching this machine to server ${opts.serverId}\u2026`);
310
- const attached = await client.attach(opts.serverId, "slock-computer");
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 set SLOCK_DEVICE_LOGIN_ENABLED, or upgrade the server)."
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 id and that you're a member."
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 mkdir2(dirname2(file), { recursive: true });
342
- await writeFile2(
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 chmod(file, 384);
551
+ await chmod2(file, 384);
359
552
  info(`Attached. Computer state written to ${file}`);
360
- info(` server: ${attached.serverId}`);
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/status.ts
370
- import { readFile as readFile5 } from "fs/promises";
371
-
372
- // src/serverState.ts
373
- import { readFile as readFile2, readdir, writeFile as writeFile3, mkdir as mkdir3, unlink, access } from "fs/promises";
374
- import { dirname as dirname3 } from "path";
375
- import { constants as fsConstants } from "fs";
376
- function parseAttachment(raw) {
377
- try {
378
- const a = JSON.parse(raw);
379
- 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") {
380
- return {
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
- return null;
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 readServerAttachment(slockHome, serverId) {
394
- if (!isValidServerId(serverId)) return null;
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
- return parseAttachment(await readFile2(serverAttachmentPath(slockHome, serverId), "utf8"));
397
- } catch {
398
- return null;
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 listAttachedServerIds(slockHome) {
402
- let entries;
651
+ async function stopLegacyDaemonByOwnerFile(ownerFile) {
652
+ let raw;
403
653
  try {
404
- entries = await readdir(serversDir(slockHome));
654
+ raw = await readFile3(ownerFile, "utf8");
405
655
  } catch {
406
- return [];
656
+ return { attempted: false, outcome: "absent" };
407
657
  }
408
- const ids = [];
409
- for (const name of entries) {
410
- if (!isValidServerId(name)) continue;
411
- if (await readServerAttachment(slockHome, name)) ids.push(name);
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
- return ids.sort();
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
- await unlink(serverManagedFlagPath(slockHome, serverId));
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 isServerManaged(slockHome, serverId) {
438
- if (!isValidServerId(serverId)) return false;
685
+ async function runAdoptLegacy(inputs) {
686
+ const slockHome = resolveSlockHome();
687
+ const sessionFile = userSessionPath(slockHome);
688
+ let session;
439
689
  try {
440
- await access(serverManagedFlagPath(slockHome, serverId), fsConstants.F_OK);
441
- return true;
690
+ session = JSON.parse(await readFile3(sessionFile, "utf8"));
442
691
  } catch {
443
- return false;
692
+ fail(
693
+ "NO_USER_SESSION",
694
+ `No user session at ${sessionFile}. Run \`slock-computer login\` first.`
695
+ );
444
696
  }
445
- }
446
- async function listManagedServerIds(slockHome) {
447
- const attached = await listAttachedServerIds(slockHome);
448
- const out = [];
449
- for (const id of attached) {
450
- if (await isServerManaged(slockHome, id)) out.push(id);
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 mkdir6, readFile as readFile4, writeFile as writeFile5, open, unlink as unlink4, rename as rename2 } from "fs/promises";
458
- import { dirname as dirname6, join as joinPath } from "path";
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 mkdir4 } from "fs/promises";
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 (isProcessAlive(pid)) return false;
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 join(computerDir(slockHome), ".quarantine");
1007
+ return join2(computerDir(slockHome), ".quarantine");
560
1008
  }
561
1009
  async function quarantineServerSubtree(slockHome, serverId) {
562
- const src = join(serversDir(slockHome), serverId);
1010
+ const src = join2(serversDir(slockHome), serverId);
563
1011
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
564
- const dest = join(quarantineDir(slockHome), `${stamp}-${serverId}`);
565
- await mkdir4(dirname4(dest), { recursive: true });
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 dirname4(p) {
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 = join(cdir, "upgrade-staging");
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 = join(stagingDir, v);
1055
+ const vdir = join2(stagingDir, v);
608
1056
  try {
609
- const s = await stat(vdir);
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 = join(cdir, "upgrade-snapshot.json");
1072
+ const snap = join2(cdir, "upgrade-snapshot.json");
625
1073
  try {
626
- const s = await stat(snap);
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 = join(computerDir(slockHome), ".lock");
1084
+ const lockDir = join2(computerDir(slockHome), ".lock");
637
1085
  try {
638
- const s = await stat(lockDir);
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 = join(computerDir(slockHome), ".lock");
1096
+ const lockDir = join2(computerDir(slockHome), ".lock");
649
1097
  try {
650
- await stat(lockDir);
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 readFile3, writeFile as writeFile4, unlink as unlink3, mkdir as mkdir5 } from "fs/promises";
672
- import { dirname as dirname5 } from "path";
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 readFile3(serverHealthPath(slockHome, serverId), "utf8");
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 mkdir5(dirname5(path2), { recursive: true });
690
- await writeFile4(path2, JSON.stringify(file), { mode: 384 });
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 recent = await readCrashHistory(slockHome, serverId, nowMs);
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 readFile4(pidfilePath, "utf8")).trim();
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 isProcessAlive(pid) {
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 mkdir6(dirname6(pidfilePath), { recursive: true });
748
- await writeFile5(pidfilePath, String(pid), { mode: 384 });
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 mkdir6(computerDir(slockHome), { recursive: true });
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
- const { DaemonCore } = await import("@slock-ai/daemon/core");
788
- return new DaemonCore({ serverUrl: creds.serverUrl, apiKey: creds.apiKey, localTrace: true });
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 = dirname6(dirname6(here));
1345
+ const installRoot = dirname7(dirname7(here));
856
1346
  let version = null;
857
1347
  try {
858
- const raw = await readFile4(joinPath(installRoot, "package.json"), "utf8");
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 writeFile5(tmp, JSON.stringify(payload) + "\n", { mode: 384 });
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 readFile4(supervisorVersionPath(slockHome), "utf8");
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 mkdir6(computerDir(slockHome), { recursive: true });
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 mkdir6(dirname6(logPath), { recursive: true });
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 && isProcessAlive(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 ${serverId} (nothing to detach).`);
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 ${serverId}.`);
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 readFile5(path2, "utf8"));
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 && isProcessAlive(pid) ? { running: true, pid } : { running: false };
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 = await pidStatus(supervisorPidPath(slockHome));
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 <serverId>`");
1787
+ info("Attachments: none \u2014 run `slock-computer attach /<serverSlug>` (e.g. `/myserver`).");
1149
1788
  } else {
1150
1789
  info("Attachments:");
1151
- info(` ${pad("SERVER", 38)}${pad("HEALTH", 12)}${pad("DAEMON", 24)}${pad("MACHINE", 38)}URL`);
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.serverId, 38)}${pad(s.health, 12)}${pad(dcol, 24)}${pad(s.serverMachineId, 38)}${s.serverUrl}`
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 <serverId> --reset-health` after fixing the underlying issue."
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
- const ids = await listAttachedServerIds(home);
1175
- if (ids.length === 0) {
1176
- fail("NO_ATTACHMENT", "No server attachments yet. Run `slock-computer attach <serverId>` first.");
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 ?? "").trim();
1839
+ const requested = normalizeServerSlug(opts.server ?? "");
1179
1840
  if (requested) {
1180
- if (!isValidServerId(requested)) {
1181
- fail("INVALID_SERVER_ID", `--server ${JSON.stringify(requested)} is not a valid server id (expected a UUID).`);
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 (!ids.includes(requested)) {
1846
+ if (!found) {
1184
1847
  fail(
1185
1848
  "NOT_ATTACHED",
1186
- `Not attached to server ${requested}. Attached servers: ${ids.join(", ")}.`
1849
+ `Server slug ${formatServerSlugDisplay(requested)} is not attached. Attached server slugs: ${attachments.map(attachmentLabel).join(", ")}.`
1187
1850
  );
1188
1851
  }
1189
- return requested;
1852
+ return found.serverId;
1190
1853
  }
1191
- if (ids.length === 1) return ids[0];
1854
+ if (attachments.length === 1) return attachments[0].serverId;
1192
1855
  fail(
1193
1856
  "AMBIGUOUS_SERVER",
1194
- `Multiple servers attached (${ids.join(", ")}). Pass \`--server <serverId>\` to choose one.`
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
- fail("INVALID_ATTACHMENT", `Attachment for ${serverId} is missing/invalid. Re-run \`slock-computer attach ${serverId}\`.`);
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 ${a.serverId} was rejected. Re-run \`slock-computer attach ${a.serverId}\`.`
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 ${a.serverId} (${result.code}). Check --server-url / server version.`
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 ${a.serverId}.`);
1899
+ info(`No runners on server ${label}.`);
1235
1900
  return;
1236
1901
  }
1237
1902
  info("");
1238
- info(`Server ${a.serverId}:`);
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 <serverId>]");
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 ${a.serverId} was rejected. Re-run \`slock-computer attach ${a.serverId}\`.`
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 ${a.serverId}.`);
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 ${a.serverId} (${result.code}). Check --server-url / server version.`
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 ${a.serverId}.`);
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 <serverId>`"
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 ${a.serverId}`,
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 ${a.serverId}`, ok: true, detail: "surface aligned (\xA79 ok)" } : {
1321
- name: `preflight ${a.serverId}`,
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 ${a.serverId}`,
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 readFile6 } from "fs/promises";
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 readFile6(file, "utf8");
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 mkdir7 } from "fs/promises";
1434
- import { join as join2 } from "path";
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 mkdir7(lockTarget, { recursive: true });
1440
- const lockfilePath = join2(lockTarget, ".lock");
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 readFile7, writeFile as writeFile6, mkdir as mkdir8 } from "fs/promises";
1478
- import { dirname as dirname7 } from "path";
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 readFile7(channelPath(slockHome), "utf8");
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 mkdir8(dirname7(p), { recursive: true });
1503
- await writeFile6(p, `${channel2}
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 readFile10 } from "fs/promises";
2192
+ import { readFile as readFile12 } from "fs/promises";
1526
2193
  import { fileURLToPath as fileURLToPath2 } from "url";
1527
- import { dirname as dirname8, join as join5 } from "path";
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 mkdir9, readFile as readFile9, writeFile as writeFile7, rm as rm2, rename as rename3 } from "fs/promises";
1532
- import { join as join4 } from "path";
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 readFile8 } from "fs/promises";
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 join3 } from "path";
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 readFile8(join3(currentBinaryDir, "package.json"), "utf8");
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(join3(currentBinaryDir, "package.json"));
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 = join3(base, ...subPath, "package.json");
2389
+ const candidate = join5(base, ...subPath, "package.json");
1723
2390
  try {
1724
- const raw = await readFile8(candidate, "utf8");
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 join4(computerDir(slockHome), "upgrade-staging", version);
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 ?? readFile9;
2474
+ const fsReadFile = deps.fsReadFile ?? readFile11;
1808
2475
  const stagedPath = upgradeStagingDir(slockHome, version);
1809
- await mkdir9(stagedPath, { recursive: true });
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 = createHash("sha1").update(tarballBytes).digest("hex");
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: join4(cwd, filename), exitCode: 0, stderr: stderrBuf });
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 join4(computerDir(slockHome), "upgrade-snapshot.json");
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 mkdir9(computerDir(slockHome), { recursive: true });
1921
- await writeFile7(tmp, JSON.stringify(snap, null, 2), { mode: 384 });
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 mkdir9(destDir, { recursive: true });
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: join4(destDir, "package") };
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 isProcessAlive2 = deps.isProcessAlive ?? defaultIsProcessAlive;
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 || !isProcessAlive2(pid)) {
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 readFile9(supervisorPidPath(slockHome), "utf8")).trim();
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
- join4(staged.stagedPath, "extracted"),
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 isProcessAlive2 = deps.isProcessAlive ?? defaultIsProcessAlive;
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 (!isProcessAlive2(pid)) {
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 readFile9(serverDaemonPidPath(slockHome, serverId), "utf8")).trim();
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: readdir4 } = await import("fs/promises");
2450
- const entries = await readdir4(stagedPath);
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 join4(stagedPath, tgz);
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 dirname8(dirname8(here));
3284
+ return dirname9(dirname9(here));
2618
3285
  }
2619
3286
  async function defaultCurrentVersion() {
2620
- const pkgPath = join5(defaultCurrentBinaryDir(), "package.json");
3287
+ const pkgPath = join7(defaultCurrentBinaryDir(), "package.json");
2621
3288
  try {
2622
- const raw = await readFile10(pkgPath, "utf8");
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 mkdir10, readdir as readdir3, stat as stat2, writeFile as writeFile8 } from "fs/promises";
2634
- import { join as join6 } from "path";
2635
- import { createHash as createHash2 } from "crypto";
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 = createHash2("sha1").update(tarballBytes).digest("hex");
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: join6(slockHome, "harness-current"),
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 writeFile8(join6(cwd, filename), tarballBytes);
2693
- return { tarballPath: join6(cwd, filename), exitCode: 0, stderr: "" };
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 mkdir10(join6(destDir, "package"), { recursive: true });
2705
- await writeFile8(join6(destDir, "package", "marker.txt"), `NEW@${targetVersion}`);
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 = join6(slockHome, "computer", "upgrade-snapshot.json");
2765
- await mkdir10(snapshotPath, { recursive: true });
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 stat2(path2);
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 readdir3(stagingPath)).sort();
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 readdir3(serversDir(slockHome));
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(join6(computerDir(slockHome), "channel")),
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 mkdir10(slockHome, { recursive: true });
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 mkdir10(upgradeOpts.currentBinaryDir, { recursive: true });
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 mkdir11, readFile as readFile11 } from "fs/promises";
2863
- import { createHash as createHash3 } from "crypto";
2864
- import { dirname as dirname9, isAbsolute, join as join7, resolve as pathResolve } from "path";
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 readFile11(tarballPath);
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 = createHash3("sha1").update(tarballBytes).digest("hex");
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 mkdir11(slockHome, { recursive: true });
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 = join7(cwd, filename);
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 dirname9(dirname9(here));
3605
+ return dirname10(dirname10(here));
2939
3606
  }
2940
3607
  async function defaultCurrentVersionLocal() {
2941
- const pkgPath = join7(defaultCurrentBinaryDirLocal(), "package.json");
3608
+ const pkgPath = join9(defaultCurrentBinaryDirLocal(), "package.json");
2942
3609
  try {
2943
- const raw = await readFile11(pkgPath, "utf8");
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("<serverId>", "target Slock server id").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("--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 (serverId, opts) => {
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({ serverId, serverUrl: opts.serverUrl, run: opts.run, foreground: opts.foreground })
3643
+ () => runAttach({ serverSlug, serverUrl: opts.serverUrl, name: opts.name, run: opts.run, foreground: opts.foreground })
2977
3644
  );
2978
3645
  }));
2979
- program.command("detach").argument("<serverId>", "server id to detach from this Computer").description("Remove ONE server's local attachment; never touches user-session or other servers.").action(withCliExit(async (serverId) => {
2980
- await withMutationLock(() => runDetach(serverId));
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("[serverId]", "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 (serverId, opts) => {
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({ foreground: opts.foreground, serverId: serverId ?? null })
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("[serverId]", "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 <serverId>'s crash history so supervisor resumes auto-restart").action(
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 (serverId, opts) => {
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 <id>", "select target server (required when \u22652 attached)").option("--supervisor", "tail the global supervisor log instead of a per-server daemon log").action(withCliExit(async (opts) => {
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 <id>", "select target server (required when \u22652 attached)").action(withCliExit(async (opts) => {
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 <id>", "select target server (required when \u22652 attached)").description("Stop a runner (server-mediated; reuses the orchestrator's agent stop).").action(withCliExit(async (agentId, opts) => {
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
  `);