@slock-ai/computer 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6917,13 +6917,13 @@ var require_infra = __commonJS({
6917
6917
  }
6918
6918
  function collectASequenceOfCodePointsFast(char, input, position) {
6919
6919
  const idx = input.indexOf(char, position.position);
6920
- const start = position.position;
6920
+ const start2 = position.position;
6921
6921
  if (idx === -1) {
6922
6922
  position.position = input.length;
6923
- return input.slice(start);
6923
+ return input.slice(start2);
6924
6924
  }
6925
6925
  position.position = idx;
6926
- return input.slice(start, position.position);
6926
+ return input.slice(start2, position.position);
6927
6927
  }
6928
6928
  var ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g;
6929
6929
  function forgivingBase64(data) {
@@ -9193,11 +9193,11 @@ var require_formdata_parser = __commonJS({
9193
9193
  }
9194
9194
  }
9195
9195
  function collectASequenceOfBytes(condition, input, position) {
9196
- let start = position.position;
9197
- while (start < input.length && condition(input[start])) {
9198
- ++start;
9196
+ let start2 = position.position;
9197
+ while (start2 < input.length && condition(input[start2])) {
9198
+ ++start2;
9199
9199
  }
9200
- return input.subarray(position.position, position.position = start);
9200
+ return input.subarray(position.position, position.position = start2);
9201
9201
  }
9202
9202
  function removeChars(buf, leading, trailing, predicate) {
9203
9203
  let lead = 0;
@@ -9210,12 +9210,12 @@ var require_formdata_parser = __commonJS({
9210
9210
  }
9211
9211
  return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1);
9212
9212
  }
9213
- function bufferStartsWith(buffer, start, position) {
9214
- if (buffer.length < start.length) {
9213
+ function bufferStartsWith(buffer, start2, position) {
9214
+ if (buffer.length < start2.length) {
9215
9215
  return false;
9216
9216
  }
9217
- for (let i = 0; i < start.length; i++) {
9218
- if (start[i] !== buffer[position.position + i]) {
9217
+ for (let i = 0; i < start2.length; i++) {
9218
+ if (start2[i] !== buffer[position.position + i]) {
9219
9219
  return false;
9220
9220
  }
9221
9221
  }
@@ -9657,8 +9657,8 @@ var require_client_h1 = __commonJS({
9657
9657
  */
9658
9658
  wasm_on_status: (p, at, len) => {
9659
9659
  assert(currentParser.ptr === p);
9660
- const start = at - currentBufferPtr + currentBufferRef.byteOffset;
9661
- return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len));
9660
+ const start2 = at - currentBufferPtr + currentBufferRef.byteOffset;
9661
+ return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start2, len));
9662
9662
  },
9663
9663
  /**
9664
9664
  * @param {number} p
@@ -9676,8 +9676,8 @@ var require_client_h1 = __commonJS({
9676
9676
  */
9677
9677
  wasm_on_header_field: (p, at, len) => {
9678
9678
  assert(currentParser.ptr === p);
9679
- const start = at - currentBufferPtr + currentBufferRef.byteOffset;
9680
- return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len));
9679
+ const start2 = at - currentBufferPtr + currentBufferRef.byteOffset;
9680
+ return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start2, len));
9681
9681
  },
9682
9682
  /**
9683
9683
  * @param {number} p
@@ -9687,8 +9687,8 @@ var require_client_h1 = __commonJS({
9687
9687
  */
9688
9688
  wasm_on_header_value: (p, at, len) => {
9689
9689
  assert(currentParser.ptr === p);
9690
- const start = at - currentBufferPtr + currentBufferRef.byteOffset;
9691
- return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len));
9690
+ const start2 = at - currentBufferPtr + currentBufferRef.byteOffset;
9691
+ return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start2, len));
9692
9692
  },
9693
9693
  /**
9694
9694
  * @param {number} p
@@ -9709,8 +9709,8 @@ var require_client_h1 = __commonJS({
9709
9709
  */
9710
9710
  wasm_on_body: (p, at, len) => {
9711
9711
  assert(currentParser.ptr === p);
9712
- const start = at - currentBufferPtr + currentBufferRef.byteOffset;
9713
- return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len));
9712
+ const start2 = at - currentBufferPtr + currentBufferRef.byteOffset;
9713
+ return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start2, len));
9714
9714
  },
9715
9715
  /**
9716
9716
  * @param {number} p
@@ -13962,8 +13962,8 @@ var require_retry_handler = __commonJS({
13962
13962
  data: { count: this.retryCount }
13963
13963
  });
13964
13964
  }
13965
- const { start, size, end = size ? size - 1 : null } = contentRange;
13966
- assert(this.start === start, "content-range mismatch");
13965
+ const { start: start2, size, end = size ? size - 1 : null } = contentRange;
13966
+ assert(this.start === start2, "content-range mismatch");
13967
13967
  assert(this.end == null || this.end === end, "content-range mismatch");
13968
13968
  return;
13969
13969
  }
@@ -13980,13 +13980,13 @@ var require_retry_handler = __commonJS({
13980
13980
  );
13981
13981
  return;
13982
13982
  }
13983
- const { start, size, end = size ? size - 1 : null } = range;
13983
+ const { start: start2, size, end = size ? size - 1 : null } = range;
13984
13984
  assert(
13985
- start != null && Number.isFinite(start),
13985
+ start2 != null && Number.isFinite(start2),
13986
13986
  "content-range mismatch"
13987
13987
  );
13988
13988
  assert(end != null && Number.isFinite(end), "invalid content-length");
13989
- this.start = start;
13989
+ this.start = start2;
13990
13990
  this.end = end;
13991
13991
  }
13992
13992
  if (this.end == null) {
@@ -14473,9 +14473,9 @@ var require_readable = __commonJS({
14473
14473
  }
14474
14474
  const { _readableState: state } = consume2.stream;
14475
14475
  if (state.bufferIndex) {
14476
- const start = state.bufferIndex;
14476
+ const start2 = state.bufferIndex;
14477
14477
  const end = state.buffer.length;
14478
- for (let n = start; n < end; n++) {
14478
+ for (let n = start2; n < end; n++) {
14479
14479
  consumePush(consume2, state.buffer[n]);
14480
14480
  }
14481
14481
  } else {
@@ -14500,11 +14500,11 @@ var require_readable = __commonJS({
14500
14500
  }
14501
14501
  const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length);
14502
14502
  const bufferLength = buffer.length;
14503
- const start = bufferLength > 2 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191 ? 3 : 0;
14503
+ const start2 = bufferLength > 2 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191 ? 3 : 0;
14504
14504
  if (!encoding || encoding === "utf8" || encoding === "utf-8") {
14505
- return buffer.utf8Slice(start, bufferLength);
14505
+ return buffer.utf8Slice(start2, bufferLength);
14506
14506
  } else {
14507
- return buffer.subarray(start, bufferLength).toString(encoding);
14507
+ return buffer.subarray(start2, bufferLength).toString(encoding);
14508
14508
  }
14509
14509
  }
14510
14510
  function chunksConcat(chunks, length) {
@@ -16617,8 +16617,8 @@ var require_snapshot_recorder = __commonJS({
16617
16617
  "../../node_modules/.pnpm/undici@7.24.8/node_modules/undici/lib/mock/snapshot-recorder.js"(exports, module) {
16618
16618
  "use strict";
16619
16619
  init_esm_shims();
16620
- var { writeFile: writeFile11, readFile: readFile14, mkdir: mkdir15 } = __require("fs/promises");
16621
- var { dirname: dirname12, resolve } = __require("path");
16620
+ var { writeFile: writeFile13, readFile: readFile17, mkdir: mkdir17 } = __require("fs/promises");
16621
+ var { dirname: dirname14, resolve } = __require("path");
16622
16622
  var { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = __require("timers");
16623
16623
  var { InvalidArgumentError: InvalidArgumentError2, UndiciError } = require_errors();
16624
16624
  var { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = require_snapshot_utils();
@@ -16819,7 +16819,7 @@ var require_snapshot_recorder = __commonJS({
16819
16819
  throw new InvalidArgumentError2("Snapshot path is required");
16820
16820
  }
16821
16821
  try {
16822
- const data = await readFile14(resolve(path3), "utf8");
16822
+ const data = await readFile17(resolve(path3), "utf8");
16823
16823
  const parsed = JSON.parse(data);
16824
16824
  if (Array.isArray(parsed)) {
16825
16825
  this.#snapshots.clear();
@@ -16849,12 +16849,12 @@ var require_snapshot_recorder = __commonJS({
16849
16849
  throw new InvalidArgumentError2("Snapshot path is required");
16850
16850
  }
16851
16851
  const resolvedPath = resolve(path3);
16852
- await mkdir15(dirname12(resolvedPath), { recursive: true });
16852
+ await mkdir17(dirname14(resolvedPath), { recursive: true });
16853
16853
  const data = Array.from(this.#snapshots.entries()).map(([hash, snapshot]) => ({
16854
16854
  hash,
16855
16855
  snapshot
16856
16856
  }));
16857
- await writeFile11(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
16857
+ await writeFile13(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
16858
16858
  }
16859
16859
  /**
16860
16860
  * Clears all recorded snapshots
@@ -22862,7 +22862,7 @@ var require_fetch = __commonJS({
22862
22862
  function handleFetchDone(response) {
22863
22863
  finalizeAndReportTiming(response, "fetch");
22864
22864
  }
22865
- function fetch4(input, init = void 0) {
22865
+ function fetch5(input, init = void 0) {
22866
22866
  webidl.argumentLengthCheck(arguments, 1, "globalThis.fetch");
22867
22867
  let p = createDeferredPromise();
22868
22868
  let requestObject;
@@ -23892,7 +23892,7 @@ var require_fetch = __commonJS({
23892
23892
  }
23893
23893
  }
23894
23894
  module.exports = {
23895
- fetch: fetch4,
23895
+ fetch: fetch5,
23896
23896
  Fetch,
23897
23897
  fetching,
23898
23898
  finalizeAndReportTiming
@@ -27902,7 +27902,7 @@ var require_undici = __commonJS({
27902
27902
  err.stack = stack ? `${stack}
27903
27903
  ${captureLines}` : capture.stack;
27904
27904
  }
27905
- module.exports.fetch = function fetch4(init, options = void 0) {
27905
+ module.exports.fetch = function fetch5(init, options = void 0) {
27906
27906
  return fetchImpl(init, options).catch((err) => {
27907
27907
  if (currentFilename) {
27908
27908
  appendFetchStackTrace(err, currentFilename);
@@ -28040,10 +28040,10 @@ var require_polyfills = __commonJS({
28040
28040
  if (platform === "win32") {
28041
28041
  fs.rename = typeof fs.rename !== "function" ? fs.rename : (function(fs$rename) {
28042
28042
  function rename5(from, to, cb) {
28043
- var start = Date.now();
28043
+ var start2 = Date.now();
28044
28044
  var backoff = 0;
28045
28045
  fs$rename(from, to, function CB(er) {
28046
- if (er && (er.code === "EACCES" || er.code === "EPERM" || er.code === "EBUSY") && Date.now() - start < 6e4) {
28046
+ if (er && (er.code === "EACCES" || er.code === "EPERM" || er.code === "EBUSY") && Date.now() - start2 < 6e4) {
28047
28047
  setTimeout(function() {
28048
28048
  fs.stat(to, function(stater, st) {
28049
28049
  if (stater && stater.code === "ENOENT")
@@ -28469,8 +28469,8 @@ var require_graceful_fs = __commonJS({
28469
28469
  fs2.createReadStream = createReadStream;
28470
28470
  fs2.createWriteStream = createWriteStream;
28471
28471
  var fs$readFile = fs2.readFile;
28472
- fs2.readFile = readFile14;
28473
- function readFile14(path3, options, cb) {
28472
+ fs2.readFile = readFile17;
28473
+ function readFile17(path3, options, cb) {
28474
28474
  if (typeof options === "function")
28475
28475
  cb = options, options = null;
28476
28476
  return go$readFile(path3, options, cb);
@@ -28486,8 +28486,8 @@ var require_graceful_fs = __commonJS({
28486
28486
  }
28487
28487
  }
28488
28488
  var fs$writeFile = fs2.writeFile;
28489
- fs2.writeFile = writeFile11;
28490
- function writeFile11(path3, data, options, cb) {
28489
+ fs2.writeFile = writeFile13;
28490
+ function writeFile13(path3, data, options, cb) {
28491
28491
  if (typeof options === "function")
28492
28492
  cb = options, options = null;
28493
28493
  return go$writeFile(path3, data, options, cb);
@@ -28504,8 +28504,8 @@ var require_graceful_fs = __commonJS({
28504
28504
  }
28505
28505
  var fs$appendFile = fs2.appendFile;
28506
28506
  if (fs$appendFile)
28507
- fs2.appendFile = appendFile2;
28508
- function appendFile2(path3, data, options, cb) {
28507
+ fs2.appendFile = appendFile4;
28508
+ function appendFile4(path3, data, options, cb) {
28509
28509
  if (typeof options === "function")
28510
28510
  cb = options, options = null;
28511
28511
  return go$appendFile(path3, data, options, cb);
@@ -29094,7 +29094,7 @@ var require_signal_exit = __commonJS({
29094
29094
  emitter.count -= 1;
29095
29095
  };
29096
29096
  module.exports.unload = unload;
29097
- emit = function emit2(event, code, signal) {
29097
+ emit7 = function emit8(event, code, signal) {
29098
29098
  if (emitter.emitted[event]) {
29099
29099
  return;
29100
29100
  }
@@ -29110,8 +29110,8 @@ var require_signal_exit = __commonJS({
29110
29110
  var listeners = process2.listeners(sig);
29111
29111
  if (listeners.length === emitter.count) {
29112
29112
  unload();
29113
- emit("exit", null, sig);
29114
- emit("afterexit", null, sig);
29113
+ emit7("exit", null, sig);
29114
+ emit7("afterexit", null, sig);
29115
29115
  if (isWin && sig === "SIGHUP") {
29116
29116
  sig = "SIGINT";
29117
29117
  }
@@ -29148,8 +29148,8 @@ var require_signal_exit = __commonJS({
29148
29148
  }
29149
29149
  process2.exitCode = code || /* istanbul ignore next */
29150
29150
  0;
29151
- emit("exit", process2.exitCode, null);
29152
- emit("afterexit", process2.exitCode, null);
29151
+ emit7("exit", process2.exitCode, null);
29152
+ emit7("afterexit", process2.exitCode, null);
29153
29153
  originalProcessReallyExit.call(process2, process2.exitCode);
29154
29154
  };
29155
29155
  originalProcessEmit = process2.emit;
@@ -29159,8 +29159,8 @@ var require_signal_exit = __commonJS({
29159
29159
  process2.exitCode = arg;
29160
29160
  }
29161
29161
  var ret = originalProcessEmit.apply(this, arguments);
29162
- emit("exit", process2.exitCode, null);
29163
- emit("afterexit", process2.exitCode, null);
29162
+ emit7("exit", process2.exitCode, null);
29163
+ emit7("afterexit", process2.exitCode, null);
29164
29164
  return ret;
29165
29165
  } else {
29166
29166
  return originalProcessEmit.apply(this, arguments);
@@ -29173,7 +29173,7 @@ var require_signal_exit = __commonJS({
29173
29173
  var EE;
29174
29174
  var emitter;
29175
29175
  var unload;
29176
- var emit;
29176
+ var emit7;
29177
29177
  var sigListeners;
29178
29178
  var loaded;
29179
29179
  var load;
@@ -29591,6 +29591,48 @@ var {
29591
29591
 
29592
29592
  // src/login.ts
29593
29593
  init_esm_shims();
29594
+
29595
+ // src/output.ts
29596
+ init_esm_shims();
29597
+ var CliExit = class extends Error {
29598
+ /**
29599
+ * v8.3.3 PR-2c — carry the closed-set / stderr token through the thrown
29600
+ * error so callers can pattern-match without parsing stderr. Optional
29601
+ * because some callsites throw `new CliExit(code)` directly without a
29602
+ * named token (e.g. the harness EX_CONFIG paths).
29603
+ */
29604
+ constructor(exitCode, code) {
29605
+ super(`CliExit(${exitCode}${code ? ` ${code}` : ""})`);
29606
+ this.exitCode = exitCode;
29607
+ this.code = code;
29608
+ this.name = "CliExit";
29609
+ }
29610
+ };
29611
+ function info(line) {
29612
+ process.stdout.write(`${line}
29613
+ `);
29614
+ }
29615
+ function fail(code, message, exitCode = 1) {
29616
+ process.stderr.write(`${JSON.stringify({ ok: false, code, message })}
29617
+ `);
29618
+ throw new CliExit(exitCode, code);
29619
+ }
29620
+
29621
+ // src/services/errors.ts
29622
+ init_esm_shims();
29623
+ var ComputerServiceError = class extends Error {
29624
+ code;
29625
+ cause;
29626
+ constructor(code, message, cause) {
29627
+ super(message);
29628
+ this.name = "ComputerServiceError";
29629
+ this.code = code;
29630
+ if (cause !== void 0) this.cause = cause;
29631
+ }
29632
+ };
29633
+
29634
+ // src/services/login.ts
29635
+ init_esm_shims();
29594
29636
  import { mkdir, writeFile } from "fs/promises";
29595
29637
  import { dirname } from "path";
29596
29638
 
@@ -29849,6 +29891,9 @@ function serverManagedFlagPath(slockHome, serverId) {
29849
29891
  function serverHealthPath(slockHome, serverId) {
29850
29892
  return path2.join(serverDir(slockHome, serverId), "health.json");
29851
29893
  }
29894
+ function serviceStatePath(slockHome) {
29895
+ return path2.join(computerDir(slockHome), "service.state.json");
29896
+ }
29852
29897
  function supervisorPidPath(slockHome) {
29853
29898
  return path2.join(computerDir(slockHome), "supervisor.pid");
29854
29899
  }
@@ -29901,32 +29946,6 @@ function formatUpgradeLogTimestamp(date = /* @__PURE__ */ new Date()) {
29901
29946
  return iso.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
29902
29947
  }
29903
29948
 
29904
- // src/output.ts
29905
- init_esm_shims();
29906
- var CliExit = class extends Error {
29907
- /**
29908
- * v8.3.3 PR-2c — carry the closed-set / stderr token through the thrown
29909
- * error so callers can pattern-match without parsing stderr. Optional
29910
- * because some callsites throw `new CliExit(code)` directly without a
29911
- * named token (e.g. the harness EX_CONFIG paths).
29912
- */
29913
- constructor(exitCode, code) {
29914
- super(`CliExit(${exitCode}${code ? ` ${code}` : ""})`);
29915
- this.exitCode = exitCode;
29916
- this.code = code;
29917
- this.name = "CliExit";
29918
- }
29919
- };
29920
- function info(line) {
29921
- process.stdout.write(`${line}
29922
- `);
29923
- }
29924
- function fail(code, message, exitCode = 1) {
29925
- process.stderr.write(`${JSON.stringify({ ok: false, code, message })}
29926
- `);
29927
- throw new CliExit(exitCode, code);
29928
- }
29929
-
29930
29949
  // src/serverUrl.ts
29931
29950
  init_esm_shims();
29932
29951
  var DEFAULT_SLOCK_SERVER_URL = "https://api.slock.ai";
@@ -29938,62 +29957,146 @@ function resolveServerUrl(...candidates) {
29938
29957
  return DEFAULT_SLOCK_SERVER_URL;
29939
29958
  }
29940
29959
 
29941
- // src/login.ts
29942
- function sleep(ms) {
29943
- return new Promise((r) => setTimeout(r, ms));
29960
+ // src/services/login.ts
29961
+ function sleep(ms, signal) {
29962
+ return new Promise((resolve, reject) => {
29963
+ if (signal?.aborted) {
29964
+ reject(abortError(signal));
29965
+ return;
29966
+ }
29967
+ const t = setTimeout(() => {
29968
+ signal?.removeEventListener("abort", onAbort);
29969
+ resolve();
29970
+ }, ms);
29971
+ const onAbort = () => {
29972
+ clearTimeout(t);
29973
+ reject(abortError(signal));
29974
+ };
29975
+ signal?.addEventListener("abort", onAbort, { once: true });
29976
+ });
29944
29977
  }
29945
- async function runLogin(opts) {
29946
- const baseUrl = resolveServerUrl(opts.serverUrl, process.env.SLOCK_SERVER_URL);
29978
+ function abortError(signal) {
29979
+ const reason = signal.reason;
29980
+ if (reason instanceof Error) return reason;
29981
+ const err = new Error("aborted");
29982
+ err.name = "AbortError";
29983
+ return err;
29984
+ }
29985
+ function emit(opts, event) {
29986
+ const cb = opts?.onEvent;
29987
+ if (!cb) return;
29988
+ try {
29989
+ cb(event);
29990
+ } catch {
29991
+ }
29992
+ }
29993
+ async function login(input, options = {}) {
29994
+ const baseUrl = resolveServerUrl(input.serverUrl, process.env.SLOCK_SERVER_URL);
29995
+ options.signal?.throwIfAborted?.();
29947
29996
  const client = new DeviceAuthClient(baseUrl);
29948
29997
  let grant;
29949
29998
  try {
29950
29999
  grant = await client.authorize("slock-computer");
29951
30000
  } catch (err) {
29952
30001
  const reason = err instanceof Error ? err.message : String(err);
29953
- fail(
30002
+ throw new ComputerServiceError(
29954
30003
  "DEVICE_AUTHORIZE_FAILED",
29955
- `Could not start device login at ${baseUrl}: ${reason}. Check that the server URL is correct and reachable.`
30004
+ `Could not start device login at ${baseUrl}: ${reason}. Check that the server URL is correct and reachable.`,
30005
+ err
29956
30006
  );
29957
30007
  }
29958
30008
  const verificationUri = grant.verificationUriComplete || grant.verificationUri;
29959
30009
  const verifyUrl = new URL(verificationUri, baseUrl).toString();
29960
- info(`To finish login, open: ${verifyUrl}`);
29961
- info(`and enter the code: ${grant.userCode}`);
29962
- info(`Waiting for approval (expires in ${grant.expiresIn}s)\u2026`);
30010
+ const expiresAt = new Date(Date.now() + grant.expiresIn * 1e3).toISOString();
30011
+ emit(options, {
30012
+ type: "device-code",
30013
+ verifyUrl,
30014
+ userCode: grant.userCode,
30015
+ expiresAt,
30016
+ expiresInSeconds: grant.expiresIn
30017
+ });
29963
30018
  const intervalMs = Math.max(1, grant.interval) * 1e3;
29964
30019
  const deadline = Date.now() + grant.expiresIn * 1e3;
29965
30020
  while (Date.now() < deadline) {
29966
- await sleep(intervalMs);
30021
+ options.signal?.throwIfAborted?.();
30022
+ await sleep(intervalMs, options.signal);
30023
+ emit(options, { type: "polling" });
29967
30024
  const r = await client.token(grant.deviceCode);
29968
30025
  if (r.status === "pending") continue;
29969
- if (r.status === "denied") fail("LOGIN_DENIED", "Login was denied in the approval page.");
29970
- if (r.status === "expired") fail("LOGIN_EXPIRED", "Login request expired before approval. Re-run `slock-computer login`.");
29971
- if (r.status === "error") fail("LOGIN_FAILED", `Login failed (${r.code}). Re-run \`slock-computer login\`.`);
30026
+ if (r.status === "denied") {
30027
+ throw new ComputerServiceError("LOGIN_DENIED", "Login was denied in the approval page.");
30028
+ }
30029
+ if (r.status === "expired") {
30030
+ throw new ComputerServiceError(
30031
+ "LOGIN_EXPIRED",
30032
+ "Login request expired before approval. Re-run `slock-computer login`."
30033
+ );
30034
+ }
30035
+ if (r.status === "error") {
30036
+ throw new ComputerServiceError(
30037
+ "LOGIN_FAILED",
30038
+ `Login failed (${r.code}). Re-run \`slock-computer login\`.`
30039
+ );
30040
+ }
29972
30041
  const slockHome = resolveSlockHome();
29973
30042
  const file = userSessionPath(slockHome);
29974
30043
  await mkdir(dirname(file), { recursive: true });
29975
30044
  await writeFile(
29976
30045
  file,
29977
30046
  JSON.stringify(
29978
- { kind: "user-session", userId: r.userId, accessToken: r.accessToken, refreshToken: r.refreshToken, serverUrl: baseUrl, createdAt: (/* @__PURE__ */ new Date()).toISOString() },
30047
+ {
30048
+ kind: "user-session",
30049
+ userId: r.userId,
30050
+ accessToken: r.accessToken,
30051
+ refreshToken: r.refreshToken,
30052
+ serverUrl: baseUrl,
30053
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
30054
+ },
29979
30055
  null,
29980
30056
  2
29981
30057
  ),
29982
30058
  { mode: 384 }
29983
30059
  );
29984
- info(`Logged in. User session written to ${file}`);
29985
- if (!opts.orchestrated) {
29986
- info(`Next: run \`slock-computer attach <serverSlug>\` to attach this machine.`);
30060
+ emit(options, { type: "approved", userId: r.userId, sessionPath: file });
30061
+ return { userId: r.userId, sessionPath: file, serverUrl: baseUrl };
30062
+ }
30063
+ throw new ComputerServiceError(
30064
+ "LOGIN_EXPIRED",
30065
+ "Login request expired before approval. Re-run `slock-computer login`."
30066
+ );
30067
+ }
30068
+
30069
+ // src/login.ts
30070
+ async function runLogin(opts) {
30071
+ try {
30072
+ await login(
30073
+ { serverUrl: opts.serverUrl },
30074
+ {
30075
+ onEvent: (event) => {
30076
+ if (event.type === "device-code") {
30077
+ info(`To finish login, open: ${event.verifyUrl}`);
30078
+ info(`and enter the code: ${event.userCode}`);
30079
+ info(`Waiting for approval (expires in ${event.expiresInSeconds}s)\u2026`);
30080
+ } else if (event.type === "approved") {
30081
+ info(`Logged in. User session written to ${event.sessionPath}`);
30082
+ if (!opts.orchestrated) {
30083
+ info(`Next: run \`slock-computer attach <serverSlug>\` to attach this machine.`);
30084
+ }
30085
+ }
30086
+ }
30087
+ }
30088
+ );
30089
+ } catch (err) {
30090
+ if (err instanceof CliExit) throw err;
30091
+ if (err instanceof ComputerServiceError) {
30092
+ fail(err.code, err.message);
29987
30093
  }
29988
- return;
30094
+ throw err;
29989
30095
  }
29990
- fail("LOGIN_EXPIRED", "Login request expired before approval. Re-run `slock-computer login`.");
29991
30096
  }
29992
30097
 
29993
30098
  // src/attach.ts
29994
30099
  init_esm_shims();
29995
- import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
29996
- import { dirname as dirname3 } from "path";
29997
30100
 
29998
30101
  // src/serverState.ts
29999
30102
  init_esm_shims();
@@ -30102,77 +30205,95 @@ async function listManagedServerIds(slockHome) {
30102
30205
  return out;
30103
30206
  }
30104
30207
 
30105
- // src/attach.ts
30106
- async function runAttach(opts) {
30208
+ // src/services/attach.ts
30209
+ init_esm_shims();
30210
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
30211
+ import { dirname as dirname3 } from "path";
30212
+ function emit2(opts, event) {
30213
+ const cb = opts?.onEvent;
30214
+ if (!cb) return;
30215
+ try {
30216
+ cb(event);
30217
+ } catch {
30218
+ }
30219
+ }
30220
+ async function attach(input, options = {}) {
30221
+ options.signal?.throwIfAborted?.();
30107
30222
  const slockHome = resolveSlockHome();
30108
30223
  const sessionFile = userSessionPath(slockHome);
30109
30224
  let session;
30110
30225
  try {
30111
30226
  session = JSON.parse(await readFile2(sessionFile, "utf8"));
30112
- } catch {
30113
- fail(
30227
+ } catch (err) {
30228
+ throw new ComputerServiceError(
30114
30229
  "NO_USER_SESSION",
30115
- `No user session at ${sessionFile}. Run \`slock-computer login\` first.`
30230
+ `No user session at ${sessionFile}. Run \`slock-computer login\` first.`,
30231
+ err
30116
30232
  );
30117
30233
  }
30118
30234
  if (session.kind !== "user-session" || typeof session.accessToken !== "string" || !session.accessToken) {
30119
- fail(
30235
+ throw new ComputerServiceError(
30120
30236
  "INVALID_USER_SESSION",
30121
30237
  `User session at ${sessionFile} is invalid. Re-run \`slock-computer login\`.`
30122
30238
  );
30123
30239
  }
30124
- const baseUrl = resolveServerUrl(opts.serverUrl, session.serverUrl, process.env.SLOCK_SERVER_URL);
30125
- const client = new ComputerAttachClient(baseUrl, session.accessToken);
30126
- const slugForServer = normalizeServerSlug(opts.serverSlug);
30240
+ const baseUrl = resolveServerUrl(input.serverUrl, session.serverUrl, process.env.SLOCK_SERVER_URL);
30241
+ const slugForServer = normalizeServerSlug(input.serverSlug);
30127
30242
  if (!slugForServer) {
30128
- fail("ATTACH_NOT_AUTHORIZED", "Server slug must not be empty.");
30243
+ throw new ComputerServiceError("ATTACH_NOT_AUTHORIZED", "Server slug must not be empty.");
30129
30244
  }
30130
- const computerName = opts.name?.trim() || deriveDefaultComputerName();
30131
- info(`Attaching this machine to server ${formatServerSlugDisplay(slugForServer)}\u2026`);
30245
+ const computerName = input.name?.trim() || deriveDefaultComputerName();
30246
+ options.signal?.throwIfAborted?.();
30247
+ emit2(options, { type: "attaching", serverSlug: slugForServer });
30248
+ const client = new ComputerAttachClient(baseUrl, session.accessToken);
30132
30249
  const attached = await client.attach(slugForServer, computerName);
30133
30250
  if (attached.status === "disabled") {
30134
- fail(
30251
+ throw new ComputerServiceError(
30135
30252
  "ATTACH_DISABLED",
30136
30253
  "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)."
30137
30254
  );
30138
30255
  }
30139
30256
  if (attached.status === "not_authorized") {
30140
- fail(
30257
+ throw new ComputerServiceError(
30141
30258
  "ATTACH_NOT_AUTHORIZED",
30142
30259
  "Not authorized to attach to that server. Check the server slug and that you're a member."
30143
30260
  );
30144
30261
  }
30145
30262
  if (attached.status === "server_not_found") {
30146
- fail(
30263
+ throw new ComputerServiceError(
30147
30264
  "ATTACH_SERVER_NOT_FOUND",
30148
30265
  `Server ${formatServerSlugDisplay(slugForServer)} was not found on ${baseUrl}. Check the slug spelling and --server-url, then retry.`
30149
30266
  );
30150
30267
  }
30151
30268
  if (attached.status === "error") {
30152
30269
  if (attached.code === "session_invalid") {
30153
- fail(
30270
+ throw new ComputerServiceError(
30154
30271
  "USER_SESSION_EXPIRED",
30155
30272
  "Your user session is no longer valid. Re-run `slock-computer login`."
30156
30273
  );
30157
30274
  }
30158
30275
  if (attached.code === "request_failed") {
30159
- fail(
30276
+ throw new ComputerServiceError(
30160
30277
  "ATTACH_REQUEST_FAILED",
30161
30278
  `Could not reach ${baseUrl} while attaching to ${formatServerSlugDisplay(slugForServer)}. Check --server-url / network connectivity, then retry.`
30162
30279
  );
30163
30280
  }
30164
30281
  if (attached.code === "COMPUTER_NAME_COLLISION") {
30165
- fail(
30282
+ throw new ComputerServiceError(
30166
30283
  "COMPUTER_NAME_COLLISION",
30167
30284
  `A Computer named ${JSON.stringify(computerName)} already exists on that server. Re-run with --name <name>.`
30168
30285
  );
30169
30286
  }
30170
- fail("ATTACH_FAILED", `Attach failed (${attached.code}). Re-run after checking --server-url / server version.`);
30287
+ throw new ComputerServiceError(
30288
+ "ATTACH_FAILED",
30289
+ `Attach failed (${attached.code}). Re-run after checking --server-url / server version.`
30290
+ );
30171
30291
  }
30172
- info(attached.resumed ? "Resumed existing attachment; running preflight\u2026" : "Attachment issued; running preflight\u2026");
30292
+ emit2(options, { type: "preflight", resumed: attached.resumed });
30293
+ options.signal?.throwIfAborted?.();
30173
30294
  const pre = await client.preflight(attached.apiKey);
30174
30295
  if (!pre.ok) {
30175
- fail(
30296
+ throw new ComputerServiceError(
30176
30297
  "PREFLIGHT_FAILED",
30177
30298
  `Server preflight failed (${pre.code}). The server's Computer surface is not aligned; nothing was written locally. Upgrade the server or retry.`
30178
30299
  );
@@ -30197,9 +30318,57 @@ async function runAttach(opts) {
30197
30318
  { mode: 384 }
30198
30319
  );
30199
30320
  await chmod2(file, 384);
30200
- info(`Attached. Computer state written to ${file}`);
30201
- info(` server: ${formatServerSlugDisplay(attached.serverSlug)}`);
30202
- info(` serverMachine: ${attached.serverMachineId}`);
30321
+ const apiKeyRedactedPrefix = attached.apiKey.slice(0, 8);
30322
+ emit2(options, {
30323
+ type: "attached",
30324
+ serverId: attached.serverId,
30325
+ serverMachineId: attached.serverMachineId,
30326
+ serverSlug: attached.serverSlug,
30327
+ attachmentPath: file,
30328
+ resumed: attached.resumed,
30329
+ apiKeyRedactedPrefix
30330
+ });
30331
+ return {
30332
+ serverId: attached.serverId,
30333
+ serverMachineId: attached.serverMachineId,
30334
+ serverSlug: attached.serverSlug,
30335
+ serverUrl: baseUrl,
30336
+ attachmentPath: file,
30337
+ resumed: attached.resumed,
30338
+ apiKeyRedactedPrefix
30339
+ };
30340
+ }
30341
+
30342
+ // src/attach.ts
30343
+ async function runAttach(opts) {
30344
+ try {
30345
+ await attach(
30346
+ {
30347
+ serverSlug: opts.serverSlug,
30348
+ serverUrl: opts.serverUrl,
30349
+ name: opts.name
30350
+ },
30351
+ {
30352
+ onEvent: (event) => {
30353
+ if (event.type === "attaching") {
30354
+ info(`Attaching this machine to server ${formatServerSlugDisplay(event.serverSlug)}\u2026`);
30355
+ } else if (event.type === "preflight") {
30356
+ info(event.resumed ? "Resumed existing attachment; running preflight\u2026" : "Attachment issued; running preflight\u2026");
30357
+ } else if (event.type === "attached") {
30358
+ info(`Attached. Computer state written to ${event.attachmentPath}`);
30359
+ info(` server: ${formatServerSlugDisplay(event.serverSlug)}`);
30360
+ info(` serverMachine: ${event.serverMachineId}`);
30361
+ }
30362
+ }
30363
+ }
30364
+ );
30365
+ } catch (err) {
30366
+ if (err instanceof CliExit) throw err;
30367
+ if (err instanceof ComputerServiceError) {
30368
+ fail(err.code, err.message);
30369
+ }
30370
+ throw err;
30371
+ }
30203
30372
  if (opts.orchestrated) {
30204
30373
  return;
30205
30374
  }
@@ -30210,85 +30379,92 @@ async function runAttach(opts) {
30210
30379
  }
30211
30380
  }
30212
30381
 
30213
- // src/adopt.ts
30382
+ // src/setup.ts
30214
30383
  init_esm_shims();
30215
- import { chmod as chmod3, mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, appendFile, stat } from "fs/promises";
30216
- import { createHash as createHash2 } from "crypto";
30217
- import { dirname as dirname4, join } from "path";
30218
- import { setTimeout as delay } from "timers/promises";
30219
- async function resolveLegacyKey(inputs, env) {
30220
- const sources = [];
30221
- if (typeof inputs.legacyApiKey === "string" && inputs.legacyApiKey.length > 0) {
30222
- sources.push({
30223
- mode: "legacy_key_argv",
30224
- load: async () => inputs.legacyApiKey.trim()
30225
- });
30384
+ var import_undici3 = __toESM(require_undici(), 1);
30385
+ import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename3, rm as rm2, writeFile as writeFile8 } from "fs/promises";
30386
+ import { dirname as dirname9 } from "path";
30387
+
30388
+ // src/lib/migration.ts
30389
+ init_esm_shims();
30390
+ import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
30391
+ import { join } from "path";
30392
+ var MACHINE_DIR_PREFIX = "machine-";
30393
+ async function readOwnerEvidence(installRoot, machineDirName) {
30394
+ const ownerFile = join(
30395
+ installRoot,
30396
+ "machines",
30397
+ machineDirName,
30398
+ "daemon.lock",
30399
+ "owner.json"
30400
+ );
30401
+ let raw;
30402
+ try {
30403
+ raw = await readFile3(ownerFile, "utf8");
30404
+ } catch {
30405
+ return {};
30226
30406
  }
30227
- if (typeof inputs.legacyApiKeyFile === "string" && inputs.legacyApiKeyFile.length > 0) {
30228
- sources.push({
30229
- mode: "legacy_key_file",
30230
- load: async () => {
30231
- const contents = await readFile3(inputs.legacyApiKeyFile, "utf8");
30232
- return contents.trim();
30233
- }
30234
- });
30407
+ let parsed;
30408
+ try {
30409
+ parsed = JSON.parse(raw);
30410
+ } catch {
30411
+ return {};
30235
30412
  }
30236
- if (inputs.legacyApiKeyStdin) {
30237
- sources.push({
30238
- mode: "legacy_key_stdin",
30239
- load: async () => readAllStdin()
30240
- });
30413
+ const machineName = typeof parsed.hostname === "string" && parsed.hostname.length > 0 ? parsed.hostname : void 0;
30414
+ const serverUrl = typeof parsed.serverUrl === "string" && parsed.serverUrl.length > 0 ? parsed.serverUrl : void 0;
30415
+ return { machineName, serverUrl };
30416
+ }
30417
+ async function detectLegacyMigration(installRoot, loggedInUserId) {
30418
+ void loggedInUserId;
30419
+ const machinesDir = join(installRoot, "machines");
30420
+ let entries;
30421
+ try {
30422
+ entries = await readdir2(machinesDir);
30423
+ } catch {
30424
+ return { kind: "no-match" };
30241
30425
  }
30242
- const envKey = env.SLOCK_LEGACY_API_KEY;
30243
- if (typeof envKey === "string" && envKey.trim().length > 0) {
30244
- sources.push({
30245
- mode: "legacy_key_env",
30246
- load: async () => envKey.trim()
30426
+ const candidates = [];
30427
+ for (const name of entries) {
30428
+ if (!name.startsWith(MACHINE_DIR_PREFIX)) continue;
30429
+ const evidence = await readOwnerEvidence(installRoot, name);
30430
+ candidates.push({
30431
+ machineId: name,
30432
+ machineName: evidence.machineName,
30433
+ serverUrl: evidence.serverUrl
30247
30434
  });
30248
30435
  }
30249
- if (sources.length === 0) {
30250
- fail(
30251
- "LEGACY_KEY_REQUIRED",
30252
- "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."
30253
- );
30254
- }
30255
- if (sources.length > 1) {
30256
- const modes = sources.map((s) => s.mode).join(", ");
30257
- fail(
30258
- "LEGACY_KEY_MULTIPLE_SOURCES",
30259
- `Multiple legacy api key sources provided (${modes}). Choose exactly one.`
30260
- );
30261
- }
30262
- const chosen = sources[0];
30263
- const rawKey = await chosen.load();
30264
- if (chosen.mode === "legacy_key_env") {
30265
- delete env.SLOCK_LEGACY_API_KEY;
30266
- }
30267
- if (!rawKey.startsWith("sk_machine_") && !rawKey.startsWith("sk_daemon_")) {
30268
- fail(
30269
- "LEGACY_KEY_INVALID",
30270
- "Provided key does not look like a legacy machine key (expected `sk_machine_*` or `sk_daemon_*`)."
30271
- );
30436
+ if (candidates.length === 0) return { kind: "no-match" };
30437
+ if (candidates.length === 1) {
30438
+ const only = candidates[0];
30439
+ return {
30440
+ kind: "match",
30441
+ machineId: only.machineId,
30442
+ machineName: only.machineName,
30443
+ serverUrl: only.serverUrl
30444
+ };
30272
30445
  }
30273
- return {
30274
- rawKey,
30275
- mode: chosen.mode,
30276
- redactedPrefix: rawKey.slice(0, 8)
30277
- };
30446
+ return { kind: "ambiguous", candidates };
30278
30447
  }
30279
- async function readAllStdin() {
30280
- return new Promise((resolve, reject) => {
30281
- const chunks = [];
30282
- process.stdin.on("data", (chunk) => chunks.push(chunk));
30283
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8").trim()));
30284
- process.stdin.on("error", reject);
30285
- });
30448
+
30449
+ // src/services/adoptLegacy.ts
30450
+ init_esm_shims();
30451
+ import { chmod as chmod3, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, appendFile, stat } from "fs/promises";
30452
+ import { createHash as createHash2 } from "crypto";
30453
+ import { dirname as dirname4, join as join2 } from "path";
30454
+ import { setTimeout as delay } from "timers/promises";
30455
+ function emit3(opts, event) {
30456
+ const cb = opts?.onEvent;
30457
+ if (!cb) return;
30458
+ try {
30459
+ cb(event);
30460
+ } catch {
30461
+ }
30286
30462
  }
30287
30463
  var LEGACY_STOP_WAIT_MS = 1e4;
30288
30464
  var LEGACY_STOP_POLL_MS = 200;
30289
30465
  function legacyLockOwnerPath(slockHome, rawKey) {
30290
30466
  const fingerprint = createHash2("sha256").update(rawKey).digest("hex").slice(0, 16);
30291
- return join(slockHome, "machines", `machine-${fingerprint}`, "daemon.lock", "owner.json");
30467
+ return join2(slockHome, "machines", `machine-${fingerprint}`, "daemon.lock", "owner.json");
30292
30468
  }
30293
30469
  function isProcessAlive(pid) {
30294
30470
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -30303,7 +30479,7 @@ function isProcessAlive(pid) {
30303
30479
  async function stopLegacyDaemonByOwnerFile(ownerFile) {
30304
30480
  let raw;
30305
30481
  try {
30306
- raw = await readFile3(ownerFile, "utf8");
30482
+ raw = await readFile4(ownerFile, "utf8");
30307
30483
  } catch {
30308
30484
  return { attempted: false, outcome: "absent" };
30309
30485
  }
@@ -30337,7 +30513,7 @@ async function stopLegacyDaemonByOwnerFile(ownerFile) {
30337
30513
  async function readLegacyOwnerEvidence(ownerFile) {
30338
30514
  let raw;
30339
30515
  try {
30340
- raw = await readFile3(ownerFile, "utf8");
30516
+ raw = await readFile4(ownerFile, "utf8");
30341
30517
  } catch {
30342
30518
  return { ownerFile, reason: "owner_file_absent" };
30343
30519
  }
@@ -30371,162 +30547,164 @@ function formatLegacyOwnerEvidence(slockHome, evidence) {
30371
30547
  if (evidence.reason) fields.push(`ownerStatus=${evidence.reason}`);
30372
30548
  return fields.join(" ");
30373
30549
  }
30374
- async function runAdoptLegacy(inputs) {
30550
+ async function adoptLegacy(input, options = {}) {
30551
+ options.signal?.throwIfAborted?.();
30375
30552
  const slockHome = resolveSlockHome();
30376
30553
  const sessionFile = userSessionPath(slockHome);
30377
30554
  let session;
30378
30555
  try {
30379
- session = JSON.parse(await readFile3(sessionFile, "utf8"));
30380
- } catch {
30381
- fail(
30556
+ session = JSON.parse(await readFile4(sessionFile, "utf8"));
30557
+ } catch (err) {
30558
+ throw new ComputerServiceError(
30382
30559
  "NO_USER_SESSION",
30383
- `No user session at ${sessionFile}. Run \`slock-computer login\` first.`
30560
+ `No user session at ${sessionFile}. Run \`slock-computer login\` first.`,
30561
+ err
30384
30562
  );
30385
30563
  }
30386
30564
  if (session.kind !== "user-session" || typeof session.accessToken !== "string" || !session.accessToken) {
30387
- fail(
30565
+ throw new ComputerServiceError(
30388
30566
  "INVALID_USER_SESSION",
30389
30567
  `User session at ${sessionFile} is invalid. Re-run \`slock-computer login\`.`
30390
30568
  );
30391
30569
  }
30392
- const baseUrl = resolveServerUrl(inputs.serverUrl, session.serverUrl, process.env.SLOCK_SERVER_URL);
30393
- const slugForServer = normalizeServerSlug(inputs.serverSlug);
30570
+ const baseUrl = resolveServerUrl(input.serverUrl, session.serverUrl, process.env.SLOCK_SERVER_URL);
30571
+ const slugForServer = normalizeServerSlug(input.serverSlug);
30394
30572
  if (!slugForServer) {
30395
- fail("ADOPT_NOT_AUTHORIZED", "Server slug must not be empty.");
30573
+ throw new ComputerServiceError("ADOPT_NOT_AUTHORIZED", "Server slug must not be empty.");
30396
30574
  }
30397
- const legacy = await resolveLegacyKey(inputs, process.env);
30398
- info(
30399
- `Adopting legacy daemon for ${formatServerSlugDisplay(slugForServer)} via ${legacy.mode}\u2026`
30400
- );
30575
+ options.signal?.throwIfAborted?.();
30576
+ emit3(options, { type: "adopting", serverSlug: slugForServer, mode: input.mode });
30401
30577
  const client = new ComputerAttachClient(baseUrl, session.accessToken);
30402
30578
  const adoptStartedAt = /* @__PURE__ */ new Date();
30403
- const legacyOwnerFile = legacyLockOwnerPath(slockHome, legacy.rawKey);
30579
+ const legacyOwnerFile = legacyLockOwnerPath(slockHome, input.rawKey);
30404
30580
  const ownerEvidence = await readLegacyOwnerEvidence(legacyOwnerFile);
30405
- const result = await client.adoptLegacy(legacy.rawKey, inputs.name);
30406
- legacy.rawKey = void 0;
30581
+ const result = await client.adoptLegacy(input.rawKey, input.name);
30582
+ input.rawKey = void 0;
30407
30583
  if (result.status === "disabled") {
30408
30584
  await appendAdoptionLog(slockHome, {
30409
- mode: legacy.mode,
30410
- redactedPrefix: legacy.redactedPrefix,
30585
+ mode: input.mode,
30586
+ redactedPrefix: input.redactedPrefix,
30411
30587
  startedAt: adoptStartedAt,
30412
30588
  outcome: "failed",
30413
30589
  failureReason: "computer_adopt_disabled"
30414
30590
  });
30415
- fail(
30591
+ throw new ComputerServiceError(
30416
30592
  "ADOPT_DISABLED",
30417
30593
  "Computer legacy adoption is not enabled on this server. Upgrade the server or set SLOCK_DEVICE_LOGIN_ENABLED."
30418
30594
  );
30419
30595
  }
30420
30596
  if (result.status === "legacy_key_invalid") {
30421
30597
  await appendAdoptionLog(slockHome, {
30422
- mode: legacy.mode,
30423
- redactedPrefix: legacy.redactedPrefix,
30598
+ mode: input.mode,
30599
+ redactedPrefix: input.redactedPrefix,
30424
30600
  startedAt: adoptStartedAt,
30425
30601
  outcome: "failed",
30426
30602
  failureReason: "legacy_key_invalid"
30427
30603
  });
30428
- fail(
30604
+ throw new ComputerServiceError(
30429
30605
  "LEGACY_KEY_INVALID",
30430
30606
  `Server rejected the legacy api key (unknown / wrong server / malformed). Local legacy owner evidence for this key: ${formatLegacyOwnerEvidence(slockHome, ownerEvidence)}. Verify the key came from this SLOCK_HOME and server, or use a fresh isolated SLOCK_HOME for a clean Computer setup.`
30431
30607
  );
30432
30608
  }
30433
30609
  if (result.status === "legacy_machine_key_migrated") {
30434
30610
  await appendAdoptionLog(slockHome, {
30435
- mode: legacy.mode,
30436
- redactedPrefix: legacy.redactedPrefix,
30611
+ mode: input.mode,
30612
+ redactedPrefix: input.redactedPrefix,
30437
30613
  startedAt: adoptStartedAt,
30438
30614
  outcome: "failed",
30439
30615
  failureReason: "legacy_machine_key_migrated"
30440
30616
  });
30441
- fail(
30617
+ throw new ComputerServiceError(
30442
30618
  "LEGACY_MACHINE_KEY_MIGRATED",
30443
30619
  `This machine has already been adopted; the legacy key is no longer accepted. Local legacy owner evidence for this key: ${formatLegacyOwnerEvidence(slockHome, ownerEvidence)}. Use \`slock-computer attach\` to add another Computer attachment, or use a fresh isolated SLOCK_HOME for a clean setup.`
30444
30620
  );
30445
30621
  }
30446
30622
  if (result.status === "auth_required") {
30447
30623
  await appendAdoptionLog(slockHome, {
30448
- mode: legacy.mode,
30449
- redactedPrefix: legacy.redactedPrefix,
30624
+ mode: input.mode,
30625
+ redactedPrefix: input.redactedPrefix,
30450
30626
  startedAt: adoptStartedAt,
30451
30627
  outcome: "failed",
30452
30628
  failureReason: "auth_required"
30453
30629
  });
30454
- fail(
30630
+ throw new ComputerServiceError(
30455
30631
  "ADOPT_AUTH_REQUIRED",
30456
30632
  `Your Computer user session was rejected by the server before legacy adoption. Local legacy owner evidence for this key: ${formatLegacyOwnerEvidence(slockHome, ownerEvidence)}. Re-run \`slock-computer login\` for this server (use the same \`--server-url\` if not on production), then retry the adopt command. No local Computer state was written.`
30457
30633
  );
30458
30634
  }
30459
30635
  if (result.status === "not_authorized") {
30460
30636
  await appendAdoptionLog(slockHome, {
30461
- mode: legacy.mode,
30462
- redactedPrefix: legacy.redactedPrefix,
30637
+ mode: input.mode,
30638
+ redactedPrefix: input.redactedPrefix,
30463
30639
  startedAt: adoptStartedAt,
30464
30640
  outcome: "failed",
30465
30641
  failureReason: "not_authorized"
30466
30642
  });
30467
- fail(
30643
+ throw new ComputerServiceError(
30468
30644
  "ADOPT_NOT_AUTHORIZED",
30469
30645
  "Not authorized to adopt this machine on this server. Check that you are a current member."
30470
30646
  );
30471
30647
  }
30472
30648
  if (result.status === "unexpected_response") {
30473
30649
  await appendAdoptionLog(slockHome, {
30474
- mode: legacy.mode,
30475
- redactedPrefix: legacy.redactedPrefix,
30650
+ mode: input.mode,
30651
+ redactedPrefix: input.redactedPrefix,
30476
30652
  startedAt: adoptStartedAt,
30477
30653
  outcome: "failed",
30478
30654
  failureReason: result.code ? `unexpected_response_${result.code}` : "unexpected_response_missing_code"
30479
30655
  });
30480
30656
  const responseDetail = result.code ? `status ${result.httpStatus}, code ${result.code}` : `status ${result.httpStatus}, missing error code`;
30481
30657
  const authHint = result.httpStatus === 401 ? " If your server may be on an older release that does not emit `code: auth_required`, re-run `slock-computer login` first to refresh your user session, then retry. If the issue persists, report it as server contract drift." : "";
30482
- fail(
30658
+ throw new ComputerServiceError(
30483
30659
  "ADOPT_UNEXPECTED_RESPONSE",
30484
30660
  `Server returned an unexpected legacy adoption response (${responseDetail}).${authHint} Local legacy owner evidence for this key: ${formatLegacyOwnerEvidence(slockHome, ownerEvidence)}. Please report this with the command, server URL, and SLOCK_HOME; no local Computer state was written.`
30485
30661
  );
30486
30662
  }
30487
30663
  if (result.status === "error") {
30488
30664
  await appendAdoptionLog(slockHome, {
30489
- mode: legacy.mode,
30490
- redactedPrefix: legacy.redactedPrefix,
30665
+ mode: input.mode,
30666
+ redactedPrefix: input.redactedPrefix,
30491
30667
  startedAt: adoptStartedAt,
30492
30668
  outcome: "failed",
30493
30669
  failureReason: result.code
30494
30670
  });
30495
- fail(
30671
+ throw new ComputerServiceError(
30496
30672
  "ADOPT_FAILED",
30497
30673
  `Adoption failed at server exchange (${result.code}). Local legacy owner evidence for this key: ${formatLegacyOwnerEvidence(slockHome, ownerEvidence)}. Confirm the user session is valid, the legacy key belongs to this server/SLOCK_HOME, and the server URL is correct. No local Computer state was written.`
30498
30674
  );
30499
30675
  }
30500
- info(result.resumed ? "Adopted (resumed prior attachment); running preflight\u2026" : "Adopted; running preflight\u2026");
30676
+ emit3(options, { type: "preflight", resumed: result.resumed });
30677
+ options.signal?.throwIfAborted?.();
30501
30678
  const pre = await client.preflight(result.apiKey);
30502
30679
  if (!pre.ok) {
30503
30680
  await appendAdoptionLog(slockHome, {
30504
- mode: legacy.mode,
30505
- redactedPrefix: legacy.redactedPrefix,
30681
+ mode: input.mode,
30682
+ redactedPrefix: input.redactedPrefix,
30506
30683
  startedAt: adoptStartedAt,
30507
30684
  outcome: "failed",
30508
30685
  failureReason: `preflight_${pre.code}`
30509
30686
  });
30510
- fail(
30687
+ throw new ComputerServiceError(
30511
30688
  "PREFLIGHT_FAILED",
30512
30689
  `Server preflight failed (${pre.code}); local state not written. Upgrade the server or retry.`
30513
30690
  );
30514
30691
  }
30515
- const stop = await stopLegacyDaemonByOwnerFile(legacyOwnerFile);
30516
- if (stop.outcome === "timed_out" || stop.outcome === "denied" || stop.outcome === "error") {
30692
+ options.signal?.throwIfAborted?.();
30693
+ const stop2 = await stopLegacyDaemonByOwnerFile(legacyOwnerFile);
30694
+ if (stop2.outcome === "timed_out" || stop2.outcome === "denied" || stop2.outcome === "error") {
30517
30695
  await appendAdoptionLog(slockHome, {
30518
- mode: legacy.mode,
30519
- redactedPrefix: legacy.redactedPrefix,
30696
+ mode: input.mode,
30697
+ redactedPrefix: input.redactedPrefix,
30520
30698
  startedAt: adoptStartedAt,
30521
30699
  outcome: "failed",
30522
- failureReason: `legacy_stop_${stop.outcome}`,
30700
+ failureReason: `legacy_stop_${stop2.outcome}`,
30523
30701
  computerId: result.computerId,
30524
30702
  machineId: result.machineId,
30525
30703
  serverId: result.serverId,
30526
- legacyStop: stop
30704
+ legacyStop: stop2
30527
30705
  });
30528
- 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"})`;
30529
- fail(
30706
+ const detail = stop2.outcome === "timed_out" ? `pid ${stop2.pid} did not exit within ${LEGACY_STOP_WAIT_MS}ms` : stop2.outcome === "denied" ? `pid ${stop2.pid} cannot be stopped (permission denied)` : `stop attempt error (${stop2.reason ?? "unknown"})`;
30707
+ throw new ComputerServiceError(
30530
30708
  "LEGACY_DAEMON_STOP_FAILED",
30531
30709
  `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.`
30532
30710
  );
@@ -30554,33 +30732,38 @@ async function runAdoptLegacy(inputs) {
30554
30732
  );
30555
30733
  await chmod3(file, 384);
30556
30734
  await appendAdoptionLog(slockHome, {
30557
- mode: legacy.mode,
30558
- redactedPrefix: legacy.redactedPrefix,
30735
+ mode: input.mode,
30736
+ redactedPrefix: input.redactedPrefix,
30559
30737
  startedAt: adoptStartedAt,
30560
30738
  outcome: "succeeded",
30561
30739
  computerId: result.computerId,
30562
30740
  machineId: result.machineId,
30563
30741
  serverId: result.serverId,
30564
- legacyStop: stop
30742
+ legacyStop: stop2
30565
30743
  });
30566
- info(`Adopted. Computer state written to ${file}`);
30567
- info(` server: ${formatServerSlugDisplay(slugForServer)}`);
30568
- info(` serverMachine: ${result.computerId}`);
30569
- info(` legacyMachine: ${result.machineId}`);
30570
- switch (stop.outcome) {
30571
- case "absent":
30572
- info(" legacy daemon: not detected on this Computer (no local lock file)");
30573
- break;
30574
- case "already_dead":
30575
- info(` legacy daemon: already stopped (pid ${stop.pid} not running)`);
30576
- break;
30577
- case "stopped":
30578
- info(` legacy daemon: stopped (pid ${stop.pid}, SIGTERM)`);
30579
- break;
30580
- }
30581
- if (!inputs.orchestrated) {
30582
- info(`Next: run \`slock-computer start\` to bring this server online under the Computer supervisor.`);
30583
- }
30744
+ const apiKeyRedactedPrefix = result.apiKey.slice(0, 8);
30745
+ emit3(options, {
30746
+ type: "adopted",
30747
+ serverId: result.serverId,
30748
+ serverMachineId: result.computerId,
30749
+ legacyMachineId: result.machineId,
30750
+ serverSlug: slugForServer,
30751
+ attachmentPath: file,
30752
+ resumed: result.resumed,
30753
+ apiKeyRedactedPrefix,
30754
+ legacyStop: stop2
30755
+ });
30756
+ return {
30757
+ serverId: result.serverId,
30758
+ serverMachineId: result.computerId,
30759
+ legacyMachineId: result.machineId,
30760
+ serverSlug: slugForServer,
30761
+ serverUrl: baseUrl,
30762
+ attachmentPath: file,
30763
+ resumed: result.resumed,
30764
+ apiKeyRedactedPrefix,
30765
+ legacyStop: stop2
30766
+ };
30584
30767
  }
30585
30768
  async function appendAdoptionLog(slockHome, line) {
30586
30769
  try {
@@ -30616,24 +30799,53 @@ async function appendAdoptionLog(slockHome, line) {
30616
30799
  }
30617
30800
  }
30618
30801
 
30619
- // src/setup.ts
30620
- init_esm_shims();
30621
- var import_undici2 = __toESM(require_undici(), 1);
30622
- import { chmod as chmod4, mkdir as mkdir8, readdir as readdir3, readFile as readFile6, rename as rename3, rm as rm2, writeFile as writeFile7 } from "fs/promises";
30623
- import { dirname as dirname8, join as join3 } from "path";
30624
-
30625
30802
  // src/supervisor.ts
30626
30803
  init_esm_shims();
30627
30804
  import { spawn as spawn2 } from "child_process";
30628
- import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile6, open, unlink as unlink4, rename as rename2 } from "fs/promises";
30629
- import { dirname as dirname7, join as joinPath } from "path";
30805
+ import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile7, open, rename as rename2 } from "fs/promises";
30806
+ import { dirname as dirname8, join as joinPath } from "path";
30630
30807
  import { fileURLToPath as fileURLToPath2 } from "url";
30631
30808
 
30632
30809
  // src/cleanup.ts
30633
30810
  init_esm_shims();
30634
- import { readdir as readdir2, stat as stat2, unlink as unlink2, rm, rmdir, rename, mkdir as mkdir5 } from "fs/promises";
30811
+ import { readdir as readdir3, stat as stat2, unlink as unlink3, rm, rmdir, rename, mkdir as mkdir6 } from "fs/promises";
30635
30812
  import { spawn } from "child_process";
30636
- import { join as join2 } from "path";
30813
+ import { join as join3 } from "path";
30814
+
30815
+ // src/internal/process-primitives.ts
30816
+ init_esm_shims();
30817
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, unlink as unlink2 } from "fs/promises";
30818
+ import { dirname as dirname5 } from "path";
30819
+ async function readPidfileAt(pidfilePath) {
30820
+ try {
30821
+ const raw = (await readFile5(pidfilePath, "utf8")).trim();
30822
+ const pid = Number.parseInt(raw, 10);
30823
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
30824
+ } catch {
30825
+ return null;
30826
+ }
30827
+ }
30828
+ function isProcessAlive2(pid) {
30829
+ if (!Number.isInteger(pid) || pid <= 0) return false;
30830
+ try {
30831
+ process.kill(pid, 0);
30832
+ return true;
30833
+ } catch (err) {
30834
+ return err.code === "EPERM";
30835
+ }
30836
+ }
30837
+ async function writePidfileAt(pidfilePath, pid) {
30838
+ await mkdir5(dirname5(pidfilePath), { recursive: true });
30839
+ await writeFile5(pidfilePath, String(pid), { mode: 384 });
30840
+ }
30841
+ async function clearPidfileAt(pidfilePath) {
30842
+ try {
30843
+ await unlink2(pidfilePath);
30844
+ } catch {
30845
+ }
30846
+ }
30847
+
30848
+ // src/cleanup.ts
30637
30849
  function emptyCleanupReport() {
30638
30850
  return {
30639
30851
  stalePidfiles: [],
@@ -30649,7 +30861,7 @@ async function cleanupStalePidfile(pidfilePath) {
30649
30861
  if (pid === null) return false;
30650
30862
  if (isProcessAlive2(pid)) return false;
30651
30863
  try {
30652
- await unlink2(pidfilePath);
30864
+ await unlink3(pidfilePath);
30653
30865
  return true;
30654
30866
  } catch {
30655
30867
  return false;
@@ -30728,24 +30940,24 @@ async function cleanupOrphanProcesses(slockHome, psSpawn) {
30728
30940
  return signaled;
30729
30941
  }
30730
30942
  function quarantineDir(slockHome) {
30731
- return join2(computerDir(slockHome), ".quarantine");
30943
+ return join3(computerDir(slockHome), ".quarantine");
30732
30944
  }
30733
30945
  async function quarantineServerSubtree(slockHome, serverId) {
30734
- const src = join2(serversDir(slockHome), serverId);
30946
+ const src = join3(serversDir(slockHome), serverId);
30735
30947
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
30736
- const dest = join2(quarantineDir(slockHome), `${stamp}-${serverId}`);
30737
- await mkdir5(dirname5(dest), { recursive: true });
30948
+ const dest = join3(quarantineDir(slockHome), `${stamp}-${serverId}`);
30949
+ await mkdir6(dirname6(dest), { recursive: true });
30738
30950
  await rename(src, dest);
30739
30951
  return dest;
30740
30952
  }
30741
- function dirname5(p) {
30953
+ function dirname6(p) {
30742
30954
  const idx = p.lastIndexOf("/");
30743
30955
  return idx > 0 ? p.slice(0, idx) : "/";
30744
30956
  }
30745
30957
  async function cleanupPowerLossPartialState(slockHome) {
30746
30958
  let entries;
30747
30959
  try {
30748
- entries = await readdir2(serversDir(slockHome));
30960
+ entries = await readdir3(serversDir(slockHome));
30749
30961
  } catch {
30750
30962
  return [];
30751
30963
  }
@@ -30772,11 +30984,11 @@ var TMP_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
30772
30984
  async function cleanupTmpFiles(slockHome) {
30773
30985
  const removed = [];
30774
30986
  const cdir = computerDir(slockHome);
30775
- const stagingDir = join2(cdir, "upgrade-staging");
30987
+ const stagingDir = join3(cdir, "upgrade-staging");
30776
30988
  try {
30777
- const versions = await readdir2(stagingDir);
30989
+ const versions = await readdir3(stagingDir);
30778
30990
  for (const v of versions) {
30779
- const vdir = join2(stagingDir, v);
30991
+ const vdir = join3(stagingDir, v);
30780
30992
  try {
30781
30993
  const s = await stat2(vdir);
30782
30994
  if (Date.now() - s.mtimeMs > TMP_MAX_AGE_MS) {
@@ -30787,17 +30999,17 @@ async function cleanupTmpFiles(slockHome) {
30787
30999
  }
30788
31000
  }
30789
31001
  try {
30790
- const remaining = await readdir2(stagingDir);
31002
+ const remaining = await readdir3(stagingDir);
30791
31003
  if (remaining.length === 0) await rmdir(stagingDir);
30792
31004
  } catch {
30793
31005
  }
30794
31006
  } catch {
30795
31007
  }
30796
- const snap = join2(cdir, "upgrade-snapshot.json");
31008
+ const snap = join3(cdir, "upgrade-snapshot.json");
30797
31009
  try {
30798
31010
  const s = await stat2(snap);
30799
31011
  if (Date.now() - s.mtimeMs > TMP_MAX_AGE_MS) {
30800
- await unlink2(snap);
31012
+ await unlink3(snap);
30801
31013
  removed.push(snap);
30802
31014
  }
30803
31015
  } catch {
@@ -30805,7 +31017,7 @@ async function cleanupTmpFiles(slockHome) {
30805
31017
  return removed;
30806
31018
  }
30807
31019
  async function cleanupStaleLock(slockHome) {
30808
- const lockDir = join2(computerDir(slockHome), ".lock");
31020
+ const lockDir = join3(computerDir(slockHome), ".lock");
30809
31021
  try {
30810
31022
  const s = await stat2(lockDir);
30811
31023
  if (Date.now() - s.mtimeMs > 60 * 1e3) {
@@ -30817,7 +31029,7 @@ async function cleanupStaleLock(slockHome) {
30817
31029
  return [];
30818
31030
  }
30819
31031
  async function forceReleaseLock(slockHome) {
30820
- const lockDir = join2(computerDir(slockHome), ".lock");
31032
+ const lockDir = join3(computerDir(slockHome), ".lock");
30821
31033
  try {
30822
31034
  await stat2(lockDir);
30823
31035
  await rm(lockDir, { recursive: true, force: true });
@@ -30841,14 +31053,14 @@ async function runFullCleanup(slockHome, options = {}) {
30841
31053
 
30842
31054
  // src/health.ts
30843
31055
  init_esm_shims();
30844
- import { readFile as readFile4, writeFile as writeFile5, unlink as unlink3, mkdir as mkdir6 } from "fs/promises";
30845
- import { dirname as dirname6 } from "path";
31056
+ import { readFile as readFile6, writeFile as writeFile6, unlink as unlink4, mkdir as mkdir7, appendFile as appendFile2 } from "fs/promises";
31057
+ import { dirname as dirname7 } from "path";
30846
31058
  var CRASH_WINDOW_MS = 6e4;
30847
31059
  var DEGRADED_THRESHOLD = 3;
30848
31060
  async function readHealthFile(slockHome, serverId) {
30849
31061
  if (!isValidServerId(serverId)) return { crashes: [] };
30850
31062
  try {
30851
- const raw = await readFile4(serverHealthPath(slockHome, serverId), "utf8");
31063
+ const raw = await readFile6(serverHealthPath(slockHome, serverId), "utf8");
30852
31064
  const parsed = JSON.parse(raw);
30853
31065
  if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
30854
31066
  return parsed;
@@ -30859,8 +31071,8 @@ async function readHealthFile(slockHome, serverId) {
30859
31071
  }
30860
31072
  async function writeHealthFile(slockHome, serverId, file) {
30861
31073
  const path3 = serverHealthPath(slockHome, serverId);
30862
- await mkdir6(dirname6(path3), { recursive: true });
30863
- await writeFile5(path3, JSON.stringify(file), { mode: 384 });
31074
+ await mkdir7(dirname7(path3), { recursive: true });
31075
+ await writeFile6(path3, JSON.stringify(file), { mode: 384 });
30864
31076
  }
30865
31077
  async function recordCrash(slockHome, serverId, exitCode, signal, nowMs = Date.now()) {
30866
31078
  if (!isValidServerId(serverId)) return;
@@ -30909,47 +31121,401 @@ async function markFatalConfig(slockHome, serverId, exitCode, signal, nowMs = Da
30909
31121
  async function resetHealth(slockHome, serverId) {
30910
31122
  if (!isValidServerId(serverId)) return;
30911
31123
  try {
30912
- await unlink3(serverHealthPath(slockHome, serverId));
31124
+ await unlink4(serverHealthPath(slockHome, serverId));
30913
31125
  } catch {
30914
31126
  }
30915
31127
  }
30916
-
30917
- // src/supervisor.ts
30918
- async function readPidfileAt(pidfilePath) {
30919
- try {
30920
- const raw = (await readFile5(pidfilePath, "utf8")).trim();
30921
- const pid = Number.parseInt(raw, 10);
30922
- return Number.isInteger(pid) && pid > 0 ? pid : null;
31128
+ async function resetRunnerHealth(slockHome, serverId, nowMs = Date.now()) {
31129
+ if (!isValidServerId(serverId)) {
31130
+ return { status: "not-found" };
31131
+ }
31132
+ const wasDegraded = await isDegraded(slockHome, serverId, nowMs);
31133
+ const recent = await readCrashHistory(slockHome, serverId, nowMs);
31134
+ const previousState = wasDegraded ? "degraded" : "running";
31135
+ await resetHealth(slockHome, serverId);
31136
+ await emitRunnerStateTransition(
31137
+ slockHome,
31138
+ serverId,
31139
+ previousState,
31140
+ "running",
31141
+ "reset-runner"
31142
+ );
31143
+ return {
31144
+ status: "ok",
31145
+ previousState,
31146
+ clearedCrashCount: recent.length
31147
+ };
31148
+ }
31149
+ async function emitRunnerStateTransition(slockHome, serverId, fromState, toState, trigger) {
31150
+ const entry = {
31151
+ at: (/* @__PURE__ */ new Date()).toISOString(),
31152
+ kind: "runner-state-changed",
31153
+ serverId,
31154
+ fromState,
31155
+ toState,
31156
+ trigger
31157
+ };
31158
+ try {
31159
+ const path3 = supervisorLogPath(slockHome);
31160
+ await mkdir7(dirname7(path3), { recursive: true });
31161
+ await appendFile2(path3, JSON.stringify(entry) + "\n");
30923
31162
  } catch {
30924
- return null;
30925
31163
  }
30926
31164
  }
30927
- function isProcessAlive2(pid) {
30928
- if (!Number.isInteger(pid) || pid <= 0) return false;
31165
+
31166
+ // src/services/start.ts
31167
+ init_esm_shims();
31168
+ var START_ENSURE_TIMEOUT_MS = 15e3;
31169
+ var START_ENSURE_POLL_INTERVAL_MS = 100;
31170
+ function emit4(opts, event) {
31171
+ const cb = opts?.onEvent;
31172
+ if (!cb) return;
30929
31173
  try {
30930
- process.kill(pid, 0);
30931
- return true;
31174
+ cb(event);
31175
+ } catch {
31176
+ }
31177
+ }
31178
+ async function waitForManagedDaemonPids(slockHome, serverIds, opts) {
31179
+ const readPidfile = opts.readPidfile ?? readPidfileAt;
31180
+ const isAlive = opts.isProcessAlive ?? isProcessAlive2;
31181
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
31182
+ const timeoutMs = opts.ensureTimeoutMs ?? START_ENSURE_TIMEOUT_MS;
31183
+ const pollIntervalMs = opts.ensurePollIntervalMs ?? START_ENSURE_POLL_INTERVAL_MS;
31184
+ const deadline = Date.now() + timeoutMs;
31185
+ const ready = /* @__PURE__ */ new Map();
31186
+ while (ready.size < serverIds.length) {
31187
+ for (const serverId of serverIds) {
31188
+ if (ready.has(serverId)) continue;
31189
+ const pid = await readPidfile(serverDaemonPidPath(slockHome, serverId));
31190
+ if (pid && isAlive(pid)) ready.set(serverId, pid);
31191
+ }
31192
+ if (ready.size === serverIds.length) return ready;
31193
+ const remaining = deadline - Date.now();
31194
+ if (remaining <= 0) return ready;
31195
+ await sleep2(Math.min(pollIntervalMs, remaining));
31196
+ }
31197
+ return ready;
31198
+ }
31199
+ function buildTimeoutMessage(slockHome, serverIds, ready, input) {
31200
+ const missing = serverIds.filter((id) => !ready.has(id));
31201
+ const target = input.serverId && missing.length === 1 ? `${input.serverLabel ?? input.serverId}` : `${missing.length} daemon(s): ${missing.join(", ")}`;
31202
+ return `Timed out waiting for ${target} to start. Run \`slock-computer status\` and inspect ${supervisorLogPath(slockHome)} plus per-server daemon logs under ~/.slock/computer/servers/<serverId>/daemon.log.`;
31203
+ }
31204
+ async function start(input, options = {}) {
31205
+ options.signal?.throwIfAborted?.();
31206
+ const slockHome = resolveSlockHome();
31207
+ const attached = await listAttachedServerIds(slockHome);
31208
+ if (attached.length === 0) {
31209
+ throw new ComputerServiceError(
31210
+ "NO_ATTACHMENT",
31211
+ "No server attachments yet. Run `slock-computer attach <serverId>` first."
31212
+ );
31213
+ }
31214
+ if (input.serverId && !attached.includes(input.serverId)) {
31215
+ throw new ComputerServiceError(
31216
+ "NOT_ATTACHED",
31217
+ `Not attached to server ${input.serverId}. Run \`slock-computer attach ${input.serverId}\` first or omit the argument.`
31218
+ );
31219
+ }
31220
+ const managedTargets = input.serverId ? [input.serverId] : attached;
31221
+ emit4(options, {
31222
+ type: "starting",
31223
+ managedTargets,
31224
+ attachedCount: attached.length,
31225
+ foreground: !!input.foreground
31226
+ });
31227
+ for (const id of managedTargets) {
31228
+ await setServerManaged(slockHome, id);
31229
+ }
31230
+ const existing = await readPidfileAt(supervisorPidPath(slockHome));
31231
+ if (existing && isProcessAlive2(existing)) {
31232
+ emit4(options, {
31233
+ type: "already_running",
31234
+ supervisorPid: existing,
31235
+ managedTargets,
31236
+ attachedCount: attached.length
31237
+ });
31238
+ const ready2 = await waitForManagedDaemonPids(slockHome, managedTargets, options);
31239
+ if (ready2.size !== managedTargets.length) {
31240
+ throw new ComputerServiceError(
31241
+ "START_DAEMON_TIMEOUT",
31242
+ buildTimeoutMessage(slockHome, managedTargets, ready2, input)
31243
+ );
31244
+ }
31245
+ emit4(options, { type: "ready", ready: ready2, managedTargets });
31246
+ return {
31247
+ status: "already_running",
31248
+ managedTargets,
31249
+ attachedCount: attached.length,
31250
+ ready: ready2,
31251
+ supervisorPid: existing,
31252
+ supervisorLogPath: supervisorLogPath(slockHome)
31253
+ };
31254
+ }
31255
+ if (input.foreground) {
31256
+ emit4(options, {
31257
+ type: "running",
31258
+ managedTargets,
31259
+ attachedCount: attached.length
31260
+ });
31261
+ const supervise = options.runSupervise ?? runSupervise;
31262
+ const previousParentLockMarker = process.env[PARENT_LOCK_HELD_ENV_VAR];
31263
+ process.env[PARENT_LOCK_HELD_ENV_VAR] = "1";
31264
+ try {
31265
+ await supervise();
31266
+ } finally {
31267
+ if (previousParentLockMarker === void 0) delete process.env[PARENT_LOCK_HELD_ENV_VAR];
31268
+ else process.env[PARENT_LOCK_HELD_ENV_VAR] = previousParentLockMarker;
31269
+ }
31270
+ return {
31271
+ status: "running",
31272
+ managedTargets,
31273
+ attachedCount: attached.length,
31274
+ ready: /* @__PURE__ */ new Map(),
31275
+ supervisorPid: null,
31276
+ supervisorLogPath: supervisorLogPath(slockHome)
31277
+ };
31278
+ }
31279
+ options.signal?.throwIfAborted?.();
31280
+ let pid;
31281
+ try {
31282
+ pid = await (options.spawnDetachedSupervisor ?? spawnDetachedSupervisor)(slockHome);
30932
31283
  } catch (err) {
30933
- return err.code === "EPERM";
31284
+ const msg = err instanceof Error ? err.message : String(err);
31285
+ throw new ComputerServiceError("SUPERVISOR_SPAWN_FAILED", msg, err);
30934
31286
  }
31287
+ emit4(options, {
31288
+ type: "spawned",
31289
+ supervisorPid: pid,
31290
+ managedTargets,
31291
+ attachedCount: attached.length
31292
+ });
31293
+ if (options.signal?.aborted) {
31294
+ const ready2 = await pollReadyOnce(slockHome, managedTargets, options);
31295
+ emit4(options, { type: "aborted", supervisorPid: pid, managedTargets, ready: ready2 });
31296
+ return {
31297
+ status: "aborted",
31298
+ managedTargets,
31299
+ attachedCount: attached.length,
31300
+ ready: ready2,
31301
+ supervisorPid: pid,
31302
+ supervisorLogPath: supervisorLogPath(slockHome)
31303
+ };
31304
+ }
31305
+ const ready = await waitForManagedDaemonPids(slockHome, managedTargets, options);
31306
+ if (ready.size !== managedTargets.length) {
31307
+ throw new ComputerServiceError(
31308
+ "START_DAEMON_TIMEOUT",
31309
+ buildTimeoutMessage(slockHome, managedTargets, ready, input)
31310
+ );
31311
+ }
31312
+ emit4(options, { type: "ready", ready, managedTargets });
31313
+ return {
31314
+ status: "spawned",
31315
+ managedTargets,
31316
+ attachedCount: attached.length,
31317
+ ready,
31318
+ supervisorPid: pid,
31319
+ supervisorLogPath: supervisorLogPath(slockHome)
31320
+ };
30935
31321
  }
30936
- async function writePidfileAt(pidfilePath, pid) {
30937
- await mkdir7(dirname7(pidfilePath), { recursive: true });
30938
- await writeFile6(pidfilePath, String(pid), { mode: 384 });
31322
+ async function pollReadyOnce(slockHome, serverIds, opts) {
31323
+ const readPidfile = opts.readPidfile ?? readPidfileAt;
31324
+ const isAlive = opts.isProcessAlive ?? isProcessAlive2;
31325
+ const ready = /* @__PURE__ */ new Map();
31326
+ for (const serverId of serverIds) {
31327
+ const pid = await readPidfile(serverDaemonPidPath(slockHome, serverId));
31328
+ if (pid && isAlive(pid)) ready.set(serverId, pid);
31329
+ }
31330
+ return ready;
30939
31331
  }
30940
- async function clearPidfileAt(pidfilePath) {
31332
+
31333
+ // src/services/stop.ts
31334
+ init_esm_shims();
31335
+ var STOP_POLL_INTERVAL_MS = 200;
31336
+ var STOP_TIMEOUT_MS = 5e3;
31337
+ function emit5(opts, event) {
31338
+ const cb = opts?.onEvent;
31339
+ if (!cb) return;
30941
31340
  try {
30942
- await unlink4(pidfilePath);
31341
+ cb(event);
30943
31342
  } catch {
30944
31343
  }
30945
31344
  }
31345
+ async function stop(input = {}, options = {}) {
31346
+ void input;
31347
+ options.signal?.throwIfAborted?.();
31348
+ const slockHome = resolveSlockHome();
31349
+ const readPidfile = options.readPidfile ?? readPidfileAt;
31350
+ const isAlive = options.isProcessAlive ?? isProcessAlive2;
31351
+ const killer = options.killSupervisor ?? ((pid2) => {
31352
+ process.kill(pid2, "SIGTERM");
31353
+ });
31354
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
31355
+ const pollIntervalMs = options.pollIntervalMs ?? STOP_POLL_INTERVAL_MS;
31356
+ const timeoutMs = options.timeoutMs ?? STOP_TIMEOUT_MS;
31357
+ const pidfilePath = supervisorPidPath(slockHome);
31358
+ const pid = await readPidfile(pidfilePath);
31359
+ emit5(options, { type: "stopping", pid });
31360
+ if (pid === null) {
31361
+ emit5(options, { type: "not_running" });
31362
+ return { status: "not_running", pid: null, pidfilePath };
31363
+ }
31364
+ if (!isAlive(pid)) {
31365
+ await clearPidfileAt(pidfilePath);
31366
+ emit5(options, { type: "stale_pidfile_cleared", pid });
31367
+ return { status: "stale_pidfile_cleared", pid, pidfilePath };
31368
+ }
31369
+ options.signal?.throwIfAborted?.();
31370
+ try {
31371
+ killer(pid);
31372
+ } catch (err) {
31373
+ const cause = err instanceof Error ? err : new Error(String(err));
31374
+ throw new ComputerServiceError(
31375
+ "STOP_SIGNAL_FAILED",
31376
+ `Failed to send SIGTERM to supervisor (pid ${pid}): ${cause.message}. Check process permissions or run: kill ${pid}`,
31377
+ err
31378
+ );
31379
+ }
31380
+ emit5(options, { type: "signaled", pid });
31381
+ const deadline = Date.now() + timeoutMs;
31382
+ while (Date.now() < deadline) {
31383
+ options.signal?.throwIfAborted?.();
31384
+ if (!isAlive(pid)) {
31385
+ await clearPidfileAt(pidfilePath);
31386
+ emit5(options, { type: "stopped", pid });
31387
+ return { status: "stopped", pid, pidfilePath };
31388
+ }
31389
+ const remaining = deadline - Date.now();
31390
+ if (remaining <= 0) break;
31391
+ await sleep2(Math.min(pollIntervalMs, remaining));
31392
+ }
31393
+ throw new ComputerServiceError(
31394
+ "STOP_TIMEOUT",
31395
+ `Supervisor (pid ${pid}) did not exit within ${timeoutMs}ms after SIGTERM. Force-kill with: kill -9 ${pid}`
31396
+ );
31397
+ }
31398
+
31399
+ // src/services/detach.ts
31400
+ init_esm_shims();
31401
+ var import_undici2 = __toESM(require_undici(), 1);
31402
+ var REVOKE_TIMEOUT_MS = 3e3;
31403
+ function emit6(opts, event) {
31404
+ const cb = opts?.onEvent;
31405
+ if (!cb) return;
31406
+ try {
31407
+ cb(event);
31408
+ } catch {
31409
+ }
31410
+ }
31411
+ async function bestEffortServerRevoke(fetchImpl, serverUrl, apiKey, serverId, timeoutMs) {
31412
+ const url = `${serverUrl.replace(/\/$/, "")}/internal/computer/attachment/revoke`;
31413
+ const controller = new AbortController();
31414
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
31415
+ try {
31416
+ const res = await fetchImpl(url, {
31417
+ method: "POST",
31418
+ headers: {
31419
+ "content-type": "application/json",
31420
+ authorization: `Bearer ${apiKey}`
31421
+ },
31422
+ body: JSON.stringify({ reason: "computer_detach" }),
31423
+ signal: controller.signal
31424
+ });
31425
+ if (!res.ok) {
31426
+ return {
31427
+ kind: "http_error",
31428
+ status: res.status,
31429
+ message: `server-side revoke for server ${serverId} returned HTTP ${res.status}; local detach proceeds, but the credential may remain valid until it expires.`
31430
+ };
31431
+ }
31432
+ return { kind: "success" };
31433
+ } catch (err) {
31434
+ const m = err instanceof Error ? err.message : String(err);
31435
+ return {
31436
+ kind: "network_error",
31437
+ message: `server-side revoke for server ${serverId} failed: ${m}. Local detach proceeds, but the credential may remain valid until it expires.`
31438
+ };
31439
+ } finally {
31440
+ clearTimeout(timeout);
31441
+ }
31442
+ }
31443
+ async function detach(input, options = {}) {
31444
+ options.signal?.throwIfAborted?.();
31445
+ const serverId = assertValidServerId(input.serverId);
31446
+ const serverLabel = input.serverLabel ?? serverId;
31447
+ const slockHome = resolveSlockHome();
31448
+ const attachment = await readServerAttachment(slockHome, serverId);
31449
+ if (!attachment) {
31450
+ throw new ComputerServiceError(
31451
+ "NOT_ATTACHED",
31452
+ `Not attached to server ${serverLabel} (nothing to detach).`
31453
+ );
31454
+ }
31455
+ emit6(options, { type: "detaching", serverId, serverLabel });
31456
+ options.signal?.throwIfAborted?.();
31457
+ const fetchImpl = options.fetch ?? import_undici2.fetch;
31458
+ const timeoutMs = options.revokeTimeoutMs ?? REVOKE_TIMEOUT_MS;
31459
+ const outcome = await bestEffortServerRevoke(
31460
+ fetchImpl,
31461
+ attachment.serverUrl,
31462
+ attachment.apiKey,
31463
+ serverId,
31464
+ timeoutMs
31465
+ );
31466
+ let revokeOutcome;
31467
+ let revokeHttpStatus;
31468
+ if (outcome.kind === "success") {
31469
+ revokeOutcome = "success";
31470
+ emit6(options, { type: "revoke_succeeded", serverId, serverLabel });
31471
+ } else if (outcome.kind === "http_error") {
31472
+ revokeOutcome = "http_error";
31473
+ revokeHttpStatus = outcome.status;
31474
+ emit6(options, {
31475
+ type: "revoke_failed",
31476
+ serverId,
31477
+ serverLabel,
31478
+ reason: "http_error",
31479
+ httpStatus: outcome.status,
31480
+ message: outcome.message
31481
+ });
31482
+ } else {
31483
+ revokeOutcome = "network_error";
31484
+ emit6(options, {
31485
+ type: "revoke_failed",
31486
+ serverId,
31487
+ serverLabel,
31488
+ reason: "network_error",
31489
+ message: outcome.message
31490
+ });
31491
+ }
31492
+ options.signal?.throwIfAborted?.();
31493
+ await clearServerManaged(slockHome, serverId);
31494
+ const subtree = [
31495
+ serverAttachmentPath(slockHome, serverId),
31496
+ serverDaemonPidPath(slockHome, serverId),
31497
+ serverDaemonLogPath(slockHome, serverId)
31498
+ ];
31499
+ for (const p of subtree) await clearPidfileAt(p);
31500
+ emit6(options, { type: "subtree_cleared", serverId, serverLabel });
31501
+ emit6(options, { type: "detached", serverId, serverLabel });
31502
+ return {
31503
+ status: "detached",
31504
+ serverId,
31505
+ serverLabel,
31506
+ revokeOutcome,
31507
+ revokeHttpStatus
31508
+ };
31509
+ }
31510
+
31511
+ // src/supervisor.ts
30946
31512
  function buildResidentSpawn(mode, serverId, selfEntry = process.argv[1] ?? "", execArgv = process.execArgv) {
30947
31513
  const tail = serverId ? [mode, serverId] : [mode];
30948
31514
  return { command: process.execPath, args: [...execArgv, selfEntry, ...tail] };
30949
31515
  }
30950
31516
  var PARENT_LOCK_HELD_ENV_VAR = "SLOCK_COMPUTER_PARENT_MUTATION_LOCK_HELD";
30951
31517
  async function spawnDetachedSupervisor(slockHome) {
30952
- await mkdir7(computerDir(slockHome), { recursive: true });
31518
+ await mkdir8(computerDir(slockHome), { recursive: true });
30953
31519
  const supLogFd = await open(supervisorLogPath(slockHome), "a");
30954
31520
  const { command, args } = buildResidentSpawn("__supervise", null);
30955
31521
  const child = spawn2(command, args, {
@@ -31034,29 +31600,6 @@ async function runResident(serverId, deps = {}) {
31034
31600
  }
31035
31601
  var RECONCILE_INTERVAL_MS = 5e3;
31036
31602
  var CHILD_RESTART_BACKOFF_MS = 2e3;
31037
- var START_ENSURE_TIMEOUT_MS = 15e3;
31038
- var START_ENSURE_POLL_INTERVAL_MS = 100;
31039
- async function waitForManagedDaemonPids(slockHome, serverIds, deps) {
31040
- const readPidfile = deps.readPidfile ?? readPidfileAt;
31041
- const isAlive = deps.isProcessAlive ?? isProcessAlive2;
31042
- const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
31043
- const timeoutMs = deps.ensureTimeoutMs ?? START_ENSURE_TIMEOUT_MS;
31044
- const pollIntervalMs = deps.ensurePollIntervalMs ?? START_ENSURE_POLL_INTERVAL_MS;
31045
- const deadline = Date.now() + timeoutMs;
31046
- const ready = /* @__PURE__ */ new Map();
31047
- while (ready.size < serverIds.length) {
31048
- for (const serverId of serverIds) {
31049
- if (ready.has(serverId)) continue;
31050
- const pid = await readPidfile(serverDaemonPidPath(slockHome, serverId));
31051
- if (pid && isAlive(pid)) ready.set(serverId, pid);
31052
- }
31053
- if (ready.size === serverIds.length) return ready;
31054
- const remaining = deadline - Date.now();
31055
- if (remaining <= 0) return ready;
31056
- await sleep2(Math.min(pollIntervalMs, remaining));
31057
- }
31058
- return ready;
31059
- }
31060
31603
  function formatReadySummary(ready, serverIds, opts) {
31061
31604
  if (opts.serverId && serverIds.length === 1) {
31062
31605
  const pid = ready.get(opts.serverId);
@@ -31064,14 +31607,6 @@ function formatReadySummary(ready, serverIds, opts) {
31064
31607
  }
31065
31608
  return `Daemons for ${serverIds.length} managed server(s) are running.`;
31066
31609
  }
31067
- function failStartEnsureTimeout(slockHome, serverIds, ready, opts) {
31068
- const missing = serverIds.filter((id) => !ready.has(id));
31069
- const target = opts.serverId && missing.length === 1 ? `${opts.serverLabel ?? opts.serverId}` : `${missing.length} daemon(s): ${missing.join(", ")}`;
31070
- fail(
31071
- "START_DAEMON_TIMEOUT",
31072
- `Timed out waiting for ${target} to start. Run \`slock-computer status\` and inspect ${supervisorLogPath(slockHome)} plus per-server daemon logs under ~/.slock/computer/servers/<serverId>/daemon.log.`
31073
- );
31074
- }
31075
31610
  async function runSupervisorStartupRecovery(slockHome) {
31076
31611
  const parentHoldsLock = process.env[PARENT_LOCK_HELD_ENV_VAR] === "1";
31077
31612
  try {
@@ -31105,10 +31640,10 @@ async function runSupervisorStartupRecovery(slockHome) {
31105
31640
  }
31106
31641
  async function resolveSupervisorIdentity() {
31107
31642
  const here = fileURLToPath2(import.meta.url);
31108
- const installRoot = dirname7(dirname7(here));
31643
+ const installRoot = dirname8(dirname8(here));
31109
31644
  let version = null;
31110
31645
  try {
31111
- const raw = await readFile5(joinPath(installRoot, "package.json"), "utf8");
31646
+ const raw = await readFile7(joinPath(installRoot, "package.json"), "utf8");
31112
31647
  const parsed = JSON.parse(raw);
31113
31648
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
31114
31649
  version = parsed.version;
@@ -31128,7 +31663,7 @@ async function writeSupervisorVersionEvidence(slockHome) {
31128
31663
  };
31129
31664
  const dest = supervisorVersionPath(slockHome);
31130
31665
  const tmp = `${dest}.tmp`;
31131
- await writeFile6(tmp, JSON.stringify(payload) + "\n", { mode: 384 });
31666
+ await writeFile7(tmp, JSON.stringify(payload) + "\n", { mode: 384 });
31132
31667
  await rename2(tmp, dest);
31133
31668
  } catch (err) {
31134
31669
  const msg = err instanceof Error ? err.message : String(err);
@@ -31140,7 +31675,7 @@ async function writeSupervisorVersionEvidence(slockHome) {
31140
31675
  }
31141
31676
  async function readSupervisorVersionEvidence(slockHome) {
31142
31677
  try {
31143
- const raw = await readFile5(supervisorVersionPath(slockHome), "utf8");
31678
+ const raw = await readFile7(supervisorVersionPath(slockHome), "utf8");
31144
31679
  const parsed = JSON.parse(raw);
31145
31680
  if (typeof parsed.installRoot !== "string" || typeof parsed.pid !== "number" || typeof parsed.writtenAt !== "string" || parsed.version !== null && typeof parsed.version !== "string") {
31146
31681
  return null;
@@ -31157,7 +31692,7 @@ async function readSupervisorVersionEvidence(slockHome) {
31157
31692
  }
31158
31693
  async function runSupervise() {
31159
31694
  const slockHome = resolveSlockHome();
31160
- await mkdir7(computerDir(slockHome), { recursive: true });
31695
+ await mkdir8(computerDir(slockHome), { recursive: true });
31161
31696
  await runSupervisorStartupRecovery(slockHome);
31162
31697
  await writePidfileAt(supervisorPidPath(slockHome), process.pid);
31163
31698
  await writeSupervisorVersionEvidence(slockHome);
@@ -31165,7 +31700,7 @@ async function runSupervise() {
31165
31700
  const { [PARENT_LOCK_HELD_ENV_VAR]: _parentLockMarker, ...childEnv } = process.env;
31166
31701
  const spawnChild = async (serverId) => {
31167
31702
  const logPath = serverDaemonLogPath(slockHome, serverId);
31168
- await mkdir7(dirname7(logPath), { recursive: true });
31703
+ await mkdir8(dirname8(logPath), { recursive: true });
31169
31704
  const logFd = await open(logPath, "a");
31170
31705
  const { command, args } = buildResidentSpawn("__run", serverId);
31171
31706
  const child = spawn2(command, args, {
@@ -31254,157 +31789,127 @@ async function runSupervise() {
31254
31789
  });
31255
31790
  }
31256
31791
  async function runStart(opts = {}, deps = {}) {
31257
- const slockHome = resolveSlockHome();
31258
- const attached = await listAttachedServerIds(slockHome);
31259
- if (attached.length === 0) {
31260
- fail(
31261
- "NO_ATTACHMENT",
31262
- "No server attachments yet. Run `slock-computer attach <serverId>` first."
31263
- );
31264
- }
31265
- if (opts.serverId && !attached.includes(opts.serverId)) {
31266
- fail(
31267
- "NOT_ATTACHED",
31268
- `Not attached to server ${opts.serverId}. Run \`slock-computer attach ${opts.serverId}\` first or omit the argument.`
31269
- );
31270
- }
31271
- const managedTargets = opts.serverId ? [opts.serverId] : attached;
31272
- for (const id of managedTargets) {
31273
- await setServerManaged(slockHome, id);
31274
- }
31275
- const existing = await readPidfileAt(supervisorPidPath(slockHome));
31276
- if (existing && isProcessAlive2(existing)) {
31277
- info(`Supervisor already running (pid ${existing}).`);
31278
- const ready2 = await waitForManagedDaemonPids(slockHome, managedTargets, deps);
31279
- if (ready2.size !== managedTargets.length) {
31280
- failStartEnsureTimeout(slockHome, managedTargets, ready2, opts);
31281
- }
31282
- info(formatReadySummary(ready2, managedTargets, opts));
31283
- return;
31284
- }
31285
- if (opts.foreground) {
31286
- info(
31287
- `Running supervisor in the foreground (managing ${managedTargets.length} of ${attached.length} attached server(s)). Ctrl-C to stop.`
31288
- );
31289
- await runSupervise();
31290
- return;
31291
- }
31292
- let pid;
31792
+ let spawnedBackground = null;
31293
31793
  try {
31294
- pid = await (deps.spawnDetachedSupervisor ?? spawnDetachedSupervisor)(slockHome);
31295
- } catch (err) {
31296
- const msg = err instanceof Error ? err.message : String(err);
31297
- fail("SUPERVISOR_SPAWN_FAILED", msg);
31298
- }
31299
- info(`Supervisor started (pid ${pid}); keeps running after this terminal closes.`);
31300
- const ready = await waitForManagedDaemonPids(slockHome, managedTargets, deps);
31301
- if (ready.size !== managedTargets.length) {
31302
- failStartEnsureTimeout(slockHome, managedTargets, ready, opts);
31303
- }
31304
- info(formatReadySummary(ready, managedTargets, opts));
31305
- info(
31306
- `Managing ${managedTargets.length} of ${attached.length} attached server(s). Logs: ${supervisorLogPath(slockHome)}`
31307
- );
31308
- info(`Per-server daemon logs: ~/.slock/computer/servers/<serverId>/daemon.log`);
31309
- info(`Check state with \`slock-computer status\`.`);
31310
- }
31311
- async function runStop(deps = {}) {
31312
- const slockHome = resolveSlockHome();
31313
- const readPidfile = deps.readPidfile ?? readPidfileAt;
31314
- const isAlive = deps.isProcessAlive ?? isProcessAlive2;
31315
- const killer = deps.killSupervisor ?? ((pid2) => {
31316
- process.kill(pid2, "SIGTERM");
31317
- });
31318
- const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
31319
- const pollIntervalMs = deps.pollIntervalMs ?? 200;
31320
- const timeoutMs = deps.timeoutMs ?? 5e3;
31321
- const pidfilePath = supervisorPidPath(slockHome);
31322
- const pid = await readPidfile(pidfilePath);
31323
- if (pid === null) {
31324
- info("Supervisor not running.");
31325
- return;
31326
- }
31327
- if (!isAlive(pid)) {
31328
- await clearPidfileAt(pidfilePath);
31329
- info(`Supervisor not running (cleared stale pidfile for pid ${pid}).`);
31330
- return;
31331
- }
31332
- try {
31333
- killer(pid);
31334
- } catch (err) {
31335
- fail(
31336
- "STOP_SIGNAL_FAILED",
31337
- `Failed to send SIGTERM to supervisor (pid ${pid}): ${err instanceof Error ? err.message : String(err)}. Check process permissions or run: kill ${pid}`
31794
+ await start(
31795
+ {
31796
+ foreground: opts.foreground,
31797
+ serverId: opts.serverId ?? null,
31798
+ serverLabel: opts.serverLabel ?? null
31799
+ },
31800
+ {
31801
+ spawnDetachedSupervisor: deps.spawnDetachedSupervisor,
31802
+ readPidfile: deps.readPidfile,
31803
+ isProcessAlive: deps.isProcessAlive,
31804
+ sleep: deps.sleep,
31805
+ ensureTimeoutMs: deps.ensureTimeoutMs,
31806
+ ensurePollIntervalMs: deps.ensurePollIntervalMs,
31807
+ onEvent: (event) => {
31808
+ if (event.type === "already_running") {
31809
+ info(`Supervisor already running (pid ${event.supervisorPid}).`);
31810
+ } else if (event.type === "running") {
31811
+ info(
31812
+ `Running supervisor in the foreground (managing ${event.managedTargets.length} of ${event.attachedCount} attached server(s)). Ctrl-C to stop.`
31813
+ );
31814
+ } else if (event.type === "spawned") {
31815
+ info(`Supervisor started (pid ${event.supervisorPid}); keeps running after this terminal closes.`);
31816
+ spawnedBackground = {
31817
+ managedCount: event.managedTargets.length,
31818
+ attachedCount: event.attachedCount,
31819
+ logPath: supervisorLogPath(resolveSlockHome())
31820
+ };
31821
+ } else if (event.type === "ready") {
31822
+ info(formatReadySummary(event.ready, event.managedTargets, opts));
31823
+ }
31824
+ }
31825
+ }
31338
31826
  );
31339
- }
31340
- const deadline = Date.now() + timeoutMs;
31341
- while (Date.now() < deadline) {
31342
- if (!isAlive(pid)) {
31343
- await clearPidfileAt(pidfilePath);
31344
- info(`Stopped supervisor (pid ${pid}).`);
31345
- return;
31827
+ } catch (err) {
31828
+ if (err instanceof CliExit) throw err;
31829
+ if (err instanceof ComputerServiceError) {
31830
+ fail(err.code, err.message);
31346
31831
  }
31347
- await sleep2(pollIntervalMs);
31348
- }
31349
- fail(
31350
- "STOP_TIMEOUT",
31351
- `Supervisor (pid ${pid}) did not exit within ${timeoutMs}ms after SIGTERM. Force-kill with: kill -9 ${pid}`
31352
- );
31353
- }
31354
- async function runDetach(serverId, serverLabel = serverId) {
31355
- assertValidServerId(serverId);
31356
- const slockHome = resolveSlockHome();
31357
- const a = await readServerAttachment(slockHome, serverId);
31358
- if (!a) {
31359
- fail("NOT_ATTACHED", `Not attached to server ${serverLabel} (nothing to detach).`);
31832
+ throw err;
31360
31833
  }
31361
- await bestEffortServerRevoke(a.serverUrl, a.apiKey, serverId);
31362
- await clearServerManaged(slockHome, serverId);
31363
- const subtree = [
31364
- serverAttachmentPath(slockHome, serverId),
31365
- serverDaemonPidPath(slockHome, serverId),
31366
- serverDaemonLogPath(slockHome, serverId)
31367
- ];
31368
- for (const p of subtree) await clearPidfileAt(p);
31369
- info(`Detached from server ${serverLabel}.`);
31370
- info(`The supervisor (if running) will stop that server's daemon on its next reconcile tick.`);
31371
- }
31372
- async function bestEffortServerRevoke(serverUrl, apiKey, serverId) {
31373
- const url = `${serverUrl.replace(/\/$/, "")}/internal/computer/attachment/revoke`;
31374
- const controller = new AbortController();
31375
- const timeout = setTimeout(() => controller.abort(), 3e3);
31376
- try {
31377
- const res = await fetch(url, {
31378
- method: "POST",
31379
- headers: {
31380
- "content-type": "application/json",
31381
- authorization: `Bearer ${apiKey}`
31382
- },
31383
- body: JSON.stringify({ reason: "computer_detach" }),
31384
- signal: controller.signal
31385
- });
31386
- if (!res.ok) {
31387
- process.stderr.write(
31388
- `Warning: server-side revoke for server ${serverId} returned HTTP ${res.status}; local detach proceeds, but the credential may remain valid until it expires.
31389
- `
31390
- );
31391
- }
31834
+ if (spawnedBackground) {
31835
+ const sb = spawnedBackground;
31836
+ info(
31837
+ `Managing ${sb.managedCount} of ${sb.attachedCount} attached server(s). Logs: ${sb.logPath}`
31838
+ );
31839
+ info(`Per-server daemon logs: ~/.slock/computer/servers/<serverId>/daemon.log`);
31840
+ info(`Check state with \`slock-computer status\`.`);
31841
+ }
31842
+ }
31843
+ async function runStop(deps = {}) {
31844
+ try {
31845
+ await stop(
31846
+ {},
31847
+ {
31848
+ readPidfile: deps.readPidfile,
31849
+ isProcessAlive: deps.isProcessAlive,
31850
+ killSupervisor: deps.killSupervisor,
31851
+ sleep: deps.sleep,
31852
+ pollIntervalMs: deps.pollIntervalMs,
31853
+ timeoutMs: deps.timeoutMs,
31854
+ onEvent: (event) => {
31855
+ if (event.type === "not_running") {
31856
+ info("Supervisor not running.");
31857
+ } else if (event.type === "stale_pidfile_cleared") {
31858
+ info(`Supervisor not running (cleared stale pidfile for pid ${event.pid}).`);
31859
+ } else if (event.type === "stopped") {
31860
+ info(`Stopped supervisor (pid ${event.pid}).`);
31861
+ }
31862
+ }
31863
+ }
31864
+ );
31392
31865
  } catch (err) {
31393
- const msg = err instanceof Error ? err.message : String(err);
31394
- process.stderr.write(
31395
- `Warning: server-side revoke for server ${serverId} failed: ${msg}. Local detach proceeds, but the credential may remain valid until it expires.
31396
- `
31866
+ if (err instanceof CliExit) throw err;
31867
+ if (err instanceof ComputerServiceError) {
31868
+ fail(err.code, err.message);
31869
+ }
31870
+ throw err;
31871
+ }
31872
+ }
31873
+ async function runDetach(serverId, serverLabel = serverId) {
31874
+ try {
31875
+ await detach(
31876
+ { serverId, serverLabel },
31877
+ {
31878
+ onEvent: (event) => {
31879
+ if (event.type === "revoke_failed") {
31880
+ process.stderr.write(`Warning: ${event.message}
31881
+ `);
31882
+ } else if (event.type === "detached") {
31883
+ info(`Detached from server ${event.serverLabel}.`);
31884
+ info(
31885
+ `The supervisor (if running) will stop that server's daemon on its next reconcile tick.`
31886
+ );
31887
+ }
31888
+ }
31889
+ }
31397
31890
  );
31398
- } finally {
31399
- clearTimeout(timeout);
31891
+ } catch (err) {
31892
+ if (err instanceof CliExit) throw err;
31893
+ if (err instanceof ComputerServiceError) {
31894
+ fail(err.code, err.message);
31895
+ }
31896
+ throw err;
31400
31897
  }
31401
31898
  }
31402
31899
 
31403
31900
  // src/setup.ts
31404
31901
  var USER_SESSION_EXPIRY_LEEWAY_MS = 3e4;
31902
+ async function readUserSessionUserId(slockHome) {
31903
+ try {
31904
+ const parsed = JSON.parse(await readFile8(userSessionPath(slockHome), "utf8"));
31905
+ return typeof parsed.userId === "string" ? parsed.userId : "";
31906
+ } catch {
31907
+ return "";
31908
+ }
31909
+ }
31405
31910
  async function hasValidUserSession(slockHome) {
31406
31911
  try {
31407
- const parsed = JSON.parse(await readFile6(userSessionPath(slockHome), "utf8"));
31912
+ const parsed = JSON.parse(await readFile8(userSessionPath(slockHome), "utf8"));
31408
31913
  return parsed.kind === "user-session" && typeof parsed.accessToken === "string" && parsed.accessToken.length > 0 && !isJwtExpired(parsed.accessToken);
31409
31914
  } catch {
31410
31915
  return false;
@@ -31424,7 +31929,7 @@ async function refreshUserSession(slockHome, serverUrl) {
31424
31929
  const file = userSessionPath(slockHome);
31425
31930
  let session;
31426
31931
  try {
31427
- session = JSON.parse(await readFile6(file, "utf8"));
31932
+ session = JSON.parse(await readFile8(file, "utf8"));
31428
31933
  } catch {
31429
31934
  return false;
31430
31935
  }
@@ -31434,7 +31939,7 @@ async function refreshUserSession(slockHome, serverUrl) {
31434
31939
  const baseUrl = resolveServerUrl(serverUrl, session.serverUrl, process.env.SLOCK_SERVER_URL);
31435
31940
  const tmpFile = `${file}.${process.pid}.${Date.now()}.tmp`;
31436
31941
  try {
31437
- const res = await (0, import_undici2.fetch)(new URL("/api/auth/refresh", baseUrl).toString(), {
31942
+ const res = await (0, import_undici3.fetch)(new URL("/api/auth/refresh", baseUrl).toString(), {
31438
31943
  method: "POST",
31439
31944
  headers: { "Content-Type": "application/json" },
31440
31945
  body: JSON.stringify({ refreshToken: session.refreshToken })
@@ -31443,8 +31948,8 @@ async function refreshUserSession(slockHome, serverUrl) {
31443
31948
  if (res.status !== 200 || typeof body?.accessToken !== "string" || typeof body.refreshToken !== "string") {
31444
31949
  return false;
31445
31950
  }
31446
- await mkdir8(dirname8(file), { recursive: true });
31447
- await writeFile7(
31951
+ await mkdir9(dirname9(file), { recursive: true });
31952
+ await writeFile8(
31448
31953
  tmpFile,
31449
31954
  JSON.stringify(
31450
31955
  {
@@ -31469,49 +31974,74 @@ async function refreshUserSession(slockHome, serverUrl) {
31469
31974
  return false;
31470
31975
  }
31471
31976
  }
31472
- async function hasLiveLegacyDaemon(slockHome) {
31473
- let machineDirs;
31474
- try {
31475
- machineDirs = await readdir3(join3(slockHome, "machines"));
31476
- } catch {
31477
- return false;
31478
- }
31479
- for (const name of machineDirs) {
31480
- if (!name.startsWith("machine-")) continue;
31481
- try {
31482
- const raw = await readFile6(join3(slockHome, "machines", name, "daemon.lock", "owner.json"), "utf8");
31483
- const owner = JSON.parse(raw);
31484
- if (typeof owner.pid === "number" && isProcessAlive2(owner.pid)) return true;
31485
- } catch {
31486
- }
31487
- }
31488
- return false;
31977
+ async function defaultConfirmMigrate(prompt) {
31978
+ process.stdout.write(prompt);
31979
+ const line = await readLine(false);
31980
+ const norm = line.trim().toLowerCase();
31981
+ return norm === "y" || norm === "yes";
31489
31982
  }
31490
- function hasExplicitLegacyKeyInput(opts) {
31491
- return typeof opts.legacyApiKey === "string" && opts.legacyApiKey.length > 0 || typeof opts.legacyApiKeyFile === "string" && opts.legacyApiKeyFile.length > 0 || opts.legacyApiKeyStdin === true;
31983
+ async function defaultReadMigrateSecret(prompt) {
31984
+ process.stdout.write(prompt);
31985
+ const line = await readLine(true);
31986
+ process.stdout.write("\n");
31987
+ return line.trim();
31492
31988
  }
31493
- function hasAmbientLegacyKeyInput() {
31494
- return typeof process.env.SLOCK_LEGACY_API_KEY === "string" && process.env.SLOCK_LEGACY_API_KEY.trim().length > 0;
31989
+ async function readLine(masked) {
31990
+ const stdin = process.stdin;
31991
+ const wasRaw = stdin.isTTY === true && stdin.isRaw === true;
31992
+ const enterRaw = masked && stdin.isTTY === true && typeof stdin.setRawMode === "function";
31993
+ if (enterRaw) stdin.setRawMode(true);
31994
+ stdin.resume();
31995
+ return await new Promise((resolve) => {
31996
+ let buf = "";
31997
+ const cleanup = () => {
31998
+ stdin.off("data", onData);
31999
+ stdin.off("end", onEnd);
32000
+ stdin.pause();
32001
+ if (enterRaw) stdin.setRawMode(wasRaw);
32002
+ };
32003
+ const onData = (chunk) => {
32004
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
32005
+ for (const ch of text) {
32006
+ if (ch === "\n" || ch === "\r") {
32007
+ cleanup();
32008
+ resolve(buf);
32009
+ return;
32010
+ }
32011
+ if (ch === "") {
32012
+ cleanup();
32013
+ resolve("");
32014
+ return;
32015
+ }
32016
+ if (ch === "\x7F" || ch === "\b") {
32017
+ buf = buf.slice(0, -1);
32018
+ continue;
32019
+ }
32020
+ buf += ch;
32021
+ }
32022
+ };
32023
+ const onEnd = () => {
32024
+ cleanup();
32025
+ resolve(buf);
32026
+ };
32027
+ stdin.on("data", onData);
32028
+ stdin.on("end", onEnd);
32029
+ });
31495
32030
  }
31496
32031
  async function runSetup(opts, deps = {}) {
31497
32032
  const slockHome = resolveSlockHome();
31498
32033
  const isTty = deps.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
31499
- const login = deps.runLogin ?? runLogin;
31500
- const attach = deps.runAttach ?? runAttach;
31501
- const adopt = deps.runAdoptLegacy ?? runAdoptLegacy;
31502
- const start = deps.runStart ?? runStart;
32034
+ const login2 = deps.runLogin ?? runLogin;
32035
+ const attach2 = deps.runAttach ?? runAttach;
32036
+ const start2 = deps.runStart ?? runStart;
31503
32037
  const refreshSession = deps.refreshUserSession ?? refreshUserSession;
31504
- const legacyDaemonCheck = deps.hasLiveLegacyDaemon ?? hasLiveLegacyDaemon;
32038
+ const detectMigration = deps.detectLegacyMigration ?? detectLegacyMigration;
32039
+ const confirmMigrate = deps.confirmMigrate ?? defaultConfirmMigrate;
32040
+ const readMigrateSecret = deps.readMigrateSecret ?? defaultReadMigrateSecret;
31505
32041
  if (!isTty && !opts.yes) {
31506
32042
  fail(
31507
32043
  "NON_INTERACTIVE_SETUP_REQUIRES_FLAGS",
31508
- "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."
31509
- );
31510
- }
31511
- if (!opts.adoptLegacy && hasExplicitLegacyKeyInput(opts)) {
31512
- fail(
31513
- "LEGACY_KEY_OUTSIDE_ADOPT",
31514
- "`--legacy-api-key*` options are only accepted with `slock-computer setup --adopt-legacy` or `slock-computer adopt-legacy`."
32044
+ "Non-interactive setup requires --yes after you have confirmed the login/attach/start actions. Run `slock-computer login`, `slock-computer attach`, and `slock-computer start` separately for fully explicit automation."
31515
32045
  );
31516
32046
  }
31517
32047
  const label = formatServerSlugDisplay(opts.serverSlug);
@@ -31527,7 +32057,7 @@ async function runSetup(opts, deps = {}) {
31527
32057
  );
31528
32058
  }
31529
32059
  info("User session: missing or expired; starting login.");
31530
- await login({ serverUrl: opts.serverUrl, orchestrated: true });
32060
+ await login2({ serverUrl: opts.serverUrl, orchestrated: true });
31531
32061
  }
31532
32062
  } else {
31533
32063
  info("User session: already logged in.");
@@ -31536,35 +32066,83 @@ async function runSetup(opts, deps = {}) {
31536
32066
  if (attachment) {
31537
32067
  info(`Attachment: already attached to ${label}.`);
31538
32068
  } else {
31539
- const liveLegacy = await legacyDaemonCheck(slockHome);
31540
- const explicitLegacyKey = hasExplicitLegacyKeyInput(opts);
31541
- const hasLegacyKey = explicitLegacyKey || opts.adoptLegacy === true && hasAmbientLegacyKeyInput();
31542
- if (!opts.adoptLegacy && liveLegacy) {
31543
- fail(
31544
- "LEGACY_DAEMON_RUNNING",
31545
- `A legacy daemon appears to be running in ${slockHome}, so Computer setup stopped before creating a second attachment. To proceed: try the Computer beta in an isolated home with \`SLOCK_HOME=$HOME/.slock-computer-beta slock-computer setup ${label}\`; or stop the legacy daemon with \`pkill -f slock-daemon && rm -f ~/.slock/daemon.pid\`; or migrate the existing daemon with \`--adopt-legacy --legacy-api-key <key>\`.`
31546
- );
31547
- }
31548
- if (opts.adoptLegacy && hasLegacyKey) {
31549
- await adopt({
31550
- serverSlug: opts.serverSlug,
31551
- serverUrl: opts.serverUrl,
31552
- name: opts.name,
31553
- legacyApiKey: opts.legacyApiKey,
31554
- legacyApiKeyFile: opts.legacyApiKeyFile,
31555
- legacyApiKeyStdin: opts.legacyApiKeyStdin,
31556
- orchestrated: true
31557
- });
31558
- } else if (opts.adoptLegacy && liveLegacy) {
31559
- fail(
31560
- "LEGACY_DAEMON_RUNNING",
31561
- `A legacy daemon is running in ${slockHome}, but no legacy key was provided. To proceed: provide --legacy-api-key-file/--legacy-api-key/--legacy-api-key-stdin to adopt it; or stop the legacy daemon with \`pkill -f slock-daemon && rm -f ~/.slock/daemon.pid\`; or use an isolated home with \`SLOCK_HOME=$HOME/.slock-computer-beta slock-computer setup ${label}\` for a clean beta trial.`
31562
- );
31563
- } else {
31564
- if (opts.adoptLegacy) {
31565
- info("Legacy key: not provided; falling back to fresh device-login attach.");
32069
+ let migrated = false;
32070
+ if (isTty) {
32071
+ const detection = await detectMigration(slockHome, await readUserSessionUserId(slockHome));
32072
+ if (detection.kind === "match") {
32073
+ const where = detection.serverUrl ? ` attached to ${detection.serverUrl}` : "";
32074
+ const who = detection.machineName ? ` "${detection.machineName}"` : "";
32075
+ const accepted = await confirmMigrate(
32076
+ `Migration: detected legacy daemon machine${who}${where}. Migrate it under Computer instead of creating a fresh attachment? [y/N] `
32077
+ );
32078
+ if (accepted) {
32079
+ const rawKey = await readMigrateSecret(
32080
+ "Paste legacy api key (sk_machine_* or sk_daemon_*); input is hidden: "
32081
+ );
32082
+ if (rawKey.length === 0) {
32083
+ info("Migration: no key provided; falling back to fresh attach.");
32084
+ } else if (!rawKey.startsWith("sk_machine_") && !rawKey.startsWith("sk_daemon_")) {
32085
+ fail(
32086
+ "LEGACY_KEY_INVALID",
32087
+ "Provided key does not look like a legacy machine key (expected `sk_machine_*` or `sk_daemon_*`)."
32088
+ );
32089
+ } else {
32090
+ try {
32091
+ await adoptLegacy(
32092
+ {
32093
+ serverSlug: opts.serverSlug,
32094
+ serverUrl: opts.serverUrl,
32095
+ name: opts.name,
32096
+ rawKey,
32097
+ mode: "legacy_key_stdin",
32098
+ redactedPrefix: rawKey.slice(0, 8)
32099
+ },
32100
+ {
32101
+ onEvent: (event) => {
32102
+ if (event.type === "adopting") {
32103
+ info(
32104
+ `Adopting legacy daemon for ${formatServerSlugDisplay(event.serverSlug)} via ${event.mode}\u2026`
32105
+ );
32106
+ } else if (event.type === "preflight") {
32107
+ info(
32108
+ event.resumed ? "Adopted (resumed prior attachment); running preflight\u2026" : "Adopted; running preflight\u2026"
32109
+ );
32110
+ } else if (event.type === "adopted") {
32111
+ info(`Adopted. Computer state written to ${event.attachmentPath}`);
32112
+ info(` server: ${formatServerSlugDisplay(event.serverSlug)}`);
32113
+ info(` serverMachine: ${event.serverMachineId}`);
32114
+ info(` legacyMachine: ${event.legacyMachineId}`);
32115
+ switch (event.legacyStop.outcome) {
32116
+ case "absent":
32117
+ info(" legacy daemon: not detected on this Computer (no local lock file)");
32118
+ break;
32119
+ case "already_dead":
32120
+ info(` legacy daemon: already stopped (pid ${event.legacyStop.pid} not running)`);
32121
+ break;
32122
+ case "stopped":
32123
+ info(` legacy daemon: stopped (pid ${event.legacyStop.pid}, SIGTERM)`);
32124
+ break;
32125
+ }
32126
+ }
32127
+ }
32128
+ }
32129
+ );
32130
+ migrated = true;
32131
+ } catch (err) {
32132
+ if (err instanceof CliExit) throw err;
32133
+ if (err instanceof ComputerServiceError) {
32134
+ fail(err.code, err.message);
32135
+ }
32136
+ throw err;
32137
+ }
32138
+ }
32139
+ } else {
32140
+ info("Migration: declined; falling back to fresh attach.");
32141
+ }
31566
32142
  }
31567
- await attach({
32143
+ }
32144
+ if (!migrated) {
32145
+ await attach2({
31568
32146
  serverSlug: opts.serverSlug,
31569
32147
  serverUrl: opts.serverUrl,
31570
32148
  name: opts.name,
@@ -31576,7 +32154,7 @@ async function runSetup(opts, deps = {}) {
31576
32154
  if (!attachment) {
31577
32155
  fail(
31578
32156
  "SETUP_ATTACHMENT_MISSING",
31579
- `Setup did not produce a local attachment for ${label}. Re-run \`slock-computer attach ${label}\` or \`slock-computer adopt-legacy ${label}\`.`
32157
+ `Setup did not produce a local attachment for ${label}. Re-run \`slock-computer attach ${label}\`.`
31580
32158
  );
31581
32159
  }
31582
32160
  }
@@ -31584,7 +32162,7 @@ async function runSetup(opts, deps = {}) {
31584
32162
  info(`Start: skipped (--no-start). Run \`slock-computer start ${label}\` when ready.`);
31585
32163
  return;
31586
32164
  }
31587
- await start({
32165
+ await start2({
31588
32166
  serverId: attachment.serverId,
31589
32167
  serverLabel: opts.serverSlug,
31590
32168
  foreground: opts.foreground
@@ -31593,10 +32171,10 @@ async function runSetup(opts, deps = {}) {
31593
32171
 
31594
32172
  // src/status.ts
31595
32173
  init_esm_shims();
31596
- import { readFile as readFile7 } from "fs/promises";
32174
+ import { readFile as readFile9 } from "fs/promises";
31597
32175
  async function readUserSession(path3) {
31598
32176
  try {
31599
- const parsed = JSON.parse(await readFile7(path3, "utf8"));
32177
+ const parsed = JSON.parse(await readFile9(path3, "utf8"));
31600
32178
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
31601
32179
  return { state: "present", session: parsed, error: null };
31602
32180
  }
@@ -31624,32 +32202,31 @@ async function deriveHealth(slockHome, serverId, daemon) {
31624
32202
  if (await isDegraded(slockHome, serverId)) return "degraded";
31625
32203
  return "ok";
31626
32204
  }
31627
- async function buildStatusReport() {
31628
- const slockHome = resolveSlockHome();
31629
- const sessionRead = await readUserSession(userSessionPath(slockHome));
32205
+ async function buildStatusReport(installRoot) {
32206
+ const sessionRead = await readUserSession(userSessionPath(installRoot));
31630
32207
  const session = sessionRead.session;
31631
- const attachments = await listServerAttachments(slockHome);
32208
+ const attachments = await listServerAttachments(installRoot);
31632
32209
  const supervisor = {
31633
- ...await pidStatus(supervisorPidPath(slockHome)),
31634
- logPath: supervisorLogPath(slockHome)
32210
+ ...await pidStatus(supervisorPidPath(installRoot)),
32211
+ logPath: supervisorLogPath(installRoot)
31635
32212
  };
31636
32213
  const servers = [];
31637
32214
  for (const a of attachments) {
31638
- const daemon = await pidStatus(serverDaemonPidPath(slockHome, a.serverId));
32215
+ const daemon = await pidStatus(serverDaemonPidPath(installRoot, a.serverId));
31639
32216
  servers.push({
31640
32217
  serverId: a.serverId,
31641
32218
  serverSlug: a.serverSlug ?? null,
31642
32219
  serverMachineId: a.serverMachineId,
31643
32220
  serverUrl: a.serverUrl,
31644
32221
  attachedAt: a.attachedAt ?? null,
31645
- daemonLogPath: serverDaemonLogPath(slockHome, a.serverId),
32222
+ daemonLogPath: serverDaemonLogPath(installRoot, a.serverId),
31646
32223
  daemon,
31647
- health: await deriveHealth(slockHome, a.serverId, daemon)
32224
+ health: await deriveHealth(installRoot, a.serverId, daemon)
31648
32225
  });
31649
32226
  }
31650
32227
  const loggedIn = session?.kind === "user-session" && typeof session.accessToken === "string" && session.accessToken.length > 0;
31651
32228
  return {
31652
- slockHome,
32229
+ slockHome: installRoot,
31653
32230
  loggedIn,
31654
32231
  userId: session ? str(session.userId) : null,
31655
32232
  loginServerUrl: session ? str(session.serverUrl) : null,
@@ -31662,7 +32239,7 @@ function pad(s, n) {
31662
32239
  return s.length >= n ? s : s + " ".repeat(n - s.length);
31663
32240
  }
31664
32241
  async function runStatus(opts) {
31665
- const report = await buildStatusReport();
32242
+ const report = await buildStatusReport(resolveSlockHome());
31666
32243
  if (opts.json) {
31667
32244
  info(JSON.stringify(report, null, 2));
31668
32245
  return;
@@ -31767,42 +32344,92 @@ async function resolveTargetAttachment(opts) {
31767
32344
  return a;
31768
32345
  }
31769
32346
 
32347
+ // src/lib/readers.ts
32348
+ init_esm_shims();
32349
+
32350
+ // src/lib/types.ts
32351
+ init_esm_shims();
32352
+ var StateReaderError = class extends Error {
32353
+ code;
32354
+ constructor(code, message) {
32355
+ super(message);
32356
+ this.name = "StateReaderError";
32357
+ this.code = code;
32358
+ }
32359
+ };
32360
+
32361
+ // src/lib/readers.ts
32362
+ async function listRunners(installRoot, opts = {}) {
32363
+ const all = await listServerAttachments(installRoot);
32364
+ let subset = all;
32365
+ if (opts.serverId !== void 0) {
32366
+ const found = all.find((a) => a.serverId === opts.serverId);
32367
+ if (!found) {
32368
+ throw new StateReaderError(
32369
+ "NOT_ATTACHED",
32370
+ `Server ${opts.serverId} is not attached to this Computer.`
32371
+ );
32372
+ }
32373
+ subset = [found];
32374
+ }
32375
+ const servers = [];
32376
+ for (const a of subset) {
32377
+ const client = new RunnersClient(a.serverUrl, a.apiKey);
32378
+ const result = await client.list();
32379
+ const idCols = { serverId: a.serverId, serverSlug: a.serverSlug ?? null };
32380
+ if (result.status === "success") {
32381
+ servers.push({
32382
+ ...idCols,
32383
+ status: "ok",
32384
+ whitelist: result.whitelist,
32385
+ runners: result.runners
32386
+ });
32387
+ } else if (result.status === "unauthorized") {
32388
+ servers.push({ ...idCols, status: "unauthorized" });
32389
+ } else {
32390
+ servers.push({ ...idCols, status: "error", code: result.code });
32391
+ }
32392
+ }
32393
+ return { servers };
32394
+ }
32395
+
31770
32396
  // src/runners.ts
31771
32397
  async function runRunnersList(opts) {
31772
- const a = await resolveTargetAttachment({ server: opts.server });
31773
- const client = new RunnersClient(a.serverUrl, a.apiKey);
31774
- const result = await client.list();
31775
- const label = a.serverSlug ? formatServerSlugDisplay(a.serverSlug) : a.serverId;
31776
- if (result.status === "unauthorized") {
32398
+ const serverId = await resolveTargetServerId({ server: opts.server });
32399
+ const installRoot = resolveSlockHome();
32400
+ const { servers } = await listRunners(installRoot, { serverId });
32401
+ const block = servers[0];
32402
+ const label = block.serverSlug ? formatServerSlugDisplay(block.serverSlug) : block.serverId;
32403
+ if (block.status === "unauthorized") {
31777
32404
  fail(
31778
32405
  "RUNNERS_UNAUTHORIZED",
31779
32406
  `The Computer credential for ${label} was rejected. Re-run \`slock-computer attach ${label}\`.`
31780
32407
  );
31781
32408
  }
31782
- if (result.status === "error") {
32409
+ if (block.status === "error") {
31783
32410
  fail(
31784
32411
  "RUNNERS_LIST_FAILED",
31785
- `Could not list runners on ${label} (${result.code}). Check --server-url / server version.`
32412
+ `Could not list runners on ${label} (${block.code}). Check --server-url / server version.`
31786
32413
  );
31787
32414
  }
31788
32415
  if (opts.json) {
31789
32416
  info(
31790
32417
  JSON.stringify(
31791
- { server: label, serverId: a.serverId, whitelist: result.whitelist, runners: result.runners },
32418
+ { server: label, serverId: block.serverId, whitelist: block.whitelist, runners: block.runners },
31792
32419
  null,
31793
32420
  2
31794
32421
  )
31795
32422
  );
31796
32423
  return;
31797
32424
  }
31798
- if (result.runners.length === 0) {
32425
+ if (block.runners.length === 0) {
31799
32426
  info(`No runners on server ${label}.`);
31800
32427
  return;
31801
32428
  }
31802
32429
  info("");
31803
32430
  info(`Server ${label}:`);
31804
32431
  info(" AGENT STATUS RUNTIME MODEL NAME");
31805
- for (const r of result.runners) {
32432
+ for (const r of block.runners) {
31806
32433
  info(
31807
32434
  ` ${r.agentId.padEnd(38)}${(r.status ?? "").padEnd(11)}${(r.runtime ?? "").padEnd(10)}${(r.model ?? "").padEnd(15)}${r.name ?? ""}`
31808
32435
  );
@@ -31851,7 +32478,8 @@ function redactSecrets(line) {
31851
32478
  return out;
31852
32479
  }
31853
32480
  async function runDoctorChecks() {
31854
- const report = await buildStatusReport();
32481
+ const slockHome = resolveSlockHome();
32482
+ const report = await buildStatusReport(slockHome);
31855
32483
  const checks = [];
31856
32484
  checks.push({ name: "SLOCK_HOME", ok: true, detail: report.slockHome });
31857
32485
  checks.push(
@@ -31864,7 +32492,6 @@ async function runDoctorChecks() {
31864
32492
  detail: "stopped (run `slock-computer start` when you want background)"
31865
32493
  }
31866
32494
  );
31867
- const slockHome = resolveSlockHome();
31868
32495
  const attachments = await listServerAttachments(slockHome);
31869
32496
  if (attachments.length === 0) {
31870
32497
  checks.push({
@@ -31976,14 +32603,14 @@ async function runDoctor(opts) {
31976
32603
 
31977
32604
  // src/logs.ts
31978
32605
  init_esm_shims();
31979
- import { readFile as readFile8 } from "fs/promises";
32606
+ import { readFile as readFile10 } from "fs/promises";
31980
32607
  var DEFAULT_LINES = 200;
31981
32608
  async function runLogs(opts) {
31982
32609
  const home = resolveSlockHome();
31983
32610
  const file = opts.supervisor ? supervisorLogPath(home) : serverDaemonLogPath(home, await resolveTargetServerId({ server: opts.server }));
31984
32611
  let content;
31985
32612
  try {
31986
- content = await readFile8(file, "utf8");
32613
+ content = await readFile10(file, "utf8");
31987
32614
  } catch {
31988
32615
  fail(
31989
32616
  "NO_DAEMON_LOG",
@@ -31997,16 +32624,166 @@ async function runLogs(opts) {
31997
32624
  for (const line of tail) info(redactSecrets(line));
31998
32625
  }
31999
32626
 
32627
+ // src/reset.ts
32628
+ init_esm_shims();
32629
+
32630
+ // src/serviceState.ts
32631
+ init_esm_shims();
32632
+ import { mkdir as mkdir10, readFile as readFile11, writeFile as writeFile9, appendFile as appendFile3 } from "fs/promises";
32633
+ import { dirname as dirname10 } from "path";
32634
+
32635
+ // src/lib/state.ts
32636
+ init_esm_shims();
32637
+ var SERVICE_STATE_VALUES = [
32638
+ "starting",
32639
+ "running",
32640
+ "degraded",
32641
+ "stopping",
32642
+ "stopped"
32643
+ ];
32644
+ function isServiceState(value) {
32645
+ return typeof value === "string" && SERVICE_STATE_VALUES.includes(value);
32646
+ }
32647
+
32648
+ // src/serviceState.ts
32649
+ var DEFAULT_STATE = {
32650
+ state: "running",
32651
+ crashHistory: []
32652
+ };
32653
+ async function readServiceState(slockHome) {
32654
+ try {
32655
+ const raw = await readFile11(serviceStatePath(slockHome), "utf8");
32656
+ const parsed = JSON.parse(raw);
32657
+ if (!parsed || typeof parsed !== "object") return { ...DEFAULT_STATE };
32658
+ const obj = parsed;
32659
+ const state = isServiceState(obj.state) ? obj.state : "running";
32660
+ const crashHistory = Array.isArray(obj.crashHistory) ? obj.crashHistory.filter(isCrashEntry) : [];
32661
+ return { state, crashHistory };
32662
+ } catch {
32663
+ return { ...DEFAULT_STATE };
32664
+ }
32665
+ }
32666
+ async function writeServiceState(slockHome, file) {
32667
+ const path3 = serviceStatePath(slockHome);
32668
+ await mkdir10(dirname10(path3), { recursive: true });
32669
+ await writeFile9(path3, JSON.stringify(file), { mode: 384 });
32670
+ }
32671
+ function isCrashEntry(value) {
32672
+ if (!value || typeof value !== "object") return false;
32673
+ const obj = value;
32674
+ return typeof obj.at === "string";
32675
+ }
32676
+ async function emitServiceStateTransition(slockHome, fromState, toState, trigger) {
32677
+ const entry = {
32678
+ at: (/* @__PURE__ */ new Date()).toISOString(),
32679
+ kind: "service-state-changed",
32680
+ fromState,
32681
+ toState,
32682
+ trigger
32683
+ };
32684
+ try {
32685
+ const path3 = supervisorLogPath(slockHome);
32686
+ await mkdir10(dirname10(path3), { recursive: true });
32687
+ await appendFile3(path3, JSON.stringify(entry) + "\n");
32688
+ } catch {
32689
+ }
32690
+ }
32691
+ async function clearServiceCrashHistory(slockHome) {
32692
+ const current = await readServiceState(slockHome);
32693
+ const previousState = current.state;
32694
+ const clearedCrashCount = current.crashHistory.length;
32695
+ const next = { state: "running", crashHistory: [] };
32696
+ await writeServiceState(slockHome, next);
32697
+ await emitServiceStateTransition(
32698
+ slockHome,
32699
+ previousState,
32700
+ "running",
32701
+ "reset-service"
32702
+ );
32703
+ return { previousState, clearedCrashCount };
32704
+ }
32705
+
32706
+ // src/reset.ts
32707
+ async function resetService(installRoot) {
32708
+ const { previousState, clearedCrashCount } = await clearServiceCrashHistory(installRoot);
32709
+ return {
32710
+ status: "ok",
32711
+ previousState,
32712
+ clearedCrashCount
32713
+ };
32714
+ }
32715
+ async function resetRunner(installRoot, serverId) {
32716
+ const attachment = await readServerAttachment(installRoot, serverId);
32717
+ if (!attachment) {
32718
+ return { status: "not-found", serverId };
32719
+ }
32720
+ const outcome = await resetRunnerHealth(installRoot, serverId);
32721
+ if (outcome.status === "not-found") {
32722
+ return { status: "not-found", serverId };
32723
+ }
32724
+ return {
32725
+ status: "ok",
32726
+ serverId,
32727
+ previousState: outcome.previousState ?? "running",
32728
+ clearedCrashCount: outcome.clearedCrashCount ?? 0
32729
+ };
32730
+ }
32731
+ async function runReset(opts) {
32732
+ const wantsService = opts.service === true;
32733
+ const wantsRunner = opts.runner === true;
32734
+ if (wantsService && wantsRunner) {
32735
+ fail(
32736
+ "RESET_SCOPE_AMBIGUOUS",
32737
+ "`--service` and `--runner` are mutually exclusive. Pass exactly one."
32738
+ );
32739
+ }
32740
+ if (!wantsService && !wantsRunner) {
32741
+ fail(
32742
+ "RESET_SCOPE_MISSING",
32743
+ "Pass `--service` to clear the service-level crash history, or `--runner` to clear a per-runner crash history."
32744
+ );
32745
+ }
32746
+ const slockHome = resolveSlockHome();
32747
+ if (wantsService) {
32748
+ const result2 = await resetService(slockHome);
32749
+ if (opts.json) {
32750
+ info(JSON.stringify(result2));
32751
+ return;
32752
+ }
32753
+ info(
32754
+ `Reset service crash history (previous state: ${result2.previousState}; cleared ${result2.clearedCrashCount} entr${result2.clearedCrashCount === 1 ? "y" : "ies"}).`
32755
+ );
32756
+ info("Service state transitioned to `running`. Runners were not touched.");
32757
+ return;
32758
+ }
32759
+ const serverId = await resolveTargetServerId({ server: opts.server });
32760
+ const result = await resetRunner(slockHome, serverId);
32761
+ if (result.status === "not-found") {
32762
+ fail(
32763
+ "RESET_RUNNER_NOT_FOUND",
32764
+ `Runner for server ${serverId} was not found.`
32765
+ );
32766
+ }
32767
+ if (opts.json) {
32768
+ info(JSON.stringify(result));
32769
+ return;
32770
+ }
32771
+ info(
32772
+ `Reset runner crash history for server ${result.serverId} (previous state: ${result.previousState}; cleared ${result.clearedCrashCount} entr${result.clearedCrashCount === 1 ? "y" : "ies"}).`
32773
+ );
32774
+ info("Runner state transitioned to `running`. Process was not respawned.");
32775
+ }
32776
+
32000
32777
  // src/concurrency.ts
32001
32778
  init_esm_shims();
32002
32779
  var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
32003
- import { mkdir as mkdir9 } from "fs/promises";
32780
+ import { mkdir as mkdir11 } from "fs/promises";
32004
32781
  import { join as join4 } from "path";
32005
32782
  var STALE_LOCK_THRESHOLD_MS = 6e4;
32006
32783
  async function withMutationLock(fn) {
32007
32784
  const slockHome = resolveSlockHome();
32008
32785
  const lockTarget = computerDir(slockHome);
32009
- await mkdir9(lockTarget, { recursive: true });
32786
+ await mkdir11(lockTarget, { recursive: true });
32010
32787
  const lockfilePath = join4(lockTarget, ".lock");
32011
32788
  let release = null;
32012
32789
  try {
@@ -32045,8 +32822,8 @@ async function withMutationLock(fn) {
32045
32822
 
32046
32823
  // src/channel.ts
32047
32824
  init_esm_shims();
32048
- import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir10 } from "fs/promises";
32049
- import { dirname as dirname9 } from "path";
32825
+ import { readFile as readFile12, writeFile as writeFile10, mkdir as mkdir12 } from "fs/promises";
32826
+ import { dirname as dirname11 } from "path";
32050
32827
  var DEFAULT_CHANNEL = "latest";
32051
32828
  var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
32052
32829
  function parseChannel(raw) {
@@ -32061,7 +32838,7 @@ function parseChannel(raw) {
32061
32838
  }
32062
32839
  async function readChannel(slockHome) {
32063
32840
  try {
32064
- const raw = await readFile9(channelPath(slockHome), "utf8");
32841
+ const raw = await readFile12(channelPath(slockHome), "utf8");
32065
32842
  const parsed = parseChannel(raw);
32066
32843
  if (parsed !== null) return parsed;
32067
32844
  } catch {
@@ -32070,8 +32847,8 @@ async function readChannel(slockHome) {
32070
32847
  }
32071
32848
  async function writeChannel(slockHome, channel2) {
32072
32849
  const p = channelPath(slockHome);
32073
- await mkdir10(dirname9(p), { recursive: true });
32074
- await writeFile8(p, `${channel2}
32850
+ await mkdir12(dirname11(p), { recursive: true });
32851
+ await writeFile10(p, `${channel2}
32075
32852
  `, { mode: 384 });
32076
32853
  }
32077
32854
  async function runChannelShow(slockHome) {
@@ -32094,20 +32871,20 @@ async function runChannelSet(slockHome, raw) {
32094
32871
 
32095
32872
  // src/upgradeCli.ts
32096
32873
  init_esm_shims();
32097
- import { readFile as readFile12 } from "fs/promises";
32874
+ import { readFile as readFile15 } from "fs/promises";
32098
32875
  import { fileURLToPath as fileURLToPath3 } from "url";
32099
- import { dirname as dirname10, join as join7 } from "path";
32876
+ import { dirname as dirname12, join as join7 } from "path";
32100
32877
 
32101
32878
  // src/upgrade.ts
32102
32879
  init_esm_shims();
32103
32880
  import { spawn as spawn4 } from "child_process";
32104
- import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile9, rm as rm3, rename as rename4 } from "fs/promises";
32881
+ import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile11, rm as rm3, rename as rename4 } from "fs/promises";
32105
32882
  import { join as join6 } from "path";
32106
32883
  import { createHash as createHash3 } from "crypto";
32107
32884
 
32108
32885
  // src/preflightDepDrift.ts
32109
32886
  init_esm_shims();
32110
- import { readFile as readFile10 } from "fs/promises";
32887
+ import { readFile as readFile13 } from "fs/promises";
32111
32888
  import { spawn as spawn3 } from "child_process";
32112
32889
  import { createRequire } from "module";
32113
32890
  import { join as join5 } from "path";
@@ -32116,12 +32893,10 @@ var DAEMON_PACKAGE_NAME = "@slock-ai/daemon";
32116
32893
  async function preflightDepDriftCheck(currentBinaryDir, stagedTarballPath, deps = {}) {
32117
32894
  const readTarball = deps.readTarballPackageJson ?? defaultReadTarballPackageJson;
32118
32895
  const readCurrent = deps.readCurrentPackageJson ?? defaultReadCurrentPackageJson;
32119
- const readInstalled = deps.readInstalledDaemonVersion ?? defaultReadInstalledDaemonVersion;
32120
32896
  const satisfies = deps.semverSatisfies ?? defaultSemverSatisfies;
32121
32897
  const allowUnsafe = deps.allowUnsafeSpec === true;
32122
32898
  let targetPkg;
32123
32899
  let currentPkg;
32124
- let installedVersion;
32125
32900
  try {
32126
32901
  targetPkg = await readTarball(stagedTarballPath);
32127
32902
  } catch (e) {
@@ -32140,15 +32915,6 @@ async function preflightDepDriftCheck(currentBinaryDir, stagedTarballPath, deps
32140
32915
  detail: `cannot read current binary package.json: ${errMsg(e)}`
32141
32916
  };
32142
32917
  }
32143
- try {
32144
- installedVersion = await readInstalled(currentBinaryDir);
32145
- } catch (e) {
32146
- return {
32147
- ok: false,
32148
- reason: "read_failed",
32149
- detail: `cannot read installed ${DAEMON_PACKAGE_NAME} version: ${errMsg(e)}`
32150
- };
32151
- }
32152
32918
  const targetSpec = targetPkg.dependencies?.[DAEMON_PACKAGE_NAME];
32153
32919
  const currentSpec = currentPkg.dependencies?.[DAEMON_PACKAGE_NAME];
32154
32920
  if (typeof targetSpec !== "string" || targetSpec.length === 0) {
@@ -32159,60 +32925,64 @@ async function preflightDepDriftCheck(currentBinaryDir, stagedTarballPath, deps
32159
32925
  currentSpec
32160
32926
  };
32161
32927
  }
32162
- if (typeof currentSpec !== "string" || currentSpec.length === 0) {
32928
+ const unsafeTarget = isUnsafeSpec(targetSpec) || isUnparseableRange(targetSpec);
32929
+ if (!allowUnsafe && unsafeTarget) {
32163
32930
  return {
32164
32931
  ok: false,
32165
- reason: "read_failed",
32166
- detail: `current binary package.json has no ${DAEMON_PACKAGE_NAME} dependency entry`,
32932
+ reason: "unsafe_spec",
32933
+ detail: `${DAEMON_PACKAGE_NAME} target spec is not a safe semver range (target="${targetSpec}"). Auto-upgrade only supports target semver ranges that can be hydrated and verified before swap.`,
32934
+ currentSpec,
32167
32935
  targetSpec
32168
32936
  };
32169
32937
  }
32170
- if (installedVersion === null || installedVersion.length === 0) {
32938
+ void satisfies;
32939
+ return { ok: true, currentSpec, targetSpec };
32940
+ }
32941
+ async function verifyHydratedDaemonDependency(extractedPackageDir, targetSpec, deps = {}) {
32942
+ const readInstalled = deps.readInstalledDaemonVersion ?? defaultReadInstalledDaemonVersion;
32943
+ const satisfies = deps.semverSatisfies ?? defaultSemverSatisfies;
32944
+ const allowUnsafe = deps.allowUnsafeSpec === true;
32945
+ const unsafeTarget = isUnsafeSpec(targetSpec) || isUnparseableRange(targetSpec);
32946
+ if (!allowUnsafe && unsafeTarget) {
32171
32947
  return {
32172
32948
  ok: false,
32173
- reason: "read_failed",
32174
- detail: `${DAEMON_PACKAGE_NAME} is not installed at the expected location next to the current binary`,
32175
- currentSpec,
32949
+ reason: "unsafe_spec",
32950
+ detail: `${DAEMON_PACKAGE_NAME} target spec is not a safe semver range (target="${targetSpec}").`,
32176
32951
  targetSpec
32177
32952
  };
32178
32953
  }
32179
- const unsafeCurrent = isUnsafeSpec(currentSpec) || isUnparseableRange(currentSpec);
32180
- const unsafeTarget = isUnsafeSpec(targetSpec) || isUnparseableRange(targetSpec);
32181
- const anyUnsafe = unsafeCurrent || unsafeTarget;
32182
- if (!allowUnsafe && anyUnsafe) {
32954
+ let installedVersion;
32955
+ try {
32956
+ installedVersion = await readInstalled(extractedPackageDir);
32957
+ } catch (e) {
32183
32958
  return {
32184
32959
  ok: false,
32185
- reason: "unsafe_spec",
32186
- detail: `${DAEMON_PACKAGE_NAME} spec is not a safe semver range (current="${currentSpec}", target="${targetSpec}"). v1 auto-upgrade only supports semver ranges; re-install manually with \`npm install -g @slock-ai/computer@<version>\`.`,
32187
- currentSpec,
32188
- targetSpec,
32189
- installedVersion
32960
+ reason: "read_failed",
32961
+ detail: `cannot read hydrated ${DAEMON_PACKAGE_NAME} version: ${errMsg(e)}`,
32962
+ targetSpec
32190
32963
  };
32191
32964
  }
32192
- if (currentSpec !== targetSpec) {
32965
+ if (installedVersion === null || installedVersion.length === 0) {
32193
32966
  return {
32194
32967
  ok: false,
32195
- reason: "spec_changed",
32196
- detail: `${DAEMON_PACKAGE_NAME} dependency spec changed from "${currentSpec}" to "${targetSpec}". Auto-upgrade only swaps the Computer package root; the runtime dependency tree is not refreshed. Run \`npm install -g @slock-ai/computer@<version>\` manually.`,
32197
- currentSpec,
32198
- targetSpec,
32199
- installedVersion
32968
+ reason: "read_failed",
32969
+ detail: `${DAEMON_PACKAGE_NAME} is not installed in the hydrated staged package`,
32970
+ targetSpec
32200
32971
  };
32201
32972
  }
32202
- if (allowUnsafe && anyUnsafe) {
32203
- return { ok: true, currentSpec, targetSpec, installedVersion };
32973
+ if (allowUnsafe && unsafeTarget) {
32974
+ return { ok: true, targetSpec, installedVersion };
32204
32975
  }
32205
32976
  if (!satisfies(installedVersion, targetSpec)) {
32206
32977
  return {
32207
32978
  ok: false,
32208
32979
  reason: "installed_unsatisfied",
32209
- detail: `installed ${DAEMON_PACKAGE_NAME}@${installedVersion} does not satisfy target spec "${targetSpec}". Re-install with \`npm install -g @slock-ai/computer@<version>\`.`,
32210
- currentSpec,
32980
+ detail: `hydrated ${DAEMON_PACKAGE_NAME}@${installedVersion} does not satisfy target spec "${targetSpec}".`,
32211
32981
  targetSpec,
32212
32982
  installedVersion
32213
32983
  };
32214
32984
  }
32215
- return { ok: true, currentSpec, targetSpec, installedVersion };
32985
+ return { ok: true, targetSpec, installedVersion };
32216
32986
  }
32217
32987
  function isUnparseableRange(spec) {
32218
32988
  const s = spec.trim();
@@ -32278,7 +33048,7 @@ async function defaultReadTarballPackageJson(tarballPath) {
32278
33048
  return JSON.parse(raw);
32279
33049
  }
32280
33050
  async function defaultReadCurrentPackageJson(currentBinaryDir) {
32281
- const raw = await readFile10(join5(currentBinaryDir, "package.json"), "utf8");
33051
+ const raw = await readFile13(join5(currentBinaryDir, "package.json"), "utf8");
32282
33052
  return JSON.parse(raw);
32283
33053
  }
32284
33054
  async function defaultReadInstalledDaemonVersion(currentBinaryDir) {
@@ -32295,7 +33065,7 @@ async function defaultReadInstalledDaemonVersion(currentBinaryDir) {
32295
33065
  for (const base of searchPaths) {
32296
33066
  const candidate = join5(base, ...subPath, "package.json");
32297
33067
  try {
32298
- const raw = await readFile10(candidate, "utf8");
33068
+ const raw = await readFile13(candidate, "utf8");
32299
33069
  const parsed = JSON.parse(raw);
32300
33070
  if (parsed.name !== DAEMON_PACKAGE_NAME) continue;
32301
33071
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
@@ -32375,9 +33145,9 @@ function satisfiesTilde(ver, base) {
32375
33145
  // src/upgrade.ts
32376
33146
  async function stagePhase(slockHome, version, deps = {}) {
32377
33147
  const npmPack = deps.npmPack ?? defaultNpmPack;
32378
- const fsReadFile = deps.fsReadFile ?? readFile11;
33148
+ const fsReadFile = deps.fsReadFile ?? readFile14;
32379
33149
  const stagedPath = upgradeStagingDir(slockHome, version);
32380
- await mkdir11(stagedPath, { recursive: true });
33150
+ await mkdir13(stagedPath, { recursive: true });
32381
33151
  const packageRef = `@slock-ai/computer@${version}`;
32382
33152
  const result = await npmPack(stagedPath, packageRef);
32383
33153
  if (result.exitCode !== 0) {
@@ -32485,8 +33255,8 @@ async function cleanupStaged(slockHome, version) {
32485
33255
  async function snapshotPhase(slockHome, snap) {
32486
33256
  const path3 = upgradeSnapshotPath(slockHome);
32487
33257
  const tmp = `${path3}.tmp`;
32488
- await mkdir11(computerDir(slockHome), { recursive: true });
32489
- await writeFile9(tmp, JSON.stringify(snap, null, 2), { mode: 384 });
33258
+ await mkdir13(computerDir(slockHome), { recursive: true });
33259
+ await writeFile11(tmp, JSON.stringify(snap, null, 2), { mode: 384 });
32490
33260
  await rename4(tmp, path3);
32491
33261
  }
32492
33262
  async function clearUpgradeSnapshot(slockHome) {
@@ -32498,7 +33268,7 @@ async function clearUpgradeSnapshot(slockHome) {
32498
33268
  }
32499
33269
  async function extractTarball(tarballPath, destDir, deps = {}) {
32500
33270
  const tarSpawn = deps.tarSpawn ?? defaultTarSpawn;
32501
- await mkdir11(destDir, { recursive: true });
33271
+ await mkdir13(destDir, { recursive: true });
32502
33272
  const result = await tarSpawn(tarballPath, destDir);
32503
33273
  if (result.exitCode !== 0) {
32504
33274
  const err = new Error(
@@ -32603,7 +33373,7 @@ async function rollbackSwap(currentBinaryDir, deps = {}) {
32603
33373
  }
32604
33374
  async function rollbackRestart(slockHome, currentBinaryDir, deps = {}) {
32605
33375
  const readSupervisorPid = deps.readSupervisorPid ?? (() => defaultReadSupervisorPid(slockHome));
32606
- const isProcessAlive3 = deps.isProcessAlive ?? defaultIsProcessAlive;
33376
+ const isProcessAlive4 = deps.isProcessAlive ?? defaultIsProcessAlive;
32607
33377
  const killSupervisor = deps.killSupervisor ?? defaultKillSupervisor;
32608
33378
  const forceKillSupervisor = deps.forceKillSupervisor ?? defaultForceKillSupervisor;
32609
33379
  const waitForExit = deps.waitForExit ?? defaultWaitForExit;
@@ -32614,7 +33384,7 @@ async function rollbackRestart(slockHome, currentBinaryDir, deps = {}) {
32614
33384
  let supervisorStopped = false;
32615
33385
  try {
32616
33386
  const pid = await readSupervisorPid();
32617
- if (pid === null || !isProcessAlive3(pid)) {
33387
+ if (pid === null || !isProcessAlive4(pid)) {
32618
33388
  supervisorStopped = true;
32619
33389
  } else {
32620
33390
  await killSupervisor(pid);
@@ -32683,9 +33453,9 @@ async function rollbackRestart(slockHome, currentBinaryDir, deps = {}) {
32683
33453
  reason: e instanceof Error ? e.message : String(e)
32684
33454
  };
32685
33455
  }
32686
- const start = Date.now();
33456
+ const start2 = Date.now();
32687
33457
  let healthy = false;
32688
- while (Date.now() - start < healthTimeoutMs) {
33458
+ while (Date.now() - start2 < healthTimeoutMs) {
32689
33459
  if (await healthCheck()) {
32690
33460
  healthy = true;
32691
33461
  break;
@@ -32746,10 +33516,10 @@ async function restartPhase(slockHome, deps = {}) {
32746
33516
  return { ok: false, reason: "spawn_failed" };
32747
33517
  }
32748
33518
  }
32749
- const start = Date.now();
32750
- while (Date.now() - start < healthTimeoutMs) {
33519
+ const start2 = Date.now();
33520
+ while (Date.now() - start2 < healthTimeoutMs) {
32751
33521
  if (await healthCheck()) {
32752
- return { ok: true, healthAfterMs: Date.now() - start };
33522
+ return { ok: true, healthAfterMs: Date.now() - start2 };
32753
33523
  }
32754
33524
  await new Promise((r) => setTimeout(r, healthPollIntervalMs));
32755
33525
  }
@@ -32757,7 +33527,7 @@ async function restartPhase(slockHome, deps = {}) {
32757
33527
  }
32758
33528
  async function defaultReadSupervisorPid(slockHome) {
32759
33529
  try {
32760
- const raw = (await readFile11(supervisorPidPath(slockHome), "utf8")).trim();
33530
+ const raw = (await readFile14(supervisorPidPath(slockHome), "utf8")).trim();
32761
33531
  const pid = Number.parseInt(raw, 10);
32762
33532
  return Number.isInteger(pid) && pid > 0 ? pid : null;
32763
33533
  } catch {
@@ -32784,8 +33554,8 @@ async function defaultIsDaemonBusy(_slockHome) {
32784
33554
  return false;
32785
33555
  }
32786
33556
  async function defaultWaitForExit(pid, timeoutMs) {
32787
- const start = Date.now();
32788
- while (Date.now() - start < timeoutMs) {
33557
+ const start2 = Date.now();
33558
+ while (Date.now() - start2 < timeoutMs) {
32789
33559
  try {
32790
33560
  process.kill(pid, 0);
32791
33561
  } catch (err) {
@@ -32876,6 +33646,18 @@ async function runUpgrade(slockHome, opts) {
32876
33646
  preflight
32877
33647
  };
32878
33648
  }
33649
+ const targetDaemonSpec = preflight.targetSpec;
33650
+ if (typeof targetDaemonSpec !== "string" || targetDaemonSpec.length === 0) {
33651
+ await cleanupStaged(slockHome, opts.targetVersion);
33652
+ return {
33653
+ ok: false,
33654
+ phase: "preflight",
33655
+ reason: `${preflight.detail ?? preflight.reason ?? "dep_drift"}: missing target ${DAEMON_PACKAGE_NAME} spec`,
33656
+ staged,
33657
+ verify,
33658
+ preflight
33659
+ };
33660
+ }
32879
33661
  const snap = {
32880
33662
  at: (/* @__PURE__ */ new Date()).toISOString(),
32881
33663
  fromVersion: opts.fromVersion,
@@ -32926,6 +33708,24 @@ async function runUpgrade(slockHome, opts) {
32926
33708
  verify
32927
33709
  };
32928
33710
  }
33711
+ const hydratedDaemon = await verifyHydratedDaemonDependency(
33712
+ extractedPackageDir,
33713
+ targetDaemonSpec,
33714
+ deps.preflight ?? {}
33715
+ );
33716
+ if (!hydratedDaemon.ok) {
33717
+ await cleanupStaged(slockHome, opts.targetVersion);
33718
+ await clearUpgradeSnapshot(slockHome);
33719
+ return {
33720
+ ok: false,
33721
+ phase: "hydrate",
33722
+ reason: `staged dependency tree verification failed: ${hydratedDaemon.detail ?? hydratedDaemon.reason ?? "hydrated dependency verification failed"}`,
33723
+ staged,
33724
+ verify,
33725
+ preflight,
33726
+ hydratedDaemon
33727
+ };
33728
+ }
32929
33729
  let swap;
32930
33730
  try {
32931
33731
  swap = await swapPhase(opts.currentBinaryDir, extractedPackageDir, {
@@ -33014,25 +33814,25 @@ async function runUpgrade(slockHome, opts) {
33014
33814
  };
33015
33815
  }
33016
33816
  await cleanupSuccessPhase(slockHome, opts.targetVersion, swap.prevBinaryDir);
33017
- return { ok: true, phase: "cleanup", staged, verify, swap, restart, rolling };
33817
+ return { ok: true, phase: "cleanup", staged, verify, preflight, hydratedDaemon, swap, restart, rolling };
33018
33818
  }
33019
33819
  async function rollingDaemonHealthCheck(slockHome, deps = {}) {
33020
33820
  const list = deps.listManagedServerIds ?? listManagedServerIds;
33021
33821
  const readDaemonPid = deps.readDaemonPid ?? defaultReadDaemonPid;
33022
- const isProcessAlive3 = deps.isProcessAlive ?? defaultIsProcessAlive;
33822
+ const isProcessAlive4 = deps.isProcessAlive ?? defaultIsProcessAlive;
33023
33823
  const perDaemonTimeoutMs = deps.perDaemonTimeoutMs ?? 3e4;
33024
33824
  const pollIntervalMs = deps.pollIntervalMs ?? 500;
33025
33825
  const managed = await list(slockHome);
33026
33826
  const daemons = [];
33027
33827
  for (const serverId of managed) {
33028
- const start = Date.now();
33828
+ const start2 = Date.now();
33029
33829
  let lastReason = "timeout";
33030
33830
  let healthy = false;
33031
- while (Date.now() - start < perDaemonTimeoutMs) {
33831
+ while (Date.now() - start2 < perDaemonTimeoutMs) {
33032
33832
  const pid = await readDaemonPid(slockHome, serverId);
33033
33833
  if (pid === null) {
33034
33834
  lastReason = "pidfile_missing";
33035
- } else if (!isProcessAlive3(pid)) {
33835
+ } else if (!isProcessAlive4(pid)) {
33036
33836
  lastReason = "process_dead";
33037
33837
  } else {
33038
33838
  healthy = true;
@@ -33040,7 +33840,7 @@ async function rollingDaemonHealthCheck(slockHome, deps = {}) {
33040
33840
  }
33041
33841
  await new Promise((r) => setTimeout(r, pollIntervalMs));
33042
33842
  }
33043
- const entry = healthy ? { serverId, ok: true, healthAfterMs: Date.now() - start } : { serverId, ok: false, reason: lastReason };
33843
+ const entry = healthy ? { serverId, ok: true, healthAfterMs: Date.now() - start2 } : { serverId, ok: false, reason: lastReason };
33044
33844
  daemons.push(entry);
33045
33845
  if (!healthy) {
33046
33846
  return { ok: false, daemons };
@@ -33050,7 +33850,7 @@ async function rollingDaemonHealthCheck(slockHome, deps = {}) {
33050
33850
  }
33051
33851
  async function defaultReadDaemonPid(slockHome, serverId) {
33052
33852
  try {
33053
- const raw = (await readFile11(serverDaemonPidPath(slockHome, serverId), "utf8")).trim();
33853
+ const raw = (await readFile14(serverDaemonPidPath(slockHome, serverId), "utf8")).trim();
33054
33854
  const pid = Number.parseInt(raw, 10);
33055
33855
  return Number.isInteger(pid) && pid > 0 ? pid : null;
33056
33856
  } catch {
@@ -33078,7 +33878,7 @@ async function locateStagedTarball(stagedPath) {
33078
33878
 
33079
33879
  // src/upgradeLog.ts
33080
33880
  init_esm_shims();
33081
- import { chmod as chmod5, mkdir as mkdir12, open as open2 } from "fs/promises";
33881
+ import { chmod as chmod5, mkdir as mkdir14, open as open2 } from "fs/promises";
33082
33882
  var FILE_MODE = 384;
33083
33883
  var UPGRADE_ERROR_CODES = [
33084
33884
  "UPGRADE_DEPS_CHANGED",
@@ -33123,7 +33923,7 @@ function assertUpgradeLogEntry(entry) {
33123
33923
  }
33124
33924
  async function appendUpgradeLogEntry(slockHome, entry) {
33125
33925
  assertUpgradeLogEntry(entry);
33126
- await mkdir12(computerDir(slockHome), { recursive: true });
33926
+ await mkdir14(computerDir(slockHome), { recursive: true });
33127
33927
  const path3 = upgradeLogPath(slockHome);
33128
33928
  const at = entry.at ?? formatUpgradeLogTimestamp();
33129
33929
  const fullEntry = { ...entry, at };
@@ -33146,7 +33946,7 @@ function isEphemeralNpxContext(binaryDir) {
33146
33946
  async function readBundledDaemonVersion(binaryDir) {
33147
33947
  try {
33148
33948
  const pkgPath = join7(binaryDir, "package.json");
33149
- const raw = await readFile12(pkgPath, "utf8");
33949
+ const raw = await readFile15(pkgPath, "utf8");
33150
33950
  const parsed = JSON.parse(raw);
33151
33951
  const pinned = parsed.dependencies?.["@slock-ai/daemon"];
33152
33952
  if (typeof pinned !== "string" || pinned.length === 0) return null;
@@ -33340,6 +34140,9 @@ function mapFailurePhaseToCode(outcome) {
33340
34140
  case "extract":
33341
34141
  return "UPGRADE_INTEGRITY_FAILED";
33342
34142
  case "hydrate":
34143
+ if (outcome.hydratedDaemon?.ok === false) {
34144
+ return "UPGRADE_INTEGRITY_FAILED";
34145
+ }
33343
34146
  return "UPGRADE_NETWORK_FAILED";
33344
34147
  case "swap":
33345
34148
  return "UPGRADE_SWAP_FAILED";
@@ -33374,12 +34177,12 @@ async function defaultFetchDistTags() {
33374
34177
  }
33375
34178
  function defaultCurrentBinaryDir() {
33376
34179
  const here = fileURLToPath3(import.meta.url);
33377
- return dirname10(dirname10(here));
34180
+ return dirname12(dirname12(here));
33378
34181
  }
33379
34182
  async function defaultCurrentVersion() {
33380
34183
  const pkgPath = join7(defaultCurrentBinaryDir(), "package.json");
33381
34184
  try {
33382
- const raw = await readFile12(pkgPath, "utf8");
34185
+ const raw = await readFile15(pkgPath, "utf8");
33383
34186
  const parsed = JSON.parse(raw);
33384
34187
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
33385
34188
  return parsed.version;
@@ -33391,7 +34194,7 @@ async function defaultCurrentVersion() {
33391
34194
 
33392
34195
  // src/upgradeTestHarness.ts
33393
34196
  init_esm_shims();
33394
- import { mkdir as mkdir13, readdir as readdir4, stat as stat3, writeFile as writeFile10 } from "fs/promises";
34197
+ import { mkdir as mkdir15, readdir as readdir4, stat as stat3, writeFile as writeFile12 } from "fs/promises";
33395
34198
  import { join as join8 } from "path";
33396
34199
  import { createHash as createHash4 } from "crypto";
33397
34200
  var PHASES = /* @__PURE__ */ new Set([
@@ -33450,7 +34253,7 @@ function buildSimulatedDeps(slockHome, opts) {
33450
34253
  return { tarballPath: "", exitCode: 1, stderr: "simulated stage failure" };
33451
34254
  }
33452
34255
  const filename = `slock-ai-computer-${targetVersion}.tgz`;
33453
- await writeFile10(join8(cwd, filename), tarballBytes);
34256
+ await writeFile12(join8(cwd, filename), tarballBytes);
33454
34257
  return { tarballPath: join8(cwd, filename), exitCode: 0, stderr: "" };
33455
34258
  },
33456
34259
  fetchAdvertisedHash: async () => {
@@ -33462,8 +34265,8 @@ function buildSimulatedDeps(slockHome, opts) {
33462
34265
  if (opts.simulateFail === "extract") {
33463
34266
  return { exitCode: 1, stderr: "simulated extract failure" };
33464
34267
  }
33465
- await mkdir13(join8(destDir, "package"), { recursive: true });
33466
- await writeFile10(join8(destDir, "package", "marker.txt"), `NEW@${targetVersion}`);
34268
+ await mkdir15(join8(destDir, "package"), { recursive: true });
34269
+ await writeFile12(join8(destDir, "package", "marker.txt"), `NEW@${targetVersion}`);
33467
34270
  return { exitCode: 0, stderr: "" };
33468
34271
  },
33469
34272
  npmInstall: async () => ({ exitCode: 0, stderr: "" }),
@@ -33524,7 +34327,7 @@ function buildSimulatedDeps(slockHome, opts) {
33524
34327
  }
33525
34328
  async function arrangeSnapshotFailure(slockHome) {
33526
34329
  const snapshotPath = join8(slockHome, "computer", "upgrade-snapshot.json");
33527
- await mkdir13(snapshotPath, { recursive: true });
34330
+ await mkdir15(snapshotPath, { recursive: true });
33528
34331
  }
33529
34332
  async function pathInfo(path3) {
33530
34333
  try {
@@ -33579,12 +34382,12 @@ async function runUpgradeTestHarness(slockHome, opts, writer = (s) => process.st
33579
34382
  process.exitCode = 1;
33580
34383
  return;
33581
34384
  }
33582
- await mkdir13(slockHome, { recursive: true });
34385
+ await mkdir15(slockHome, { recursive: true });
33583
34386
  if (opts.simulateFail === "snapshot") {
33584
34387
  await arrangeSnapshotFailure(slockHome);
33585
34388
  }
33586
34389
  const { opts: upgradeOpts } = buildSimulatedDeps(slockHome, opts);
33587
- await mkdir13(upgradeOpts.currentBinaryDir, { recursive: true });
34390
+ await mkdir15(upgradeOpts.currentBinaryDir, { recursive: true });
33588
34391
  let outcome;
33589
34392
  try {
33590
34393
  outcome = await runUpgrade(slockHome, upgradeOpts);
@@ -33622,9 +34425,9 @@ async function runUpgradeTestHarness(slockHome, opts, writer = (s) => process.st
33622
34425
 
33623
34426
  // src/upgradeInstallSmoke.ts
33624
34427
  init_esm_shims();
33625
- import { copyFile, mkdir as mkdir14, readFile as readFile13 } from "fs/promises";
34428
+ import { copyFile, mkdir as mkdir16, readFile as readFile16 } from "fs/promises";
33626
34429
  import { createHash as createHash5 } from "crypto";
33627
- import { dirname as dirname11, isAbsolute, join as join9, resolve as pathResolve } from "path";
34430
+ import { dirname as dirname13, isAbsolute, join as join9, resolve as pathResolve } from "path";
33628
34431
  import { fileURLToPath as fileURLToPath4 } from "url";
33629
34432
  async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
33630
34433
  if (typeof opts.packageTarball !== "string" || opts.packageTarball.trim().length === 0) {
@@ -33633,7 +34436,7 @@ async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
33633
34436
  const tarballPath = isAbsolute(opts.packageTarball) ? opts.packageTarball : pathResolve(opts.packageTarball);
33634
34437
  let tarballBytes;
33635
34438
  try {
33636
- tarballBytes = await readFile13(tarballPath);
34439
+ tarballBytes = await readFile16(tarballPath);
33637
34440
  } catch (e) {
33638
34441
  const msg = e instanceof Error ? e.message : String(e);
33639
34442
  throw new Error(`__upgrade-install-smoke: cannot read --package-tarball ${tarballPath}: ${msg}`);
@@ -33645,7 +34448,7 @@ async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
33645
34448
  const spawnFreshSupervisor = deps.spawnFreshSupervisor ?? (async (h) => {
33646
34449
  await spawnDetachedSupervisor(h);
33647
34450
  });
33648
- await mkdir14(slockHome, { recursive: true });
34451
+ await mkdir16(slockHome, { recursive: true });
33649
34452
  const outcome = await runUpgrade(slockHome, {
33650
34453
  targetVersion: opts.targetVersion,
33651
34454
  fromVersion,
@@ -33703,12 +34506,12 @@ async function runUpgradeInstallSmokeCli(slockHome, opts, writer = (s) => proces
33703
34506
  }
33704
34507
  function defaultCurrentBinaryDirLocal() {
33705
34508
  const here = fileURLToPath4(import.meta.url);
33706
- return dirname11(dirname11(here));
34509
+ return dirname13(dirname13(here));
33707
34510
  }
33708
34511
  async function defaultCurrentVersionLocal() {
33709
34512
  const pkgPath = join9(defaultCurrentBinaryDirLocal(), "package.json");
33710
34513
  try {
33711
- const raw = await readFile13(pkgPath, "utf8");
34514
+ const raw = await readFile16(pkgPath, "utf8");
33712
34515
  const parsed = JSON.parse(raw);
33713
34516
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
33714
34517
  return parsed.version;
@@ -33758,17 +34561,13 @@ program2.command("attach").argument("<serverSlug>", "target Slock server slug (c
33758
34561
  () => runAttach({ serverSlug, serverUrl: opts.serverUrl, name: opts.name, run: opts.run, foreground: opts.foreground })
33759
34562
  );
33760
34563
  }));
33761
- program2.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 the saved user session, SLOCK_SERVER_URL, or ${DEFAULT_SLOCK_SERVER_URL}`).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(
34564
+ program2.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 (or \xA7X.1 migrate-prompt) if needed, then start.").option("--server-url <url>", `Slock API base URL; defaults to the saved user session, SLOCK_SERVER_URL, or ${DEFAULT_SLOCK_SERVER_URL}`).option("--name <name>", "Computer display name for a new attachment; defaults to a sanitized hostname").option("--no-start", "stop after login + attach; 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(
33762
34565
  withCliExit(async (serverSlug, opts) => {
33763
34566
  await withMutationLock(
33764
34567
  () => runSetup({
33765
34568
  serverSlug,
33766
34569
  serverUrl: opts.serverUrl,
33767
34570
  name: opts.name,
33768
- adoptLegacy: opts.adoptLegacy,
33769
- legacyApiKey: opts.legacyApiKey,
33770
- legacyApiKeyFile: opts.legacyApiKeyFile,
33771
- legacyApiKeyStdin: opts.legacyApiKeyStdin,
33772
34571
  start: opts.start,
33773
34572
  foreground: opts.foreground,
33774
34573
  yes: opts.yes
@@ -33776,22 +34575,6 @@ program2.command("setup").argument("<serverSlug>", "target Slock server slug (ca
33776
34575
  );
33777
34576
  })
33778
34577
  );
33779
- program2.command("adopt-legacy").argument("<serverSlug>", "target Slock server slug (the legacy machine's server)").description(
33780
- "One-time migration: trade a legacy sk_machine_* / sk_daemon_* key for a fresh Computer attachment (sk_computer_*)."
33781
- ).option("--server-url <url>", `Slock API base URL; defaults to the saved user session, SLOCK_SERVER_URL, or ${DEFAULT_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(
33782
- withCliExit(async (serverSlug, opts) => {
33783
- await withMutationLock(
33784
- () => runAdoptLegacy({
33785
- serverSlug,
33786
- serverUrl: opts.serverUrl,
33787
- name: opts.name,
33788
- legacyApiKey: opts.legacyApiKey,
33789
- legacyApiKeyFile: opts.legacyApiKeyFile,
33790
- legacyApiKeyStdin: opts.legacyApiKeyStdin
33791
- })
33792
- );
33793
- })
33794
- );
33795
34578
  program2.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) => {
33796
34579
  await withMutationLock(async () => runDetach(await resolveTargetServerId({ server: serverSlug }), serverSlug));
33797
34580
  }));
@@ -33825,6 +34608,18 @@ program2.command("doctor").argument("[serverSlug]", "optional: scope detail (rec
33825
34608
  }
33826
34609
  )
33827
34610
  );
34611
+ program2.command("reset").description("Clear a degraded state and resume the supervisor's auto-restart loop.").option("--service", "clear the service-level crash history (cascade record)").option("--runner", "clear a runner's crash history (selected by `--server`)").option("--server <slug>", "with `--runner`, select target server slug (required when \u22652 attached)").option("--json", "emit the machine-readable result").action(
34612
+ withCliExit(async (opts) => {
34613
+ await withMutationLock(
34614
+ () => runReset({
34615
+ service: opts.service,
34616
+ runner: opts.runner,
34617
+ server: opts.server,
34618
+ json: opts.json
34619
+ })
34620
+ );
34621
+ })
34622
+ );
33828
34623
  program2.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) => {
33829
34624
  await runLogs({ lines: opts.lines, server: opts.server ?? null, supervisor: !!opts.supervisor });
33830
34625
  }));
@@ -33850,9 +34645,8 @@ program2.command("upgrade").description(
33850
34645
  [
33851
34646
  "Auto-upgrade this Computer to the latest version in its channel (or --target-version <semver>).",
33852
34647
  "",
33853
- "v1 swaps the @slock-ai/computer package root only. Releases that change runtime",
33854
- "dependency requirements (e.g. @slock-ai/daemon version range) fail closed with",
33855
- "UPGRADE_DEPS_CHANGED and require manual `npm install -g @slock-ai/computer@<version>`."
34648
+ "Stages the target package, hydrates production dependencies, verifies the",
34649
+ "hydrated @slock-ai/daemon version, then swaps the Computer package root."
33856
34650
  ].join("\n")
33857
34651
  ).option("--dry-run", "stage + verify only; do not swap or restart").option("--channel <name>", "override channel for this invocation (latest | alpha | pinned:<semver>)").option("--target-version <semver>", "override target version explicitly (bypasses channel resolution)").option("--drain <mode>", "\xA72.7 drain mode: drain (default) | defer (abort if daemon busy) | force (SIGKILL, drop in-flight)").option("--force", "alias for --drain force (stuck-daemon recovery; drops in-flight turns)").action(
33858
34652
  withCliExit(
@@ -33945,21 +34739,6 @@ program2.command("__upgrade-install-smoke", { hidden: true }).requiredOption("--
33945
34739
  );
33946
34740
  })
33947
34741
  );
33948
- {
33949
- const argv = process.argv.slice(2);
33950
- const isAdopt = argv[0] === "adopt-legacy";
33951
- const isSetup = argv[0] === "setup";
33952
- const strayLegacyFlag = argv.find(
33953
- (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"
33954
- );
33955
- if (strayLegacyFlag && !isAdopt && !isSetup) {
33956
- process.stderr.write(
33957
- `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.
33958
- `
33959
- );
33960
- process.exit(2);
33961
- }
33962
- }
33963
34742
  program2.parseAsync(process.argv).catch((err) => {
33964
34743
  process.stderr.write(`slock-computer: ${err instanceof Error ? err.message : String(err)}
33965
34744
  `);