@slock-ai/computer 0.0.35 → 0.0.36

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
@@ -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: writeFile13, readFile: readFile17, mkdir: mkdir17 } = __require("fs/promises");
16621
- var { dirname: dirname14, resolve: resolve2 } = __require("path");
16620
+ var { writeFile: writeFile12, readFile: readFile15, mkdir: mkdir15 } = __require("fs/promises");
16621
+ var { dirname: dirname13, resolve: resolve2 } = __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 readFile17(resolve2(path3), "utf8");
16822
+ const data = await readFile15(resolve2(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 = resolve2(path3);
16852
- await mkdir17(dirname14(resolvedPath), { recursive: true });
16852
+ await mkdir15(dirname13(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 writeFile13(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
16857
+ await writeFile12(resolvedPath, JSON.stringify(data, null, 2), { flush: true });
16858
16858
  }
16859
16859
  /**
16860
16860
  * Clears all recorded snapshots
@@ -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 = readFile17;
28473
- function readFile17(path3, options, cb) {
28472
+ fs2.readFile = readFile15;
28473
+ function readFile15(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 = writeFile13;
28490
- function writeFile13(path3, data, options, cb) {
28489
+ fs2.writeFile = writeFile12;
28490
+ function writeFile12(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);
@@ -28522,8 +28522,8 @@ var require_graceful_fs = __commonJS({
28522
28522
  }
28523
28523
  var fs$copyFile = fs2.copyFile;
28524
28524
  if (fs$copyFile)
28525
- fs2.copyFile = copyFile2;
28526
- function copyFile2(src, dest, flags, cb) {
28525
+ fs2.copyFile = copyFile;
28526
+ function copyFile(src, dest, flags, cb) {
28527
28527
  if (typeof flags === "function") {
28528
28528
  cb = flags;
28529
28529
  flags = 0;
@@ -28541,9 +28541,9 @@ var require_graceful_fs = __commonJS({
28541
28541
  }
28542
28542
  }
28543
28543
  var fs$readdir = fs2.readdir;
28544
- fs2.readdir = readdir5;
28544
+ fs2.readdir = readdir4;
28545
28545
  var noReaddirOptionVersions = /^v[0-5]\./;
28546
- function readdir5(path3, options, cb) {
28546
+ function readdir4(path3, options, cb) {
28547
28547
  if (typeof options === "function")
28548
28548
  cb = options, options = null;
28549
28549
  var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir2(path4, options2, cb2, startTime) {
@@ -29193,11 +29193,11 @@ var require_mtime_precision = __commonJS({
29193
29193
  function probe(file, fs, callback) {
29194
29194
  const cachedPrecision = fs[cacheSymbol];
29195
29195
  if (cachedPrecision) {
29196
- return fs.stat(file, (err, stat5) => {
29196
+ return fs.stat(file, (err, stat4) => {
29197
29197
  if (err) {
29198
29198
  return callback(err);
29199
29199
  }
29200
- callback(null, stat5.mtime, cachedPrecision);
29200
+ callback(null, stat4.mtime, cachedPrecision);
29201
29201
  });
29202
29202
  }
29203
29203
  const mtime = new Date(Math.ceil(Date.now() / 1e3) * 1e3 + 5);
@@ -29205,13 +29205,13 @@ var require_mtime_precision = __commonJS({
29205
29205
  if (err) {
29206
29206
  return callback(err);
29207
29207
  }
29208
- fs.stat(file, (err2, stat5) => {
29208
+ fs.stat(file, (err2, stat4) => {
29209
29209
  if (err2) {
29210
29210
  return callback(err2);
29211
29211
  }
29212
- const precision = stat5.mtime.getTime() % 1e3 === 0 ? "s" : "ms";
29212
+ const precision = stat4.mtime.getTime() % 1e3 === 0 ? "s" : "ms";
29213
29213
  Object.defineProperty(fs, cacheSymbol, { value: precision });
29214
- callback(null, stat5.mtime, precision);
29214
+ callback(null, stat4.mtime, precision);
29215
29215
  });
29216
29216
  });
29217
29217
  }
@@ -29266,14 +29266,14 @@ var require_lockfile = __commonJS({
29266
29266
  if (options.stale <= 0) {
29267
29267
  return callback(Object.assign(new Error("Lock file is already being held"), { code: "ELOCKED", file }));
29268
29268
  }
29269
- options.fs.stat(lockfilePath, (err2, stat5) => {
29269
+ options.fs.stat(lockfilePath, (err2, stat4) => {
29270
29270
  if (err2) {
29271
29271
  if (err2.code === "ENOENT") {
29272
29272
  return acquireLock(file, { ...options, stale: 0 }, callback);
29273
29273
  }
29274
29274
  return callback(err2);
29275
29275
  }
29276
- if (!isLockStale(stat5, options)) {
29276
+ if (!isLockStale(stat4, options)) {
29277
29277
  return callback(Object.assign(new Error("Lock file is already being held"), { code: "ELOCKED", file }));
29278
29278
  }
29279
29279
  removeLock(file, options, (err3) => {
@@ -29285,8 +29285,8 @@ var require_lockfile = __commonJS({
29285
29285
  });
29286
29286
  });
29287
29287
  }
29288
- function isLockStale(stat5, options) {
29289
- return stat5.mtime.getTime() < Date.now() - options.stale;
29288
+ function isLockStale(stat4, options) {
29289
+ return stat4.mtime.getTime() < Date.now() - options.stale;
29290
29290
  }
29291
29291
  function removeLock(file, options, callback) {
29292
29292
  options.fs.rmdir(getLockFile(file, options), (err) => {
@@ -29304,7 +29304,7 @@ var require_lockfile = __commonJS({
29304
29304
  lock2.updateDelay = lock2.updateDelay || options.update;
29305
29305
  lock2.updateTimeout = setTimeout(() => {
29306
29306
  lock2.updateTimeout = null;
29307
- options.fs.stat(lock2.lockfilePath, (err, stat5) => {
29307
+ options.fs.stat(lock2.lockfilePath, (err, stat4) => {
29308
29308
  const isOverThreshold = lock2.lastUpdate + options.stale < Date.now();
29309
29309
  if (err) {
29310
29310
  if (err.code === "ENOENT" || isOverThreshold) {
@@ -29313,7 +29313,7 @@ var require_lockfile = __commonJS({
29313
29313
  lock2.updateDelay = 1e3;
29314
29314
  return updateLock(file, options);
29315
29315
  }
29316
- const isMtimeOurs = lock2.mtime.getTime() === stat5.mtime.getTime();
29316
+ const isMtimeOurs = lock2.mtime.getTime() === stat4.mtime.getTime();
29317
29317
  if (!isMtimeOurs) {
29318
29318
  return setLockAsCompromised(
29319
29319
  file,
@@ -29438,11 +29438,11 @@ var require_lockfile = __commonJS({
29438
29438
  if (err) {
29439
29439
  return callback(err);
29440
29440
  }
29441
- options.fs.stat(getLockFile(file2, options), (err2, stat5) => {
29441
+ options.fs.stat(getLockFile(file2, options), (err2, stat4) => {
29442
29442
  if (err2) {
29443
29443
  return err2.code === "ENOENT" ? callback(null, false) : callback(err2);
29444
29444
  }
29445
- return callback(null, !isLockStale(stat5, options));
29445
+ return callback(null, !isLockStale(stat4, options));
29446
29446
  });
29447
29447
  });
29448
29448
  }
@@ -29570,6 +29570,7 @@ var require_proper_lockfile = __commonJS({
29570
29570
 
29571
29571
  // src/index.ts
29572
29572
  init_esm_shims();
29573
+ import { pathToFileURL } from "url";
29573
29574
 
29574
29575
  // ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
29575
29576
  init_esm_shims();
@@ -30042,12 +30043,6 @@ function adoptionLogPath(slockHome) {
30042
30043
  function channelPath(slockHome) {
30043
30044
  return path2.join(computerDir(slockHome), "channel");
30044
30045
  }
30045
- function upgradeStagingDir(slockHome, version) {
30046
- return path2.join(computerDir(slockHome), "upgrade-staging", version);
30047
- }
30048
- function upgradeSnapshotPath(slockHome) {
30049
- return path2.join(computerDir(slockHome), "upgrade-snapshot.json");
30050
- }
30051
30046
  function upgradeLogPath(slockHome) {
30052
30047
  return path2.join(computerDir(slockHome), "upgrade.log");
30053
30048
  }
@@ -30510,8 +30505,8 @@ async function runAttach(opts) {
30510
30505
  // src/setup.ts
30511
30506
  init_esm_shims();
30512
30507
  var import_undici3 = __toESM(require_undici(), 1);
30513
- import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename3, rm as rm2, writeFile as writeFile8 } from "fs/promises";
30514
- import { dirname as dirname9 } from "path";
30508
+ import { chmod as chmod5, mkdir as mkdir11, readFile as readFile10, rename as rename4, rm as rm3, writeFile as writeFile10 } from "fs/promises";
30509
+ import { dirname as dirname10 } from "path";
30515
30510
 
30516
30511
  // src/lib/migration.ts
30517
30512
  init_esm_shims();
@@ -31018,11 +31013,14 @@ async function appendAdoptionLog(slockHome, line) {
31018
31013
  // src/service.ts
31019
31014
  init_esm_shims();
31020
31015
  import { spawn as spawn2 } from "child_process";
31016
+ import { createRequire as createRequire2 } from "module";
31021
31017
 
31022
31018
  // src/version.ts
31023
31019
  init_esm_shims();
31024
31020
  import { createRequire } from "module";
31025
31021
  function readComputerVersion(moduleUrl = import.meta.url) {
31022
+ const baked = process.env.SLOCK_COMPUTER_VERSION;
31023
+ if (typeof baked === "string" && baked.length > 0) return baked;
31026
31024
  try {
31027
31025
  const require2 = createRequire(moduleUrl);
31028
31026
  const pkg = require2("../package.json");
@@ -31034,8 +31032,8 @@ function readComputerVersion(moduleUrl = import.meta.url) {
31034
31032
  var COMPUTER_VERSION = readComputerVersion();
31035
31033
 
31036
31034
  // src/service.ts
31037
- import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile7, open, rename as rename2 } from "fs/promises";
31038
- import { dirname as dirname8, join as joinPath } from "path";
31035
+ import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile9, open, rename as rename3 } from "fs/promises";
31036
+ import { dirname as dirname9, join as joinPath } from "path";
31039
31037
  import { fileURLToPath as fileURLToPath2 } from "url";
31040
31038
 
31041
31039
  // src/cleanup.ts
@@ -31792,14 +31790,317 @@ async function detach(input, options = {}) {
31792
31790
  };
31793
31791
  }
31794
31792
 
31793
+ // src/upgradeSea.ts
31794
+ init_esm_shims();
31795
+ import { createHash as createHash3 } from "crypto";
31796
+ import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename2, rm as rm2, writeFile as writeFile8 } from "fs/promises";
31797
+ import { join as join4 } from "path";
31798
+
31799
+ // src/channel.ts
31800
+ init_esm_shims();
31801
+ import { readFile as readFile7, writeFile as writeFile7, mkdir as mkdir8 } from "fs/promises";
31802
+ import { dirname as dirname8 } from "path";
31803
+ var DEFAULT_CHANNEL = "latest";
31804
+ var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
31805
+ function parseChannel(raw) {
31806
+ const v = raw.trim();
31807
+ if (v === "latest" || v === "alpha") return v;
31808
+ if (v.startsWith("pinned:")) {
31809
+ const semver = v.slice("pinned:".length);
31810
+ if (SEMVER_RE.test(semver)) return `pinned:${semver}`;
31811
+ return null;
31812
+ }
31813
+ return null;
31814
+ }
31815
+ async function readChannel(slockHome) {
31816
+ try {
31817
+ const raw = await readFile7(channelPath(slockHome), "utf8");
31818
+ const parsed = parseChannel(raw);
31819
+ if (parsed !== null) return parsed;
31820
+ } catch {
31821
+ }
31822
+ return DEFAULT_CHANNEL;
31823
+ }
31824
+ async function writeChannel(slockHome, channel2) {
31825
+ const p = channelPath(slockHome);
31826
+ await mkdir8(dirname8(p), { recursive: true });
31827
+ await writeFile7(p, `${channel2}
31828
+ `, { mode: 384 });
31829
+ }
31830
+ async function runChannelShow(slockHome) {
31831
+ void computerDir;
31832
+ const channel2 = await readChannel(slockHome);
31833
+ info(channel2);
31834
+ }
31835
+ async function runChannelSet(slockHome, raw) {
31836
+ const parsed = parseChannel(raw);
31837
+ if (parsed === null) {
31838
+ fail(
31839
+ "CHANNEL_INVALID",
31840
+ `Invalid channel "${raw}". Accepted: \`latest\`, \`alpha\`, or \`pinned:<semver>\` (e.g. pinned:0.52.2).`
31841
+ );
31842
+ }
31843
+ await writeChannel(slockHome, parsed);
31844
+ info(`Channel set to ${parsed}.`);
31845
+ info(`Note: service reads channel at startup; restart \`slock-computer start\` to apply.`);
31846
+ }
31847
+
31848
+ // src/upgradeSea.ts
31849
+ var DEFAULT_UPGRADE_BASE_URL = "https://cdn.slock.ai/computer";
31850
+ var UPGRADE_BASE_URL_ENV = "SLOCK_COMPUTER_UPGRADE_BASE_URL";
31851
+ function resolveUpgradeBaseUrl(env = process.env) {
31852
+ const override = env[UPGRADE_BASE_URL_ENV];
31853
+ if (typeof override === "string" && override.trim().length > 0) {
31854
+ return override.trim().replace(/\/+$/, "");
31855
+ }
31856
+ return DEFAULT_UPGRADE_BASE_URL;
31857
+ }
31858
+ async function stageSeaPhase(stagingDir, version, platform, arch, deps = {}) {
31859
+ const fetchFn = deps.fetchFn ?? fetch;
31860
+ const baseUrl = deps.baseUrl ?? resolveUpgradeBaseUrl();
31861
+ const targetKey = `${platform}-${arch}`;
31862
+ const manifestUrl = `${baseUrl}/${version}/manifest.json`;
31863
+ const manifestRes = await fetchFn(manifestUrl);
31864
+ if (!manifestRes.ok) {
31865
+ throw new Error(`UPGRADE_MANIFEST_FETCH_FAILED: ${manifestRes.status} for ${manifestUrl}`);
31866
+ }
31867
+ const manifest = await manifestRes.json();
31868
+ const target = manifest.targets?.[targetKey];
31869
+ if (!target || typeof target.file !== "string" || typeof target.sha256 !== "string") {
31870
+ throw new Error(
31871
+ `UPGRADE_NO_BINARY_FOR_PLATFORM: manifest for ${version} has no usable binary for ${targetKey}`
31872
+ );
31873
+ }
31874
+ const binaryUrl = `${baseUrl}/${version}/${target.file}`;
31875
+ const binRes = await fetchFn(binaryUrl);
31876
+ if (!binRes.ok) {
31877
+ throw new Error(`UPGRADE_DOWNLOAD_FAILED: ${binRes.status} for ${binaryUrl}`);
31878
+ }
31879
+ const onProgress = deps.onProgress;
31880
+ const progressRequestId = deps.requestId ?? "";
31881
+ const emitDownloadProgress = (downloaded, total) => {
31882
+ if (!onProgress) return;
31883
+ if (total == null || total <= 0) {
31884
+ onProgress({ requestId: progressRequestId, phase: "downloading" });
31885
+ return;
31886
+ }
31887
+ onProgress({
31888
+ requestId: progressRequestId,
31889
+ phase: "downloading",
31890
+ percent: Math.min(100, Math.floor(downloaded / total * 100))
31891
+ });
31892
+ };
31893
+ let bytes;
31894
+ const body = binRes.body;
31895
+ if (onProgress && body) {
31896
+ const totalHeader = binRes.headers?.get?.("content-length");
31897
+ const total = totalHeader != null && /^\d+$/.test(totalHeader) ? Number(totalHeader) : null;
31898
+ const reader = body.getReader();
31899
+ const chunks = [];
31900
+ let downloaded = 0;
31901
+ const THROTTLE_MS = 200;
31902
+ let lastEmit = 0;
31903
+ try {
31904
+ for (; ; ) {
31905
+ const { done, value } = await reader.read();
31906
+ if (done) break;
31907
+ if (value) {
31908
+ chunks.push(value);
31909
+ downloaded += value.byteLength;
31910
+ const now = Date.now();
31911
+ if (now - lastEmit >= THROTTLE_MS) {
31912
+ lastEmit = now;
31913
+ emitDownloadProgress(downloaded, total);
31914
+ }
31915
+ }
31916
+ }
31917
+ } finally {
31918
+ await reader.cancel().catch(() => {
31919
+ });
31920
+ }
31921
+ emitDownloadProgress(downloaded, total);
31922
+ bytes = Buffer.concat(chunks);
31923
+ } else {
31924
+ bytes = Buffer.from(await binRes.arrayBuffer());
31925
+ }
31926
+ const actualSha = createHash3("sha256").update(bytes).digest("hex");
31927
+ if (actualSha !== target.sha256) {
31928
+ throw new Error(
31929
+ `UPGRADE_SHA_MISMATCH: ${targetKey} ${version} expected ${target.sha256} got ${actualSha}`
31930
+ );
31931
+ }
31932
+ await mkdir9(stagingDir, { recursive: true });
31933
+ const stagedBinaryPath = join4(stagingDir, target.file);
31934
+ await writeFile8(stagedBinaryPath, bytes, { mode: 493 });
31935
+ await chmod4(stagedBinaryPath, 493);
31936
+ return { stagedBinaryPath, version, sha256: actualSha };
31937
+ }
31938
+ async function swapSeaPhase(currentBinaryPath, stagedBinaryPath) {
31939
+ const prevBinaryPath = `${currentBinaryPath}.prev`;
31940
+ await rm2(prevBinaryPath, { force: true });
31941
+ await rename2(currentBinaryPath, prevBinaryPath);
31942
+ try {
31943
+ await rename2(stagedBinaryPath, currentBinaryPath);
31944
+ await chmod4(currentBinaryPath, 493);
31945
+ } catch (err) {
31946
+ await rename2(prevBinaryPath, currentBinaryPath).catch(() => {
31947
+ });
31948
+ throw err;
31949
+ }
31950
+ return { prevBinaryPath, currentBinaryPath };
31951
+ }
31952
+ function pendingUpgradeMarkerPath(slockHome) {
31953
+ return join4(slockHome, "upgrade-pending.json");
31954
+ }
31955
+ async function writePendingUpgradeMarker(slockHome, marker) {
31956
+ const path3 = pendingUpgradeMarkerPath(slockHome);
31957
+ const tmp = `${path3}.tmp`;
31958
+ await mkdir9(slockHome, { recursive: true });
31959
+ await writeFile8(tmp, `${JSON.stringify(marker, null, 2)}
31960
+ `, "utf8");
31961
+ await rename2(tmp, path3);
31962
+ }
31963
+ async function readPendingUpgradeMarker(slockHome) {
31964
+ try {
31965
+ const raw = await readFile8(pendingUpgradeMarkerPath(slockHome), "utf8");
31966
+ const parsed = JSON.parse(raw);
31967
+ if (typeof parsed.requestId === "string" && typeof parsed.targetVersion === "string" && typeof parsed.fromVersion === "string" && typeof parsed.startedAt === "string") {
31968
+ return parsed;
31969
+ }
31970
+ return null;
31971
+ } catch {
31972
+ return null;
31973
+ }
31974
+ }
31975
+ async function clearPendingUpgradeMarker(slockHome) {
31976
+ await rm2(pendingUpgradeMarkerPath(slockHome), { force: true });
31977
+ }
31978
+ async function runSeaUpgrade(opts) {
31979
+ const d = opts.deps ?? {};
31980
+ const platform = opts.platform ?? process.platform;
31981
+ const arch = opts.arch ?? process.arch;
31982
+ const emit7 = (phase, message) => d.onProgress?.({ requestId: opts.requestId, phase, message });
31983
+ const stagingDir = d.stagingDir ?? join4(opts.slockHome, "upgrade-staging", opts.targetVersion);
31984
+ emit7("downloading", `downloading ${opts.targetVersion}`);
31985
+ let staged;
31986
+ try {
31987
+ staged = await (d.stageFn ?? stageSeaPhase)(stagingDir, opts.targetVersion, platform, arch, {
31988
+ fetchFn: d.fetchFn,
31989
+ baseUrl: d.baseUrl,
31990
+ // Thread requestId + the progress sink so the stage can stream throttled
31991
+ // byte-% `downloading` frames (otherwise the bar sits static at the
31992
+ // downloading milestone for the whole download).
31993
+ requestId: opts.requestId,
31994
+ onProgress: d.onProgress
31995
+ });
31996
+ } catch (e) {
31997
+ return { ok: false, failedPhase: "stage", reason: e instanceof Error ? e.message : String(e) };
31998
+ }
31999
+ emit7("verifying", `verified sha256 ${staged.sha256.slice(0, 12)}`);
32000
+ await writePendingUpgradeMarker(opts.slockHome, {
32001
+ requestId: opts.requestId,
32002
+ fromVersion: opts.fromVersion,
32003
+ targetVersion: opts.targetVersion,
32004
+ startedAt: opts.nowIso
32005
+ });
32006
+ emit7("applying", "swapping binary");
32007
+ let swap;
32008
+ try {
32009
+ swap = await (d.swapFn ?? swapSeaPhase)(opts.currentBinaryPath, staged.stagedBinaryPath);
32010
+ } catch (e) {
32011
+ await clearPendingUpgradeMarker(opts.slockHome).catch(() => {
32012
+ });
32013
+ return { ok: false, failedPhase: "swap", reason: e instanceof Error ? e.message : String(e) };
32014
+ }
32015
+ emit7("restarting", "restarting service on new version");
32016
+ return { ok: true, swap };
32017
+ }
32018
+ async function fetchComputerDistTags(fetchFn = fetch) {
32019
+ const url = "https://registry.npmjs.org/@slock-ai/computer";
32020
+ const controller = new AbortController();
32021
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
32022
+ try {
32023
+ const res = await fetchFn(url, { signal: controller.signal, headers: { accept: "application/json" } });
32024
+ if (!res.ok) return null;
32025
+ const data = await res.json();
32026
+ return data["dist-tags"] ?? null;
32027
+ } catch {
32028
+ return null;
32029
+ } finally {
32030
+ clearTimeout(timeoutId);
32031
+ }
32032
+ }
32033
+ async function resolveSeaTargetVersion(channel2, fetchDistTags = () => fetchComputerDistTags()) {
32034
+ if (channel2.startsWith("pinned:")) {
32035
+ const v = channel2.slice("pinned:".length);
32036
+ return SEMVER_RE.test(v) ? v : null;
32037
+ }
32038
+ const tags = await fetchDistTags();
32039
+ const resolved = tags?.[channel2];
32040
+ return typeof resolved === "string" && resolved.length > 0 ? resolved : null;
32041
+ }
32042
+ async function resolveAndRunSeaUpgrade(opts) {
32043
+ const d = opts.deps ?? {};
32044
+ let targetVersion;
32045
+ try {
32046
+ if (d.resolveTargetVersion) {
32047
+ targetVersion = await d.resolveTargetVersion();
32048
+ } else {
32049
+ const channel2 = await (d.readChannelFn ?? readChannel)(opts.slockHome);
32050
+ targetVersion = await resolveSeaTargetVersion(channel2, d.fetchDistTags);
32051
+ }
32052
+ } catch (e) {
32053
+ return { ok: false, failedPhase: "resolve", reason: e instanceof Error ? e.message : String(e) };
32054
+ }
32055
+ if (!targetVersion) {
32056
+ return { ok: false, failedPhase: "resolve", reason: "UPGRADE_VERSION_RESOLVE_FAILED" };
32057
+ }
32058
+ if (targetVersion === opts.fromVersion) {
32059
+ return { ok: true, alreadyCurrent: true, targetVersion };
32060
+ }
32061
+ const result = await (d.runSeaUpgradeFn ?? runSeaUpgrade)({
32062
+ slockHome: opts.slockHome,
32063
+ requestId: opts.requestId,
32064
+ fromVersion: opts.fromVersion,
32065
+ targetVersion,
32066
+ currentBinaryPath: opts.currentBinaryPath,
32067
+ nowIso: opts.nowIso,
32068
+ platform: opts.platform,
32069
+ arch: opts.arch,
32070
+ deps: {
32071
+ baseUrl: d.baseUrl,
32072
+ fetchFn: d.fetchFn,
32073
+ onProgress: d.onProgress,
32074
+ stageFn: d.stageFn,
32075
+ swapFn: d.swapFn,
32076
+ stagingDir: d.stagingDir
32077
+ }
32078
+ });
32079
+ return { ...result, targetVersion };
32080
+ }
32081
+
31795
32082
  // src/service.ts
31796
- function buildResidentSpawn(mode, serverId, selfEntry = process.argv[1] ?? "", execArgv = process.execArgv) {
32083
+ var seaRequire = createRequire2(import.meta.url);
32084
+ var cachedIsSea;
32085
+ function isSeaBinary() {
32086
+ if (cachedIsSea !== void 0) return cachedIsSea;
32087
+ try {
32088
+ cachedIsSea = seaRequire("node:sea").isSea();
32089
+ } catch {
32090
+ cachedIsSea = false;
32091
+ }
32092
+ return cachedIsSea;
32093
+ }
32094
+ function buildResidentSpawn(mode, serverId, selfEntry = process.argv[1] ?? "", execArgv = process.execArgv, isSea = isSeaBinary()) {
31797
32095
  const tail = serverId ? [mode, serverId] : [mode];
32096
+ if (isSea) {
32097
+ return { command: process.execPath, args: [...execArgv, ...tail] };
32098
+ }
31798
32099
  return { command: process.execPath, args: [...execArgv, selfEntry, ...tail] };
31799
32100
  }
31800
32101
  var PARENT_LOCK_HELD_ENV_VAR = "SLOCK_COMPUTER_PARENT_MUTATION_LOCK_HELD";
31801
32102
  async function spawnDetachedService(slockHome) {
31802
- await mkdir8(serviceRunDir(slockHome), { recursive: true });
32103
+ await mkdir10(serviceRunDir(slockHome), { recursive: true });
31803
32104
  const supLogFd = await open(serviceLogPath(slockHome), "a");
31804
32105
  const { command, args } = buildResidentSpawn("__service", null);
31805
32106
  const child = spawn2(command, args, {
@@ -31824,7 +32125,22 @@ async function spawnDetachedService(slockHome) {
31824
32125
  return pid;
31825
32126
  }
31826
32127
  var EX_CONFIG_EXIT_CODE = 78;
32128
+ async function ensureSeaRuntimePackageDir() {
32129
+ if (!isSeaBinary() || process.env.PI_PACKAGE_DIR) return;
32130
+ try {
32131
+ const dir = joinPath(resolveSlockHome(), "runtime-pkg");
32132
+ await mkdir10(dir, { recursive: true });
32133
+ await writeFile9(
32134
+ joinPath(dir, "package.json"),
32135
+ `${JSON.stringify({ name: "slock-computer-sea-runtime", version: COMPUTER_VERSION })}
32136
+ `
32137
+ );
32138
+ process.env.PI_PACKAGE_DIR = dir;
32139
+ } catch {
32140
+ }
32141
+ }
31827
32142
  var defaultCoreFactory = async (creds) => {
32143
+ await ensureSeaRuntimePackageDir();
31828
32144
  let coreMod;
31829
32145
  try {
31830
32146
  coreMod = await import("@slock-ai/daemon/core");
@@ -31849,37 +32165,85 @@ var defaultCoreFactory = async (creds) => {
31849
32165
  serverUrl: creds.serverUrl,
31850
32166
  apiKey: creds.apiKey,
31851
32167
  localTrace: true,
32168
+ // In a SEA single-binary there is no sidecar `slock` CLI script for the
32169
+ // daemon to resolve, so inject the `__cli` sentinel: the agent CLI wrapper
32170
+ // then execs `<exe> __cli "$@"`, re-execing this binary in CLI mode
32171
+ // (runBundledSlockCli). Normal installs leave this unset → the daemon
32172
+ // resolves the bundled CLI dist path as before.
32173
+ slockCliPath: isSeaBinary() ? "__cli" : void 0,
31852
32174
  // Managed-Computer remote control (server → WS → runner). This runner
31853
32175
  // is the service's in-process `__run` child, so:
31854
32176
  // restart → SIGTERM ourselves → runResident's shutdown does
31855
32177
  // core.stop() + exit 0 → classifyRunnerExit = graceful → the
31856
32178
  // service supervisor respawns this runner (effective restart).
31857
- // upgrade → launch `slock-computer upgrade` out-of-band (detached)
31858
- // so the §12 drain+swap+restart runs outside this event loop.
31859
- onComputerControl: (action) => {
32179
+ // upgrade (SEA + requestId) run the SEA upgrade IN-PROCESS so we can
32180
+ // stream `computer:upgrade:progress` over this live WS, then SIGTERM
32181
+ // ourselves; the supervisor respawns the swapped binary and the new
32182
+ // process reports `done` via onComputerUpgradeReconcile on reconnect.
32183
+ // upgrade (non-SEA dev run, or no requestId) → SEA-only no-op. The
32184
+ // npm-install-root upgrade path is retired: a managed Computer is a
32185
+ // SEA single binary; a source/dev run has no binary to self-swap.
32186
+ onComputerControl: (action, ctx) => {
31860
32187
  if (action === "restart") {
31861
32188
  process.kill(process.pid, "SIGTERM");
31862
32189
  return;
31863
32190
  }
31864
- try {
31865
- const selfEntry = process.argv[1] ?? "";
31866
- const child = spawn2(process.execPath, [...process.execArgv, selfEntry, "upgrade"], {
31867
- detached: true,
31868
- stdio: "ignore",
31869
- windowsHide: true,
31870
- // Attribute this upgrade to the web surface in upgrade.log — the
31871
- // `upgrade` command reads SLOCK_UPGRADE_TRIGGER and defaults to
31872
- // "cli" when unset.
31873
- env: { ...process.env, SLOCK_UPGRADE_TRIGGER: "web" }
31874
- });
31875
- child.on("error", () => {
31876
- });
31877
- child.unref();
31878
- } catch {
32191
+ if (isSeaBinary() && ctx.requestId) {
32192
+ return runManagedSeaUpgrade(ctx.requestId, ctx);
31879
32193
  }
32194
+ console.warn(
32195
+ `[Computer] Ignoring upgrade request: in-process SEA upgrade unavailable (isSea=${isSeaBinary()}, requestId=${ctx.requestId ? "present" : "absent"}). Managed upgrade requires a SEA binary invoked with a requestId.`
32196
+ );
32197
+ },
32198
+ // Blip-stitch: on every (re)connect, if THIS process booted from a freshly
32199
+ // swapped binary (a pending-upgrade marker is present), report the upgrade
32200
+ // done with the version we actually are now, then clear the marker.
32201
+ onComputerUpgradeReconcile: async (emitDone) => {
32202
+ const slockHome = resolveSlockHome();
32203
+ const marker = await readPendingUpgradeMarker(slockHome);
32204
+ if (!marker) return;
32205
+ const rolledBack = COMPUTER_VERSION !== marker.targetVersion;
32206
+ emitDone({
32207
+ requestId: marker.requestId,
32208
+ ok: !rolledBack,
32209
+ newVersion: COMPUTER_VERSION,
32210
+ ...rolledBack ? { rolledBack: true } : {}
32211
+ });
32212
+ await clearPendingUpgradeMarker(slockHome);
31880
32213
  }
31881
32214
  });
31882
32215
  };
32216
+ function seaProgressToFrame(e) {
32217
+ return { phase: e.phase, message: e.message, percent: e.percent };
32218
+ }
32219
+ async function runManagedSeaUpgrade(requestId, ctx) {
32220
+ const slockHome = resolveSlockHome();
32221
+ let result;
32222
+ try {
32223
+ result = await resolveAndRunSeaUpgrade({
32224
+ slockHome,
32225
+ requestId,
32226
+ fromVersion: COMPUTER_VERSION,
32227
+ currentBinaryPath: process.execPath,
32228
+ nowIso: (/* @__PURE__ */ new Date()).toISOString(),
32229
+ deps: {
32230
+ onProgress: (e) => ctx.emitUpgradeProgress(seaProgressToFrame(e))
32231
+ }
32232
+ });
32233
+ } catch (err) {
32234
+ ctx.emitUpgradeDone({ ok: false, error: err instanceof Error ? err.message : String(err) });
32235
+ return;
32236
+ }
32237
+ if (!result.ok) {
32238
+ ctx.emitUpgradeDone({ ok: false, error: result.reason ?? result.failedPhase ?? "upgrade_failed" });
32239
+ return;
32240
+ }
32241
+ if (result.alreadyCurrent) {
32242
+ ctx.emitUpgradeDone({ ok: true, newVersion: COMPUTER_VERSION });
32243
+ return;
32244
+ }
32245
+ process.kill(process.pid, "SIGTERM");
32246
+ }
31883
32247
  function classifyRunnerExit(code, signal) {
31884
32248
  if (code === EX_CONFIG_EXIT_CODE) return "config-error";
31885
32249
  if (signal === "SIGTERM" || signal === "SIGINT") return "graceful";
@@ -31918,6 +32282,9 @@ async function runResident(serverId, deps = {}) {
31918
32282
  }
31919
32283
  var RECONCILE_INTERVAL_MS = 5e3;
31920
32284
  var CHILD_RESTART_BACKOFF_MS = 2e3;
32285
+ function canSupervisorSpawnChild(serverId, running, restarting) {
32286
+ return !running.has(serverId) && !restarting.has(serverId);
32287
+ }
31921
32288
  function formatReadySummary(ready, serverIds, opts) {
31922
32289
  if (opts.serverId && serverIds.length === 1) {
31923
32290
  const pid = ready.get(opts.serverId);
@@ -31958,10 +32325,10 @@ async function runServiceStartupRecovery(slockHome) {
31958
32325
  }
31959
32326
  async function resolveServiceIdentity() {
31960
32327
  const here = fileURLToPath2(import.meta.url);
31961
- const installRoot = dirname8(dirname8(here));
32328
+ const installRoot = dirname9(dirname9(here));
31962
32329
  let version = null;
31963
32330
  try {
31964
- const raw = await readFile7(joinPath(installRoot, "package.json"), "utf8");
32331
+ const raw = await readFile9(joinPath(installRoot, "package.json"), "utf8");
31965
32332
  const parsed = JSON.parse(raw);
31966
32333
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
31967
32334
  version = parsed.version;
@@ -31981,8 +32348,8 @@ async function writeServiceVersionEvidence(slockHome) {
31981
32348
  };
31982
32349
  const dest = serviceVersionPath(slockHome);
31983
32350
  const tmp = `${dest}.tmp`;
31984
- await writeFile7(tmp, JSON.stringify(payload) + "\n", { mode: 384 });
31985
- await rename2(tmp, dest);
32351
+ await writeFile9(tmp, JSON.stringify(payload) + "\n", { mode: 384 });
32352
+ await rename3(tmp, dest);
31986
32353
  } catch (err) {
31987
32354
  const msg = err instanceof Error ? err.message : String(err);
31988
32355
  process.stderr.write(
@@ -31991,34 +32358,19 @@ async function writeServiceVersionEvidence(slockHome) {
31991
32358
  );
31992
32359
  }
31993
32360
  }
31994
- async function readServiceVersionEvidence(slockHome) {
31995
- try {
31996
- const raw = await readFile7(serviceVersionPath(slockHome), "utf8");
31997
- const parsed = JSON.parse(raw);
31998
- if (typeof parsed.installRoot !== "string" || typeof parsed.pid !== "number" || typeof parsed.writtenAt !== "string" || parsed.version !== null && typeof parsed.version !== "string") {
31999
- return null;
32000
- }
32001
- return {
32002
- version: parsed.version ?? null,
32003
- installRoot: parsed.installRoot,
32004
- pid: parsed.pid,
32005
- writtenAt: parsed.writtenAt
32006
- };
32007
- } catch {
32008
- return null;
32009
- }
32010
- }
32011
32361
  async function runService() {
32012
32362
  const slockHome = resolveSlockHome();
32013
- await mkdir8(serviceRunDir(slockHome), { recursive: true });
32363
+ await mkdir10(serviceRunDir(slockHome), { recursive: true });
32014
32364
  await runServiceStartupRecovery(slockHome);
32015
32365
  await writePidfileAt(servicePidPath(slockHome), process.pid);
32016
32366
  await writeServiceVersionEvidence(slockHome);
32017
32367
  const children = /* @__PURE__ */ new Map();
32368
+ const restarting = /* @__PURE__ */ new Set();
32018
32369
  const { [PARENT_LOCK_HELD_ENV_VAR]: _parentLockMarker, ...childEnv } = process.env;
32019
32370
  const spawnChild = async (serverId) => {
32371
+ if (children.has(serverId)) return;
32020
32372
  const logPath = serverRunnerLogPath(slockHome, serverId);
32021
- await mkdir8(dirname8(logPath), { recursive: true });
32373
+ await mkdir10(dirname9(logPath), { recursive: true });
32022
32374
  const logFd = await open(logPath, "a");
32023
32375
  const { command, args } = buildResidentSpawn("__run", serverId);
32024
32376
  const child = spawn2(command, args, {
@@ -32049,9 +32401,14 @@ async function runService() {
32049
32401
  return;
32050
32402
  }
32051
32403
  if (classification === "graceful") {
32052
- await new Promise((r) => setTimeout(r, CHILD_RESTART_BACKOFF_MS));
32053
- if (await readServerAttachment(slockHome, serverId) && !shuttingDown) {
32054
- await spawnChild(serverId);
32404
+ restarting.add(serverId);
32405
+ try {
32406
+ await new Promise((r) => setTimeout(r, CHILD_RESTART_BACKOFF_MS));
32407
+ if (await readServerAttachment(slockHome, serverId) && !shuttingDown) {
32408
+ await spawnChild(serverId);
32409
+ }
32410
+ } finally {
32411
+ restarting.delete(serverId);
32055
32412
  }
32056
32413
  return;
32057
32414
  }
@@ -32066,9 +32423,14 @@ async function runService() {
32066
32423
  );
32067
32424
  return;
32068
32425
  }
32069
- await new Promise((r) => setTimeout(r, CHILD_RESTART_BACKOFF_MS));
32070
- if (await readServerAttachment(slockHome, serverId) && !shuttingDown) {
32071
- await spawnChild(serverId);
32426
+ restarting.add(serverId);
32427
+ try {
32428
+ await new Promise((r) => setTimeout(r, CHILD_RESTART_BACKOFF_MS));
32429
+ if (await readServerAttachment(slockHome, serverId) && !shuttingDown) {
32430
+ await spawnChild(serverId);
32431
+ }
32432
+ } finally {
32433
+ restarting.delete(serverId);
32072
32434
  }
32073
32435
  })();
32074
32436
  });
@@ -32083,7 +32445,7 @@ async function runService() {
32083
32445
  const reconcile = async () => {
32084
32446
  const wanted = new Set(await listManagedServerIds(slockHome));
32085
32447
  for (const id of wanted) {
32086
- if (!children.has(id)) await spawnChild(id);
32448
+ if (canSupervisorSpawnChild(id, children, restarting)) await spawnChild(id);
32087
32449
  }
32088
32450
  for (const [id, handle] of children) {
32089
32451
  if (!wanted.has(id)) killChild(handle);
@@ -32219,7 +32581,7 @@ async function runDetach(serverId, serverLabel = serverId) {
32219
32581
  var USER_SESSION_EXPIRY_LEEWAY_MS = 3e4;
32220
32582
  async function readUserSessionAuth(slockHome) {
32221
32583
  try {
32222
- const parsed = JSON.parse(await readFile8(userSessionPath(slockHome), "utf8"));
32584
+ const parsed = JSON.parse(await readFile10(userSessionPath(slockHome), "utf8"));
32223
32585
  return {
32224
32586
  accessToken: typeof parsed.accessToken === "string" ? parsed.accessToken : "",
32225
32587
  ...typeof parsed.serverUrl === "string" ? { serverUrl: parsed.serverUrl } : {}
@@ -32230,7 +32592,7 @@ async function readUserSessionAuth(slockHome) {
32230
32592
  }
32231
32593
  async function hasValidUserSession(slockHome) {
32232
32594
  try {
32233
- const parsed = JSON.parse(await readFile8(userSessionPath(slockHome), "utf8"));
32595
+ const parsed = JSON.parse(await readFile10(userSessionPath(slockHome), "utf8"));
32234
32596
  return parsed.kind === "user-session" && typeof parsed.accessToken === "string" && parsed.accessToken.length > 0 && !isJwtExpired(parsed.accessToken);
32235
32597
  } catch {
32236
32598
  return false;
@@ -32250,7 +32612,7 @@ async function refreshUserSession(slockHome, serverUrl) {
32250
32612
  const file = userSessionPath(slockHome);
32251
32613
  let session;
32252
32614
  try {
32253
- session = JSON.parse(await readFile8(file, "utf8"));
32615
+ session = JSON.parse(await readFile10(file, "utf8"));
32254
32616
  } catch {
32255
32617
  return false;
32256
32618
  }
@@ -32269,8 +32631,8 @@ async function refreshUserSession(slockHome, serverUrl) {
32269
32631
  if (res.status !== 200 || typeof body?.accessToken !== "string" || typeof body.refreshToken !== "string") {
32270
32632
  return false;
32271
32633
  }
32272
- await mkdir9(dirname9(file), { recursive: true });
32273
- await writeFile8(
32634
+ await mkdir11(dirname10(file), { recursive: true });
32635
+ await writeFile10(
32274
32636
  tmpFile,
32275
32637
  JSON.stringify(
32276
32638
  {
@@ -32287,11 +32649,11 @@ async function refreshUserSession(slockHome, serverUrl) {
32287
32649
  ),
32288
32650
  { mode: 384 }
32289
32651
  );
32290
- await chmod4(tmpFile, 384);
32291
- await rename3(tmpFile, file);
32652
+ await chmod5(tmpFile, 384);
32653
+ await rename4(tmpFile, file);
32292
32654
  return true;
32293
32655
  } catch {
32294
- await rm2(tmpFile, { force: true }).catch(() => void 0);
32656
+ await rm3(tmpFile, { force: true }).catch(() => void 0);
32295
32657
  return false;
32296
32658
  }
32297
32659
  }
@@ -32599,10 +32961,10 @@ async function runSetup(opts, deps = {}) {
32599
32961
 
32600
32962
  // src/status.ts
32601
32963
  init_esm_shims();
32602
- import { readFile as readFile9 } from "fs/promises";
32964
+ import { readFile as readFile11 } from "fs/promises";
32603
32965
  async function readUserSession(path3) {
32604
32966
  try {
32605
- const parsed = JSON.parse(await readFile9(path3, "utf8"));
32967
+ const parsed = JSON.parse(await readFile11(path3, "utf8"));
32606
32968
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
32607
32969
  return { state: "present", session: parsed, error: null };
32608
32970
  }
@@ -32962,15 +33324,10 @@ async function runDoctorChecks() {
32962
33324
  }
32963
33325
  async function runDoctor(opts) {
32964
33326
  const slockHome = resolveSlockHome();
32965
- if (opts.resetHealth && opts.serverId) {
32966
- await resetHealth(slockHome, opts.serverId);
32967
- info(`Reset health state for server ${opts.serverLabel ?? opts.serverId}.`);
32968
- info(`Service will resume auto-restart on next daemon exit.`);
32969
- }
32970
33327
  const checks = await runDoctorChecks();
32971
33328
  const allOk = checks.every((c) => c.ok);
32972
33329
  let cleanupReport = null;
32973
- if (opts.cleanup || opts.fix) {
33330
+ if (opts.cleanup) {
32974
33331
  cleanupReport = await runFullCleanup(slockHome);
32975
33332
  }
32976
33333
  let crashes = [];
@@ -33000,7 +33357,7 @@ async function runDoctor(opts) {
33000
33357
  info(` - ${c.at}${code}${sig}`);
33001
33358
  }
33002
33359
  info(` \u2192 Once you fix the underlying issue, run`);
33003
- info(` \`slock-computer doctor ${opts.serverLabel ?? opts.serverId} --reset-health\` to resume auto-restart.`);
33360
+ info(` \`slock-computer reset --runner --server ${opts.serverLabel ?? opts.serverId}\` to resume auto-restart.`);
33004
33361
  }
33005
33362
  if (cleanupReport) {
33006
33363
  info("");
@@ -33035,14 +33392,14 @@ async function runDoctor(opts) {
33035
33392
 
33036
33393
  // src/logs.ts
33037
33394
  init_esm_shims();
33038
- import { readFile as readFile10 } from "fs/promises";
33395
+ import { readFile as readFile12 } from "fs/promises";
33039
33396
  var DEFAULT_LINES = 200;
33040
33397
  async function runLogs(opts) {
33041
33398
  const home = resolveSlockHome();
33042
33399
  const file = opts.service ? serviceLogPath(home) : serverRunnerLogPath(home, await resolveTargetServerId({ server: opts.server }));
33043
33400
  let content;
33044
33401
  try {
33045
- content = await readFile10(file, "utf8");
33402
+ content = await readFile12(file, "utf8");
33046
33403
  } catch {
33047
33404
  fail(
33048
33405
  "NO_DAEMON_LOG",
@@ -33061,8 +33418,8 @@ init_esm_shims();
33061
33418
 
33062
33419
  // src/serviceState.ts
33063
33420
  init_esm_shims();
33064
- import { mkdir as mkdir10, readFile as readFile11, writeFile as writeFile9, appendFile as appendFile3 } from "fs/promises";
33065
- import { dirname as dirname10 } from "path";
33421
+ import { mkdir as mkdir12, readFile as readFile13, writeFile as writeFile11, appendFile as appendFile3 } from "fs/promises";
33422
+ import { dirname as dirname11 } from "path";
33066
33423
 
33067
33424
  // src/lib/state.ts
33068
33425
  init_esm_shims();
@@ -33084,7 +33441,7 @@ var DEFAULT_STATE = {
33084
33441
  };
33085
33442
  async function readServiceState(slockHome) {
33086
33443
  try {
33087
- const raw = await readFile11(serviceStatePath(slockHome), "utf8");
33444
+ const raw = await readFile13(serviceStatePath(slockHome), "utf8");
33088
33445
  const parsed = JSON.parse(raw);
33089
33446
  if (!parsed || typeof parsed !== "object") return { ...DEFAULT_STATE };
33090
33447
  const obj = parsed;
@@ -33097,8 +33454,8 @@ async function readServiceState(slockHome) {
33097
33454
  }
33098
33455
  async function writeServiceState(slockHome, file) {
33099
33456
  const path3 = serviceStatePath(slockHome);
33100
- await mkdir10(dirname10(path3), { recursive: true });
33101
- await writeFile9(path3, JSON.stringify(file), { mode: 384 });
33457
+ await mkdir12(dirname11(path3), { recursive: true });
33458
+ await writeFile11(path3, JSON.stringify(file), { mode: 384 });
33102
33459
  }
33103
33460
  function isCrashEntry(value) {
33104
33461
  if (!value || typeof value !== "object") return false;
@@ -33115,7 +33472,7 @@ async function emitServiceStateTransition(slockHome, fromState, toState, trigger
33115
33472
  };
33116
33473
  try {
33117
33474
  const path3 = serviceLogPath(slockHome);
33118
- await mkdir10(dirname10(path3), { recursive: true });
33475
+ await mkdir12(dirname11(path3), { recursive: true });
33119
33476
  await appendFile3(path3, JSON.stringify(entry) + "\n");
33120
33477
  } catch {
33121
33478
  }
@@ -33209,14 +33566,14 @@ async function runReset(opts) {
33209
33566
  // src/concurrency.ts
33210
33567
  init_esm_shims();
33211
33568
  var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
33212
- import { mkdir as mkdir11 } from "fs/promises";
33213
- import { join as join4 } from "path";
33569
+ import { mkdir as mkdir13 } from "fs/promises";
33570
+ import { join as join5 } from "path";
33214
33571
  var STALE_LOCK_THRESHOLD_MS = 6e4;
33215
33572
  async function withMutationLock(fn) {
33216
33573
  const slockHome = resolveSlockHome();
33217
33574
  const lockTarget = computerDir(slockHome);
33218
- await mkdir11(lockTarget, { recursive: true });
33219
- const lockfilePath = join4(lockTarget, ".lock");
33575
+ await mkdir13(lockTarget, { recursive: true });
33576
+ const lockfilePath = join5(lockTarget, ".lock");
33220
33577
  let release = null;
33221
33578
  try {
33222
33579
  release = await import_proper_lockfile.default.lock(lockTarget, {
@@ -33252,1060 +33609,15 @@ async function withMutationLock(fn) {
33252
33609
  }
33253
33610
  }
33254
33611
 
33255
- // src/channel.ts
33256
- init_esm_shims();
33257
- import { readFile as readFile12, writeFile as writeFile10, mkdir as mkdir12 } from "fs/promises";
33258
- import { dirname as dirname11 } from "path";
33259
- var DEFAULT_CHANNEL = "latest";
33260
- var SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
33261
- function parseChannel(raw) {
33262
- const v = raw.trim();
33263
- if (v === "latest" || v === "alpha") return v;
33264
- if (v.startsWith("pinned:")) {
33265
- const semver = v.slice("pinned:".length);
33266
- if (SEMVER_RE.test(semver)) return `pinned:${semver}`;
33267
- return null;
33268
- }
33269
- return null;
33270
- }
33271
- async function readChannel(slockHome) {
33272
- try {
33273
- const raw = await readFile12(channelPath(slockHome), "utf8");
33274
- const parsed = parseChannel(raw);
33275
- if (parsed !== null) return parsed;
33276
- } catch {
33277
- }
33278
- return DEFAULT_CHANNEL;
33279
- }
33280
- async function writeChannel(slockHome, channel2) {
33281
- const p = channelPath(slockHome);
33282
- await mkdir12(dirname11(p), { recursive: true });
33283
- await writeFile10(p, `${channel2}
33284
- `, { mode: 384 });
33285
- }
33286
- async function runChannelShow(slockHome) {
33287
- void computerDir;
33288
- const channel2 = await readChannel(slockHome);
33289
- info(channel2);
33290
- }
33291
- async function runChannelSet(slockHome, raw) {
33292
- const parsed = parseChannel(raw);
33293
- if (parsed === null) {
33294
- fail(
33295
- "CHANNEL_INVALID",
33296
- `Invalid channel "${raw}". Accepted: \`latest\`, \`alpha\`, or \`pinned:<semver>\` (e.g. pinned:0.52.2).`
33297
- );
33298
- }
33299
- await writeChannel(slockHome, parsed);
33300
- info(`Channel set to ${parsed}.`);
33301
- info(`Note: service reads channel at startup; restart \`slock-computer start\` to apply.`);
33302
- }
33303
-
33304
33612
  // src/upgradeCli.ts
33305
33613
  init_esm_shims();
33306
- import { readFile as readFile15 } from "fs/promises";
33614
+ import { readFile as readFile14, rm as rm4 } from "fs/promises";
33307
33615
  import { fileURLToPath as fileURLToPath3 } from "url";
33308
- import { dirname as dirname12, join as join7 } from "path";
33616
+ import { dirname as dirname12, join as join6 } from "path";
33309
33617
 
33310
- // src/upgrade.ts
33311
- init_esm_shims();
33312
- import { spawn as spawn4 } from "child_process";
33313
- import { mkdir as mkdir13, readFile as readFile14, writeFile as writeFile11, rm as rm3, rename as rename4 } from "fs/promises";
33314
- import { join as join6 } from "path";
33315
- import { createHash as createHash3 } from "crypto";
33316
-
33317
- // src/preflightDepDrift.ts
33618
+ // src/upgradeLog.ts
33318
33619
  init_esm_shims();
33319
- import { readFile as readFile13 } from "fs/promises";
33320
- import { spawn as spawn3 } from "child_process";
33321
- import { createRequire as createRequire2 } from "module";
33322
- import { join as join5 } from "path";
33323
- import { pathToFileURL } from "url";
33324
- var DAEMON_PACKAGE_NAME = "@slock-ai/daemon";
33325
- async function preflightDepDriftCheck(currentBinaryDir, stagedTarballPath, deps = {}) {
33326
- const readTarball = deps.readTarballPackageJson ?? defaultReadTarballPackageJson;
33327
- const readCurrent = deps.readCurrentPackageJson ?? defaultReadCurrentPackageJson;
33328
- const satisfies = deps.semverSatisfies ?? defaultSemverSatisfies;
33329
- const allowUnsafe = deps.allowUnsafeSpec === true;
33330
- let targetPkg;
33331
- let currentPkg;
33332
- try {
33333
- targetPkg = await readTarball(stagedTarballPath);
33334
- } catch (e) {
33335
- return {
33336
- ok: false,
33337
- reason: "read_failed",
33338
- detail: `cannot read target tarball package.json: ${errMsg(e)}`
33339
- };
33340
- }
33341
- try {
33342
- currentPkg = await readCurrent(currentBinaryDir);
33343
- } catch (e) {
33344
- return {
33345
- ok: false,
33346
- reason: "read_failed",
33347
- detail: `cannot read current binary package.json: ${errMsg(e)}`
33348
- };
33349
- }
33350
- const targetSpec = targetPkg.dependencies?.[DAEMON_PACKAGE_NAME];
33351
- const currentSpec = currentPkg.dependencies?.[DAEMON_PACKAGE_NAME];
33352
- if (typeof targetSpec !== "string" || targetSpec.length === 0) {
33353
- return {
33354
- ok: false,
33355
- reason: "read_failed",
33356
- detail: `target tarball package.json has no ${DAEMON_PACKAGE_NAME} dependency entry`,
33357
- currentSpec
33358
- };
33359
- }
33360
- const unsafeTarget = isUnsafeSpec(targetSpec) || isUnparseableRange(targetSpec);
33361
- if (!allowUnsafe && unsafeTarget) {
33362
- return {
33363
- ok: false,
33364
- reason: "unsafe_spec",
33365
- 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.`,
33366
- currentSpec,
33367
- targetSpec
33368
- };
33369
- }
33370
- void satisfies;
33371
- return { ok: true, currentSpec, targetSpec };
33372
- }
33373
- async function verifyHydratedDaemonDependency(extractedPackageDir, targetSpec, deps = {}) {
33374
- const readInstalled = deps.readInstalledDaemonVersion ?? defaultReadInstalledDaemonVersion;
33375
- const satisfies = deps.semverSatisfies ?? defaultSemverSatisfies;
33376
- const allowUnsafe = deps.allowUnsafeSpec === true;
33377
- const unsafeTarget = isUnsafeSpec(targetSpec) || isUnparseableRange(targetSpec);
33378
- if (!allowUnsafe && unsafeTarget) {
33379
- return {
33380
- ok: false,
33381
- reason: "unsafe_spec",
33382
- detail: `${DAEMON_PACKAGE_NAME} target spec is not a safe semver range (target="${targetSpec}").`,
33383
- targetSpec
33384
- };
33385
- }
33386
- let installedVersion;
33387
- try {
33388
- installedVersion = await readInstalled(extractedPackageDir);
33389
- } catch (e) {
33390
- return {
33391
- ok: false,
33392
- reason: "read_failed",
33393
- detail: `cannot read hydrated ${DAEMON_PACKAGE_NAME} version: ${errMsg(e)}`,
33394
- targetSpec
33395
- };
33396
- }
33397
- if (installedVersion === null || installedVersion.length === 0) {
33398
- return {
33399
- ok: false,
33400
- reason: "read_failed",
33401
- detail: `${DAEMON_PACKAGE_NAME} is not installed in the hydrated staged package`,
33402
- targetSpec
33403
- };
33404
- }
33405
- if (allowUnsafe && unsafeTarget) {
33406
- return { ok: true, targetSpec, installedVersion };
33407
- }
33408
- if (!satisfies(installedVersion, targetSpec)) {
33409
- return {
33410
- ok: false,
33411
- reason: "installed_unsatisfied",
33412
- detail: `hydrated ${DAEMON_PACKAGE_NAME}@${installedVersion} does not satisfy target spec "${targetSpec}".`,
33413
- targetSpec,
33414
- installedVersion
33415
- };
33416
- }
33417
- return { ok: true, targetSpec, installedVersion };
33418
- }
33419
- function isUnparseableRange(spec) {
33420
- const s = spec.trim();
33421
- if (s.length === 0) return true;
33422
- if (/\s/.test(s) && !s.includes("||")) {
33423
- return s.split(/\s+/).some((p) => isUnparseableRange(p));
33424
- }
33425
- if (s.includes("||")) {
33426
- return s.split("||").every((p) => isUnparseableRange(p.trim()));
33427
- }
33428
- if (s.includes("x") || s.split(".").length < 3) {
33429
- const canon = s.replace(/\.x/g, ".0");
33430
- const padded = canon.split(".").length < 3 ? `${canon}.0.0`.split(".").slice(0, 3).join(".") : canon;
33431
- return parseSemver(padded) === null;
33432
- }
33433
- let core = s;
33434
- if (core.startsWith(">=") || core.startsWith("<=")) core = core.slice(2);
33435
- else if (core.startsWith("^") || core.startsWith("~") || core.startsWith(">") || core.startsWith("<") || core.startsWith("=")) {
33436
- core = core.slice(1);
33437
- }
33438
- return parseSemver(core) === null;
33439
- }
33440
- function isUnsafeSpec(spec) {
33441
- const s = spec.trim();
33442
- if (s.length === 0) return true;
33443
- if (s === "*" || s === "latest") return true;
33444
- if (s.startsWith("workspace:")) return true;
33445
- if (s.startsWith("file:") || s.startsWith("link:")) return true;
33446
- if (s.startsWith("git+") || s.startsWith("git:")) return true;
33447
- if (s.startsWith("github:") || /^[^/\s]+\/[^/\s]+(?:#.*)?$/.test(s)) {
33448
- return true;
33449
- }
33450
- if (s.startsWith("http://") || s.startsWith("https://")) return true;
33451
- if (s.startsWith("npm:")) return true;
33452
- if (s.startsWith("./") || s.startsWith("../") || s.startsWith("/")) return true;
33453
- return false;
33454
- }
33455
- function errMsg(e) {
33456
- return e instanceof Error ? e.message : String(e);
33457
- }
33458
- async function defaultReadTarballPackageJson(tarballPath) {
33459
- const raw = await new Promise((resolve2, reject) => {
33460
- const child = spawn3("tar", ["-xzOf", tarballPath, "package/package.json"], {
33461
- stdio: ["ignore", "pipe", "pipe"]
33462
- });
33463
- let stdoutBuf = "";
33464
- let stderrBuf = "";
33465
- child.stdout.on("data", (chunk) => {
33466
- stdoutBuf += String(chunk);
33467
- });
33468
- child.stderr.on("data", (chunk) => {
33469
- stderrBuf += String(chunk);
33470
- });
33471
- child.on("error", (err) => reject(err));
33472
- child.on("close", (code) => {
33473
- if (code !== 0) {
33474
- reject(new Error(`tar -xzOf exited ${code}: ${stderrBuf.slice(0, 300)}`));
33475
- return;
33476
- }
33477
- resolve2(stdoutBuf);
33478
- });
33479
- });
33480
- return JSON.parse(raw);
33481
- }
33482
- async function defaultReadCurrentPackageJson(currentBinaryDir) {
33483
- const raw = await readFile13(join5(currentBinaryDir, "package.json"), "utf8");
33484
- return JSON.parse(raw);
33485
- }
33486
- async function defaultReadInstalledDaemonVersion(currentBinaryDir) {
33487
- let searchPaths;
33488
- try {
33489
- const anchor = pathToFileURL(join5(currentBinaryDir, "package.json"));
33490
- const req = createRequire2(anchor);
33491
- searchPaths = req.resolve.paths(DAEMON_PACKAGE_NAME);
33492
- } catch {
33493
- return null;
33494
- }
33495
- if (!searchPaths || searchPaths.length === 0) return null;
33496
- const subPath = DAEMON_PACKAGE_NAME.split("/");
33497
- for (const base of searchPaths) {
33498
- const candidate = join5(base, ...subPath, "package.json");
33499
- try {
33500
- const raw = await readFile13(candidate, "utf8");
33501
- const parsed = JSON.parse(raw);
33502
- if (parsed.name !== DAEMON_PACKAGE_NAME) continue;
33503
- if (typeof parsed.version === "string" && parsed.version.length > 0) {
33504
- return parsed.version;
33505
- }
33506
- return null;
33507
- } catch {
33508
- }
33509
- }
33510
- return null;
33511
- }
33512
- function defaultSemverSatisfies(version, range) {
33513
- const ver = parseSemver(version);
33514
- if (ver === null) return false;
33515
- const trimmed = range.trim();
33516
- if (trimmed.length === 0) return false;
33517
- if (/\s/.test(trimmed) && !trimmed.includes("||")) {
33518
- return trimmed.split(/\s+/).every((part) => defaultSemverSatisfies(version, part));
33519
- }
33520
- if (trimmed.includes("||")) {
33521
- return trimmed.split("||").some((part) => defaultSemverSatisfies(version, part.trim()));
33522
- }
33523
- if (trimmed.startsWith("^")) {
33524
- return satisfiesCaret(ver, trimmed.slice(1));
33525
- }
33526
- if (trimmed.startsWith("~")) {
33527
- return satisfiesTilde(ver, trimmed.slice(1));
33528
- }
33529
- if (trimmed.startsWith(">=")) return cmp(ver, trimmed.slice(2)) >= 0;
33530
- if (trimmed.startsWith("<=")) return cmp(ver, trimmed.slice(2)) <= 0;
33531
- if (trimmed.startsWith(">")) return cmp(ver, trimmed.slice(1)) > 0;
33532
- if (trimmed.startsWith("<")) return cmp(ver, trimmed.slice(1)) < 0;
33533
- if (trimmed.startsWith("=")) return cmp(ver, trimmed.slice(1)) === 0;
33534
- if (trimmed.includes("x") || trimmed.split(".").length < 3) {
33535
- const canon = trimmed.replace(/\.x/g, ".0");
33536
- const padded = canon.split(".").length < 3 ? `${canon}.0.0`.split(".").slice(0, 3).join(".") : canon;
33537
- return satisfiesCaret(ver, padded);
33538
- }
33539
- return cmp(ver, trimmed) === 0;
33540
- }
33541
- function parseSemver(s) {
33542
- const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(s.trim());
33543
- if (!m) return null;
33544
- return {
33545
- major: Number.parseInt(m[1], 10),
33546
- minor: Number.parseInt(m[2], 10),
33547
- patch: Number.parseInt(m[3], 10),
33548
- pre: m[4] ?? ""
33549
- };
33550
- }
33551
- function cmp(a, bStr) {
33552
- const b = parseSemver(bStr);
33553
- if (b === null) return Number.NaN;
33554
- if (a.major !== b.major) return a.major - b.major;
33555
- if (a.minor !== b.minor) return a.minor - b.minor;
33556
- if (a.patch !== b.patch) return a.patch - b.patch;
33557
- if (a.pre === "" && b.pre !== "") return 1;
33558
- if (a.pre !== "" && b.pre === "") return -1;
33559
- if (a.pre === b.pre) return 0;
33560
- return a.pre < b.pre ? -1 : 1;
33561
- }
33562
- function satisfiesCaret(ver, base) {
33563
- const b = parseSemver(base);
33564
- if (b === null) return false;
33565
- if (cmp(ver, base) < 0) return false;
33566
- if (b.major > 0) return ver.major === b.major;
33567
- if (b.minor > 0) return ver.major === 0 && ver.minor === b.minor;
33568
- return ver.major === 0 && ver.minor === 0 && ver.patch === b.patch;
33569
- }
33570
- function satisfiesTilde(ver, base) {
33571
- const b = parseSemver(base);
33572
- if (b === null) return false;
33573
- if (cmp(ver, base) < 0) return false;
33574
- return ver.major === b.major && ver.minor === b.minor;
33575
- }
33576
-
33577
- // src/upgrade.ts
33578
- async function stagePhase(slockHome, version, deps = {}) {
33579
- const npmPack = deps.npmPack ?? defaultNpmPack;
33580
- const fsReadFile = deps.fsReadFile ?? readFile14;
33581
- const stagedPath = upgradeStagingDir(slockHome, version);
33582
- await mkdir13(stagedPath, { recursive: true });
33583
- const packageRef = `@slock-ai/computer@${version}`;
33584
- const result = await npmPack(stagedPath, packageRef);
33585
- if (result.exitCode !== 0) {
33586
- const err = new Error(
33587
- `npm pack failed for ${packageRef}: exit ${result.exitCode}. stderr: ${result.stderr.slice(0, 500)}`
33588
- );
33589
- err.code = "UPGRADE_DOWNLOAD_FAILED";
33590
- throw err;
33591
- }
33592
- let tarballBytes;
33593
- try {
33594
- tarballBytes = await fsReadFile(result.tarballPath);
33595
- } catch (e) {
33596
- const msg = e instanceof Error ? e.message : String(e);
33597
- const err = new Error(`staged tarball missing at ${result.tarballPath}: ${msg}`);
33598
- err.code = "UPGRADE_DOWNLOAD_FAILED";
33599
- throw err;
33600
- }
33601
- const tarballSha1 = createHash3("sha1").update(tarballBytes).digest("hex");
33602
- return { stagedPath, version, tarballSha1 };
33603
- }
33604
- function defaultNpmPack(cwd, packageRef) {
33605
- return new Promise((resolve2) => {
33606
- const child = spawn4("npm", ["pack", packageRef, "--json"], {
33607
- cwd,
33608
- stdio: ["ignore", "pipe", "pipe"]
33609
- });
33610
- let stdoutBuf = "";
33611
- let stderrBuf = "";
33612
- child.stdout.on("data", (chunk) => {
33613
- stdoutBuf += String(chunk);
33614
- });
33615
- child.stderr.on("data", (chunk) => {
33616
- stderrBuf += String(chunk);
33617
- });
33618
- child.on("error", (err) => {
33619
- resolve2({ tarballPath: "", exitCode: 1, stderr: String(err) });
33620
- });
33621
- child.on("close", (code) => {
33622
- if (code !== 0) {
33623
- resolve2({ tarballPath: "", exitCode: code ?? 1, stderr: stderrBuf });
33624
- return;
33625
- }
33626
- try {
33627
- const parsed = JSON.parse(stdoutBuf);
33628
- const filename = parsed[0]?.filename;
33629
- if (!filename) {
33630
- resolve2({ tarballPath: "", exitCode: 1, stderr: "npm pack returned no filename" });
33631
- return;
33632
- }
33633
- resolve2({ tarballPath: join6(cwd, filename), exitCode: 0, stderr: stderrBuf });
33634
- } catch (e) {
33635
- resolve2({ tarballPath: "", exitCode: 1, stderr: `parse npm pack output: ${e}` });
33636
- }
33637
- });
33638
- });
33639
- }
33640
- async function verifyPhase(staged, deps = {}) {
33641
- const fetchAdvertisedHash = deps.fetchAdvertisedHash ?? defaultFetchAdvertisedHash;
33642
- let advertised;
33643
- try {
33644
- advertised = await fetchAdvertisedHash(staged.version);
33645
- } catch (e) {
33646
- void e;
33647
- return { ok: false, reason: "registry_fetch_failed" };
33648
- }
33649
- if (advertised === null) {
33650
- return { ok: false, reason: "no_advertised_sha" };
33651
- }
33652
- if (staged.tarballSha1 !== advertised) {
33653
- return {
33654
- ok: false,
33655
- reason: "checksum_mismatch",
33656
- actual: staged.tarballSha1,
33657
- advertised
33658
- };
33659
- }
33660
- return { ok: true, actual: staged.tarballSha1, advertised };
33661
- }
33662
- async function defaultFetchAdvertisedHash(version) {
33663
- const url = "https://registry.npmjs.org/@slock-ai/computer";
33664
- const controller = new AbortController();
33665
- const timeoutId = setTimeout(() => controller.abort(), 1e4);
33666
- try {
33667
- const res = await fetch(url, {
33668
- signal: controller.signal,
33669
- headers: { accept: "application/json" }
33670
- });
33671
- if (!res.ok) return null;
33672
- const data = await res.json();
33673
- const shasum = data.versions?.[version]?.dist?.shasum;
33674
- return typeof shasum === "string" && shasum.length > 0 ? shasum : null;
33675
- } catch {
33676
- return null;
33677
- } finally {
33678
- clearTimeout(timeoutId);
33679
- }
33680
- }
33681
- async function cleanupStaged(slockHome, version) {
33682
- try {
33683
- await rm3(upgradeStagingDir(slockHome, version), { recursive: true, force: true });
33684
- } catch {
33685
- }
33686
- }
33687
- async function snapshotPhase(slockHome, snap) {
33688
- const path3 = upgradeSnapshotPath(slockHome);
33689
- const tmp = `${path3}.tmp`;
33690
- await mkdir13(computerDir(slockHome), { recursive: true });
33691
- await writeFile11(tmp, JSON.stringify(snap, null, 2), { mode: 384 });
33692
- await rename4(tmp, path3);
33693
- }
33694
- async function clearUpgradeSnapshot(slockHome) {
33695
- try {
33696
- const { unlink: unlink5 } = await import("fs/promises");
33697
- await unlink5(upgradeSnapshotPath(slockHome));
33698
- } catch {
33699
- }
33700
- }
33701
- async function extractTarball(tarballPath, destDir, deps = {}) {
33702
- const tarSpawn = deps.tarSpawn ?? defaultTarSpawn;
33703
- await mkdir13(destDir, { recursive: true });
33704
- const result = await tarSpawn(tarballPath, destDir);
33705
- if (result.exitCode !== 0) {
33706
- const err = new Error(
33707
- `tar -xzf failed for ${tarballPath}: exit ${result.exitCode}. stderr: ${result.stderr.slice(0, 500)}`
33708
- );
33709
- err.code = "UPGRADE_EXTRACT_FAILED";
33710
- throw err;
33711
- }
33712
- return { extractedPackageDir: join6(destDir, "package") };
33713
- }
33714
- function defaultTarSpawn(tarballPath, destDir) {
33715
- return new Promise((resolve2) => {
33716
- const child = spawn4("tar", ["-xzf", tarballPath, "-C", destDir], {
33717
- stdio: ["ignore", "ignore", "pipe"]
33718
- });
33719
- let stderrBuf = "";
33720
- child.stderr.on("data", (chunk) => {
33721
- stderrBuf += String(chunk);
33722
- });
33723
- child.on("error", (err) => {
33724
- resolve2({ exitCode: 1, stderr: String(err) });
33725
- });
33726
- child.on("close", (code) => {
33727
- resolve2({ exitCode: code ?? 1, stderr: stderrBuf });
33728
- });
33729
- });
33730
- }
33731
- async function hydrateStagedDependencies(extractedPackageDir, deps = {}) {
33732
- const npmInstall = deps.npmInstall ?? defaultNpmInstallProductionDeps;
33733
- const result = await npmInstall(extractedPackageDir);
33734
- if (result.exitCode !== 0) {
33735
- const err = new Error(
33736
- `npm install failed for staged package dependencies: exit ${result.exitCode}`
33737
- );
33738
- err.code = "UPGRADE_DEPENDENCY_INSTALL_FAILED";
33739
- throw err;
33740
- }
33741
- }
33742
- function defaultNpmInstallProductionDeps(cwd) {
33743
- return new Promise((resolve2) => {
33744
- const child = spawn4(
33745
- "npm",
33746
- [
33747
- "install",
33748
- "--omit=dev",
33749
- "--ignore-scripts",
33750
- "--no-audit",
33751
- "--no-fund",
33752
- "--package-lock=false"
33753
- ],
33754
- {
33755
- cwd,
33756
- stdio: ["ignore", "ignore", "pipe"]
33757
- }
33758
- );
33759
- let stderrBuf = "";
33760
- child.stderr.on("data", (chunk) => {
33761
- stderrBuf += String(chunk);
33762
- });
33763
- child.on("error", (err) => {
33764
- resolve2({ exitCode: 1, stderr: String(err) });
33765
- });
33766
- child.on("close", (code) => {
33767
- resolve2({ exitCode: code ?? 1, stderr: stderrBuf });
33768
- });
33769
- });
33770
- }
33771
- async function swapPhase(currentBinaryDir, stagedBinaryDir, deps = {}) {
33772
- const fsRename = deps.fsRename ?? rename4;
33773
- const fsRm = deps.fsRm ?? ((p) => rm3(p, { recursive: true, force: true }));
33774
- const prevBinaryDir = `${currentBinaryDir}.prev`;
33775
- try {
33776
- await fsRm(prevBinaryDir);
33777
- } catch {
33778
- }
33779
- await fsRename(currentBinaryDir, prevBinaryDir);
33780
- try {
33781
- await fsRename(stagedBinaryDir, currentBinaryDir);
33782
- } catch (e) {
33783
- try {
33784
- await fsRename(prevBinaryDir, currentBinaryDir);
33785
- } catch {
33786
- }
33787
- const msg = e instanceof Error ? e.message : String(e);
33788
- const err = new Error(`swap failed (staged \u2192 current): ${msg}`);
33789
- err.code = "UPGRADE_SWAP_FAILED";
33790
- throw err;
33791
- }
33792
- return { prevBinaryDir, newBinaryDir: currentBinaryDir };
33793
- }
33794
- async function rollbackSwap(currentBinaryDir, deps = {}) {
33795
- const fsRename = deps.fsRename ?? rename4;
33796
- const fsRm = deps.fsRm ?? ((p) => rm3(p, { recursive: true, force: true }));
33797
- const prevBinaryDir = `${currentBinaryDir}.prev`;
33798
- try {
33799
- await fsRm(currentBinaryDir);
33800
- await fsRename(prevBinaryDir, currentBinaryDir);
33801
- return { rolledBack: true };
33802
- } catch {
33803
- return { rolledBack: false };
33804
- }
33805
- }
33806
- async function rollbackRestart(slockHome, currentBinaryDir, deps = {}) {
33807
- const readServicePid = deps.readServicePid ?? (() => defaultReadServicePid(slockHome));
33808
- const isProcessAlive4 = deps.isProcessAlive ?? defaultIsProcessAlive;
33809
- const killService = deps.killService ?? defaultKillService;
33810
- const forceKillService = deps.forceKillService ?? defaultForceKillService;
33811
- const waitForExit = deps.waitForExit ?? defaultWaitForExit;
33812
- const spawnFreshService = deps.spawnFreshService;
33813
- const healthCheck = deps.healthCheck ?? (() => defaultHealthCheck(slockHome));
33814
- const healthTimeoutMs = deps.healthTimeoutMs ?? 3e4;
33815
- const healthPollIntervalMs = deps.healthPollIntervalMs ?? 500;
33816
- let serviceStopped = false;
33817
- try {
33818
- const pid = await readServicePid();
33819
- if (pid === null || !isProcessAlive4(pid)) {
33820
- serviceStopped = true;
33821
- } else {
33822
- await killService(pid);
33823
- const exited = await waitForExit(pid, 1e4);
33824
- if (!exited) {
33825
- await forceKillService(pid);
33826
- const escalatedExit = await waitForExit(pid, 5e3);
33827
- serviceStopped = escalatedExit;
33828
- } else {
33829
- serviceStopped = true;
33830
- }
33831
- }
33832
- } catch (e) {
33833
- return {
33834
- binaryRestored: false,
33835
- serviceStopped: false,
33836
- serviceRespawned: false,
33837
- serviceHealthy: false,
33838
- failedStep: "stop",
33839
- reason: e instanceof Error ? e.message : String(e)
33840
- };
33841
- }
33842
- if (!serviceStopped) {
33843
- return {
33844
- binaryRestored: false,
33845
- serviceStopped: false,
33846
- serviceRespawned: false,
33847
- serviceHealthy: false,
33848
- failedStep: "stop",
33849
- reason: "new-version service did not exit (graceful + SIGKILL both timed out)"
33850
- };
33851
- }
33852
- const swapRb = await rollbackSwap(currentBinaryDir, {
33853
- fsRename: deps.fsRename,
33854
- fsRm: deps.fsRm
33855
- });
33856
- if (!swapRb.rolledBack) {
33857
- return {
33858
- binaryRestored: false,
33859
- serviceStopped: true,
33860
- serviceRespawned: false,
33861
- serviceHealthy: false,
33862
- failedStep: "binary",
33863
- reason: "could not restore .prev \u2192 currentBinaryDir"
33864
- };
33865
- }
33866
- if (!spawnFreshService) {
33867
- return {
33868
- binaryRestored: true,
33869
- serviceStopped: true,
33870
- serviceRespawned: false,
33871
- serviceHealthy: false,
33872
- failedStep: "respawn",
33873
- reason: "no spawnFreshService callback wired; old service not respawned"
33874
- };
33875
- }
33876
- try {
33877
- await spawnFreshService();
33878
- } catch (e) {
33879
- return {
33880
- binaryRestored: true,
33881
- serviceStopped: true,
33882
- serviceRespawned: false,
33883
- serviceHealthy: false,
33884
- failedStep: "respawn",
33885
- reason: e instanceof Error ? e.message : String(e)
33886
- };
33887
- }
33888
- const start2 = Date.now();
33889
- let healthy = false;
33890
- while (Date.now() - start2 < healthTimeoutMs) {
33891
- if (await healthCheck()) {
33892
- healthy = true;
33893
- break;
33894
- }
33895
- await new Promise((r) => setTimeout(r, healthPollIntervalMs));
33896
- }
33897
- if (!healthy) {
33898
- return {
33899
- binaryRestored: true,
33900
- serviceStopped: true,
33901
- serviceRespawned: true,
33902
- serviceHealthy: false,
33903
- failedStep: "health",
33904
- reason: "old service failed health check after rollback respawn"
33905
- };
33906
- }
33907
- return {
33908
- binaryRestored: true,
33909
- serviceStopped: true,
33910
- serviceRespawned: true,
33911
- serviceHealthy: true
33912
- };
33913
- }
33914
- async function restartPhase(slockHome, deps = {}) {
33915
- const readServicePid = deps.readServicePid ?? (() => defaultReadServicePid(slockHome));
33916
- const killService = deps.killService ?? defaultKillService;
33917
- const forceKillService = deps.forceKillService ?? defaultForceKillService;
33918
- const waitForExit = deps.waitForExit ?? defaultWaitForExit;
33919
- const spawnFreshService = deps.spawnFreshService;
33920
- const healthCheck = deps.healthCheck ?? (() => defaultHealthCheck(slockHome));
33921
- const healthTimeoutMs = deps.healthTimeoutMs ?? 3e4;
33922
- const healthPollIntervalMs = deps.healthPollIntervalMs ?? 500;
33923
- const forceKill = deps.forceKill === true;
33924
- const oldPid = await readServicePid();
33925
- if (oldPid !== null) {
33926
- try {
33927
- if (forceKill) {
33928
- await forceKillService(oldPid);
33929
- } else {
33930
- await killService(oldPid);
33931
- const exited = await waitForExit(oldPid, 1e4);
33932
- if (!exited) {
33933
- await forceKillService(oldPid);
33934
- const escalatedExit = await waitForExit(oldPid, 5e3);
33935
- if (!escalatedExit) {
33936
- return { ok: false, reason: "service_kill_failed" };
33937
- }
33938
- }
33939
- }
33940
- } catch {
33941
- return { ok: false, reason: "service_kill_failed" };
33942
- }
33943
- }
33944
- if (spawnFreshService) {
33945
- try {
33946
- await spawnFreshService();
33947
- } catch {
33948
- return { ok: false, reason: "spawn_failed" };
33949
- }
33950
- }
33951
- const start2 = Date.now();
33952
- while (Date.now() - start2 < healthTimeoutMs) {
33953
- if (await healthCheck()) {
33954
- return { ok: true, healthAfterMs: Date.now() - start2 };
33955
- }
33956
- await new Promise((r) => setTimeout(r, healthPollIntervalMs));
33957
- }
33958
- return { ok: false, reason: "health_check_timeout" };
33959
- }
33960
- async function defaultReadServicePid(slockHome) {
33961
- const { pid } = await findLiveServicePid(slockHome);
33962
- return pid;
33963
- }
33964
- async function defaultKillService(pid) {
33965
- try {
33966
- process.kill(pid, "SIGTERM");
33967
- } catch (err) {
33968
- if (err.code === "ESRCH") return;
33969
- throw err;
33970
- }
33971
- }
33972
- async function defaultForceKillService(pid) {
33973
- try {
33974
- process.kill(pid, "SIGKILL");
33975
- } catch (err) {
33976
- if (err.code === "ESRCH") return;
33977
- throw err;
33978
- }
33979
- }
33980
- async function defaultIsDaemonBusy(_slockHome) {
33981
- return false;
33982
- }
33983
- async function defaultWaitForExit(pid, timeoutMs) {
33984
- const start2 = Date.now();
33985
- while (Date.now() - start2 < timeoutMs) {
33986
- try {
33987
- process.kill(pid, 0);
33988
- } catch (err) {
33989
- if (err.code === "ESRCH") return true;
33990
- }
33991
- await new Promise((r) => setTimeout(r, 200));
33992
- }
33993
- return false;
33994
- }
33995
- async function defaultHealthCheck(slockHome) {
33996
- const pid = await defaultReadServicePid(slockHome);
33997
- if (pid === null) return false;
33998
- try {
33999
- process.kill(pid, 0);
34000
- return true;
34001
- } catch (err) {
34002
- return err.code === "EPERM";
34003
- }
34004
- }
34005
- async function cleanupSuccessPhase(slockHome, version, prevBinaryDir) {
34006
- await cleanupStaged(slockHome, version);
34007
- await clearUpgradeSnapshot(slockHome);
34008
- try {
34009
- await rm3(prevBinaryDir, { recursive: true, force: true });
34010
- } catch {
34011
- }
34012
- }
34013
- async function runUpgrade(slockHome, opts) {
34014
- const deps = opts.deps ?? {};
34015
- const drainMode = opts.drainMode ?? "drain";
34016
- if (drainMode === "defer") {
34017
- const isBusy = deps.isDaemonBusy ?? defaultIsDaemonBusy;
34018
- let busy = false;
34019
- try {
34020
- busy = await isBusy(slockHome);
34021
- } catch {
34022
- busy = false;
34023
- }
34024
- if (busy) {
34025
- return {
34026
- ok: false,
34027
- phase: "drain",
34028
- reason: "deferred: daemon busy (--drain defer)"
34029
- };
34030
- }
34031
- }
34032
- let staged;
34033
- try {
34034
- staged = await stagePhase(slockHome, opts.targetVersion, {
34035
- npmPack: deps.npmPack,
34036
- fsReadFile: deps.fsReadFile
34037
- });
34038
- } catch (e) {
34039
- await cleanupStaged(slockHome, opts.targetVersion);
34040
- return { ok: false, phase: "stage", reason: e instanceof Error ? e.message : String(e) };
34041
- }
34042
- const verify = await verifyPhase(staged, { fetchAdvertisedHash: deps.fetchAdvertisedHash });
34043
- if (!verify.ok) {
34044
- await cleanupStaged(slockHome, opts.targetVersion);
34045
- return { ok: false, phase: "verify", reason: verify.reason, staged, verify };
34046
- }
34047
- let stagedTarballPath;
34048
- try {
34049
- stagedTarballPath = await locateStagedTarball(staged.stagedPath);
34050
- } catch (e) {
34051
- await cleanupStaged(slockHome, opts.targetVersion);
34052
- return {
34053
- ok: false,
34054
- phase: "preflight",
34055
- reason: e instanceof Error ? e.message : String(e),
34056
- staged,
34057
- verify
34058
- };
34059
- }
34060
- const preflight = await preflightDepDriftCheck(
34061
- opts.currentBinaryDir,
34062
- stagedTarballPath,
34063
- deps.preflight ?? {}
34064
- );
34065
- if (!preflight.ok) {
34066
- await cleanupStaged(slockHome, opts.targetVersion);
34067
- return {
34068
- ok: false,
34069
- phase: "preflight",
34070
- reason: preflight.detail ?? preflight.reason ?? "dep_drift",
34071
- staged,
34072
- verify,
34073
- preflight
34074
- };
34075
- }
34076
- const targetDaemonSpec = preflight.targetSpec;
34077
- if (typeof targetDaemonSpec !== "string" || targetDaemonSpec.length === 0) {
34078
- await cleanupStaged(slockHome, opts.targetVersion);
34079
- return {
34080
- ok: false,
34081
- phase: "preflight",
34082
- reason: `${preflight.detail ?? preflight.reason ?? "dep_drift"}: missing target ${DAEMON_PACKAGE_NAME} spec`,
34083
- staged,
34084
- verify,
34085
- preflight
34086
- };
34087
- }
34088
- const snap = {
34089
- at: (/* @__PURE__ */ new Date()).toISOString(),
34090
- fromVersion: opts.fromVersion,
34091
- toVersion: opts.targetVersion,
34092
- channel: opts.channel
34093
- };
34094
- try {
34095
- await snapshotPhase(slockHome, snap);
34096
- } catch (e) {
34097
- await cleanupStaged(slockHome, opts.targetVersion);
34098
- return {
34099
- ok: false,
34100
- phase: "snapshot",
34101
- reason: e instanceof Error ? e.message : String(e),
34102
- staged,
34103
- verify
34104
- };
34105
- }
34106
- let extractedPackageDir;
34107
- try {
34108
- const ex = await extractTarball(
34109
- stagedTarballPath,
34110
- join6(staged.stagedPath, "extracted"),
34111
- { tarSpawn: deps.tarSpawn }
34112
- );
34113
- extractedPackageDir = ex.extractedPackageDir;
34114
- } catch (e) {
34115
- await cleanupStaged(slockHome, opts.targetVersion);
34116
- await clearUpgradeSnapshot(slockHome);
34117
- return {
34118
- ok: false,
34119
- phase: "extract",
34120
- reason: e instanceof Error ? e.message : String(e),
34121
- staged,
34122
- verify
34123
- };
34124
- }
34125
- try {
34126
- await hydrateStagedDependencies(extractedPackageDir, { npmInstall: deps.npmInstall });
34127
- } catch (e) {
34128
- await cleanupStaged(slockHome, opts.targetVersion);
34129
- await clearUpgradeSnapshot(slockHome);
34130
- return {
34131
- ok: false,
34132
- phase: "hydrate",
34133
- reason: e instanceof Error ? e.message : String(e),
34134
- staged,
34135
- verify
34136
- };
34137
- }
34138
- const hydratedDaemon = await verifyHydratedDaemonDependency(
34139
- extractedPackageDir,
34140
- targetDaemonSpec,
34141
- deps.preflight ?? {}
34142
- );
34143
- if (!hydratedDaemon.ok) {
34144
- await cleanupStaged(slockHome, opts.targetVersion);
34145
- await clearUpgradeSnapshot(slockHome);
34146
- return {
34147
- ok: false,
34148
- phase: "hydrate",
34149
- reason: `staged dependency tree verification failed: ${hydratedDaemon.detail ?? hydratedDaemon.reason ?? "hydrated dependency verification failed"}`,
34150
- staged,
34151
- verify,
34152
- preflight,
34153
- hydratedDaemon
34154
- };
34155
- }
34156
- let swap;
34157
- try {
34158
- swap = await swapPhase(opts.currentBinaryDir, extractedPackageDir, {
34159
- fsRename: deps.fsRename,
34160
- fsRm: deps.fsRm
34161
- });
34162
- } catch (e) {
34163
- return {
34164
- ok: false,
34165
- phase: "swap",
34166
- reason: e instanceof Error ? e.message : String(e),
34167
- staged,
34168
- verify
34169
- };
34170
- }
34171
- const restart = await restartPhase(slockHome, {
34172
- readServicePid: deps.readServicePid,
34173
- killService: deps.killService,
34174
- forceKillService: deps.forceKillService,
34175
- waitForExit: deps.waitForExit,
34176
- spawnFreshService: deps.spawnFreshService,
34177
- healthCheck: deps.healthCheck,
34178
- healthTimeoutMs: deps.healthTimeoutMs,
34179
- healthPollIntervalMs: deps.healthPollIntervalMs,
34180
- forceKill: drainMode === "force"
34181
- });
34182
- if (!restart.ok) {
34183
- const rb = await rollbackRestart(slockHome, opts.currentBinaryDir, {
34184
- readServicePid: deps.readServicePid,
34185
- isProcessAlive: deps.isProcessAlive,
34186
- killService: deps.killService,
34187
- forceKillService: deps.forceKillService,
34188
- waitForExit: deps.waitForExit,
34189
- spawnFreshService: deps.spawnFreshService,
34190
- healthCheck: deps.healthCheck,
34191
- healthTimeoutMs: deps.healthTimeoutMs,
34192
- healthPollIntervalMs: deps.healthPollIntervalMs,
34193
- fsRename: deps.fsRename,
34194
- fsRm: deps.fsRm
34195
- });
34196
- return {
34197
- ok: false,
34198
- phase: "restart",
34199
- reason: restart.reason,
34200
- staged,
34201
- verify,
34202
- swap,
34203
- restart,
34204
- rolledBack: rb.binaryRestored && rb.serviceHealthy,
34205
- rollback: rb
34206
- };
34207
- }
34208
- const rolling = await rollingDaemonHealthCheck(slockHome, {
34209
- listManagedServerIds: deps.listManagedServerIds,
34210
- readDaemonPid: deps.readDaemonPid,
34211
- isProcessAlive: deps.isProcessAlive,
34212
- perDaemonTimeoutMs: deps.perDaemonTimeoutMs,
34213
- pollIntervalMs: deps.daemonPollIntervalMs
34214
- });
34215
- if (!rolling.ok) {
34216
- const rb = await rollbackRestart(slockHome, opts.currentBinaryDir, {
34217
- readServicePid: deps.readServicePid,
34218
- isProcessAlive: deps.isProcessAlive,
34219
- killService: deps.killService,
34220
- forceKillService: deps.forceKillService,
34221
- waitForExit: deps.waitForExit,
34222
- spawnFreshService: deps.spawnFreshService,
34223
- healthCheck: deps.healthCheck,
34224
- healthTimeoutMs: deps.healthTimeoutMs,
34225
- healthPollIntervalMs: deps.healthPollIntervalMs,
34226
- fsRename: deps.fsRename,
34227
- fsRm: deps.fsRm
34228
- });
34229
- const failed = rolling.daemons.find((d) => !d.ok);
34230
- return {
34231
- ok: false,
34232
- phase: "rolling_health",
34233
- reason: failed ? `daemon ${failed.serverId} unhealthy after restart: ${failed.reason ?? "unknown"}` : "rolling daemon health failed",
34234
- staged,
34235
- verify,
34236
- swap,
34237
- restart,
34238
- rolling,
34239
- rolledBack: rb.binaryRestored && rb.serviceHealthy,
34240
- rollback: rb
34241
- };
34242
- }
34243
- await cleanupSuccessPhase(slockHome, opts.targetVersion, swap.prevBinaryDir);
34244
- return { ok: true, phase: "cleanup", staged, verify, preflight, hydratedDaemon, swap, restart, rolling };
34245
- }
34246
- async function rollingDaemonHealthCheck(slockHome, deps = {}) {
34247
- const list = deps.listManagedServerIds ?? listManagedServerIds;
34248
- const readDaemonPid = deps.readDaemonPid ?? defaultReadDaemonPid;
34249
- const isProcessAlive4 = deps.isProcessAlive ?? defaultIsProcessAlive;
34250
- const perDaemonTimeoutMs = deps.perDaemonTimeoutMs ?? 3e4;
34251
- const pollIntervalMs = deps.pollIntervalMs ?? 500;
34252
- const managed = await list(slockHome);
34253
- const daemons = [];
34254
- for (const serverId of managed) {
34255
- const start2 = Date.now();
34256
- let lastReason = "timeout";
34257
- let healthy = false;
34258
- while (Date.now() - start2 < perDaemonTimeoutMs) {
34259
- const pid = await readDaemonPid(slockHome, serverId);
34260
- if (pid === null) {
34261
- lastReason = "pidfile_missing";
34262
- } else if (!isProcessAlive4(pid)) {
34263
- lastReason = "process_dead";
34264
- } else {
34265
- healthy = true;
34266
- break;
34267
- }
34268
- await new Promise((r) => setTimeout(r, pollIntervalMs));
34269
- }
34270
- const entry = healthy ? { serverId, ok: true, healthAfterMs: Date.now() - start2 } : { serverId, ok: false, reason: lastReason };
34271
- daemons.push(entry);
34272
- if (!healthy) {
34273
- return { ok: false, daemons };
34274
- }
34275
- }
34276
- return { ok: true, daemons };
34277
- }
34278
- async function defaultReadDaemonPid(slockHome, serverId) {
34279
- try {
34280
- const raw = (await readFile14(serverRunnerPidPath(slockHome, serverId), "utf8")).trim();
34281
- const pid = Number.parseInt(raw, 10);
34282
- return Number.isInteger(pid) && pid > 0 ? pid : null;
34283
- } catch {
34284
- return null;
34285
- }
34286
- }
34287
- function defaultIsProcessAlive(pid) {
34288
- if (!Number.isInteger(pid) || pid <= 0) return false;
34289
- try {
34290
- process.kill(pid, 0);
34291
- return true;
34292
- } catch (err) {
34293
- return err.code === "EPERM";
34294
- }
34295
- }
34296
- async function locateStagedTarball(stagedPath) {
34297
- const { readdir: readdir5 } = await import("fs/promises");
34298
- const entries = await readdir5(stagedPath);
34299
- const tgz = entries.find((e) => e.endsWith(".tgz"));
34300
- if (!tgz) {
34301
- throw new Error(`no .tgz tarball found in staged dir ${stagedPath}`);
34302
- }
34303
- return join6(stagedPath, tgz);
34304
- }
34305
-
34306
- // src/upgradeLog.ts
34307
- init_esm_shims();
34308
- import { chmod as chmod5, mkdir as mkdir14, open as open2 } from "fs/promises";
33620
+ import { chmod as chmod6, mkdir as mkdir14, open as open2 } from "fs/promises";
34309
33621
  var FILE_MODE = 384;
34310
33622
  function resolveUpgradeTrigger(raw) {
34311
33623
  return raw === "web" || raw === "tray" ? raw : "cli";
@@ -34365,31 +33677,29 @@ async function appendUpgradeLogEntry(slockHome, entry) {
34365
33677
  await handle.close();
34366
33678
  }
34367
33679
  if (process.platform !== "win32") {
34368
- await chmod5(path3, FILE_MODE);
33680
+ await chmod6(path3, FILE_MODE);
34369
33681
  }
34370
33682
  }
34371
33683
 
34372
33684
  // src/upgradeCli.ts
34373
- function isEphemeralNpxContext(binaryDir) {
34374
- return binaryDir.split(/[\\/]/).includes("_npx");
34375
- }
34376
- async function readBundledDaemonVersion(binaryDir) {
34377
- try {
34378
- const pkgPath = join7(binaryDir, "package.json");
34379
- const raw = await readFile15(pkgPath, "utf8");
34380
- const parsed = JSON.parse(raw);
34381
- const pinned = parsed.dependencies?.["@slock-ai/daemon"];
34382
- if (typeof pinned !== "string" || pinned.length === 0) return null;
34383
- if (pinned === "workspace:*") return null;
34384
- return pinned;
34385
- } catch {
34386
- return null;
33685
+ function mapSeaFailedPhaseToCode(failedPhase, reason) {
33686
+ switch (failedPhase) {
33687
+ case "resolve":
33688
+ return "UPGRADE_NETWORK_FAILED";
33689
+ case "stage":
33690
+ return reason && /sha[_ ]?mismatch|UPGRADE_SHA_MISMATCH/i.test(reason) ? "UPGRADE_INTEGRITY_FAILED" : "UPGRADE_NETWORK_FAILED";
33691
+ case "swap":
33692
+ return "UPGRADE_SWAP_FAILED";
34387
33693
  }
34388
33694
  }
34389
- async function defaultSpawnFreshService(slockHome) {
34390
- await spawnDetachedService(slockHome);
34391
- }
34392
33695
  async function runUpgradeCli(slockHome, opts, deps = {}) {
33696
+ const isSea = deps.isSeaBinary ?? isSeaBinary;
33697
+ if (!isSea()) {
33698
+ fail(
33699
+ "UPGRADE_SEA_ONLY",
33700
+ "`slock-computer upgrade` self-upgrades the SEA single-binary; this run is not a SEA binary (npm-installed or source/dev). Re-install the SEA binary to upgrade: `curl -fsSL https://github.com/botiverse/slock/releases/latest/download/install.sh | sh`."
33701
+ );
33702
+ }
34393
33703
  let channel2;
34394
33704
  if (opts.channel !== void 0) {
34395
33705
  const parsed = parseChannel(opts.channel);
@@ -34423,8 +33733,7 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
34423
33733
  `Could not reach npm registry to resolve ${channel2} dist-tag. Check connectivity or pass --target-version <semver>.`
34424
33734
  );
34425
33735
  }
34426
- const tagName = channel2;
34427
- const resolved = tags[tagName];
33736
+ const resolved = tags[channel2];
34428
33737
  if (typeof resolved !== "string" || resolved.length === 0) {
34429
33738
  fail(
34430
33739
  "UPGRADE_VERSION_RESOLVE_FAILED",
@@ -34434,14 +33743,7 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
34434
33743
  targetVersion = resolved;
34435
33744
  }
34436
33745
  const fromVersion = await (deps.currentVersion ?? defaultCurrentVersion)();
34437
- const currentBinaryDir = (deps.currentBinaryDir ?? defaultCurrentBinaryDir)();
34438
- const isEphemeralFn = deps.isEphemeralNpxContext ?? isEphemeralNpxContext;
34439
- if (isEphemeralFn(currentBinaryDir)) {
34440
- fail(
34441
- "UPGRADE_EPHEMERAL_CONTEXT",
34442
- `You're running upgrade via the npx ephemeral entrypoint, which can't reach your installed Computer service. Please run the installed \`slock-computer upgrade\` instead. If \`slock-computer\` isn't on your PATH, install it globally first: \`npm i -g @slock-ai/computer@latest\`.`
34443
- );
34444
- }
33746
+ const currentBinaryPath = (deps.currentBinaryPath ?? (() => process.execPath))();
34445
33747
  if (targetVersion === fromVersion) {
34446
33748
  info(`Already at ${fromVersion} (channel ${channel2}). No upgrade target.`);
34447
33749
  await appendUpgradeLogEntry(slockHome, {
@@ -34457,87 +33759,60 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
34457
33759
  }
34458
33760
  info(`Planning upgrade: ${fromVersion} \u2192 ${targetVersion} (channel ${channel2}).`);
34459
33761
  if (opts.dryRun) {
34460
- const stagePhaseFn = deps.stagePhaseFn ?? stagePhase;
34461
- const verifyPhaseFn = deps.verifyPhaseFn ?? verifyPhase;
34462
- let staged;
33762
+ const stageFn = deps.stageSeaPhaseFn ?? stageSeaPhase;
33763
+ const stagingDir = join6(slockHome, "upgrade-staging", `${targetVersion}-dryrun`);
34463
33764
  try {
34464
- staged = await stagePhaseFn(slockHome, targetVersion);
33765
+ const staged = await stageFn(stagingDir, targetVersion, process.platform, process.arch);
33766
+ info(
33767
+ `Dry run OK: ${targetVersion} downloaded + verified (sha256 ${staged.sha256.slice(0, 12)}). No swap performed.`
33768
+ );
34465
33769
  } catch (e) {
34466
33770
  const msg = e instanceof Error ? e.message : String(e);
34467
- fail("UPGRADE_DOWNLOAD_FAILED", `Dry run aborted: stage failed: ${msg}`);
34468
- }
34469
- const verify = await verifyPhaseFn(staged);
34470
- await cleanupStaged(slockHome, targetVersion);
34471
- if (!verify.ok) {
34472
- fail(
34473
- "UPGRADE_VERIFY_FAILED",
34474
- `Dry run: verify failed (${verify.reason}). actual=${verify.actual ?? "?"} advertised=${verify.advertised ?? "?"}.`
34475
- );
33771
+ const code2 = mapSeaFailedPhaseToCode("stage", msg);
33772
+ await rm4(stagingDir, { recursive: true, force: true }).catch(() => {
33773
+ });
33774
+ fail(code2, `Dry run aborted: stage failed: ${msg}`);
34476
33775
  }
34477
- info(
34478
- `Dry run OK: ${targetVersion} downloaded + verified (sha1 ${verify.actual}). No swap performed.`
34479
- );
33776
+ await rm4(stagingDir, { recursive: true, force: true }).catch(() => {
33777
+ });
34480
33778
  return;
34481
33779
  }
34482
- let drainMode = opts.drain;
34483
- if (drainMode === void 0 && opts.force === true) {
34484
- drainMode = "force";
34485
- }
34486
- if (drainMode !== void 0 && drainMode !== "drain") {
34487
- info(`Drain mode: ${drainMode}${drainMode === "force" ? " (in-flight turns will be dropped)" : ""}.`);
34488
- }
34489
- const readBundledFn = deps.readBundledDaemonVersion ?? readBundledDaemonVersion;
34490
- const fromBundledDaemonVersion = await readBundledFn(currentBinaryDir);
34491
- const runUpgradeFn = deps.runUpgradeFn ?? runUpgrade;
34492
- const spawnFreshService = deps.spawnFreshService ?? defaultSpawnFreshService;
34493
- const outcome = await runUpgradeFn(slockHome, {
34494
- targetVersion,
33780
+ const runFn = deps.resolveAndRunSeaUpgradeFn ?? resolveAndRunSeaUpgrade;
33781
+ const result = await runFn({
33782
+ slockHome,
33783
+ // Synthetic requestId — the CLI path has no remote requestId; the marker
33784
+ // is still written so a post-restart reconcile reports done coherently.
33785
+ requestId: `cli-${Date.now()}`,
34495
33786
  fromVersion,
34496
- channel: channel2,
34497
- currentBinaryDir,
34498
- drainMode,
33787
+ currentBinaryPath,
33788
+ nowIso: (/* @__PURE__ */ new Date()).toISOString(),
34499
33789
  deps: {
34500
- // Wire the production service-spawn into BOTH phase 5 (happy)
34501
- // and the operational-rollback respawn path inside `runUpgrade`.
34502
- // Without this, phase 5 falls through to a no-spawn health-poll
34503
- // and rollback later trips `failedStep="respawn"`. See Dayu
34504
- // blocker (#wg-slock-computer:b43b36fb msg=911eb84e).
34505
- spawnFreshService: () => spawnFreshService(slockHome)
33790
+ // Force the already-resolved target so the SEA flow doesn't re-resolve
33791
+ // the channel (and so `--target-version` / `--channel` overrides win).
33792
+ resolveTargetVersion: async () => targetVersion
34506
33793
  }
34507
33794
  });
34508
- const toBundledDaemonVersion = await readBundledFn(currentBinaryDir);
34509
- const bundle = (version, bundledDaemonVersion) => bundledDaemonVersion !== null ? { computerVersion: version, bundledDaemonVersion } : { computerVersion: version };
34510
- const fromBundle = bundle(fromVersion, fromBundledDaemonVersion);
34511
- const toBundle = bundle(targetVersion, toBundledDaemonVersion);
34512
33795
  const logTrigger = opts.trigger ?? "cli";
34513
- if (outcome.ok) {
34514
- info(
34515
- `Upgrade ${fromVersion} \u2192 ${targetVersion} succeeded (health-OK in ${outcome.restart?.healthAfterMs ?? 0}ms).`
34516
- );
34517
- await appendUpgradeLogEntry(slockHome, {
34518
- fromBundle,
34519
- toBundle,
34520
- channel: channel2,
34521
- trigger: logTrigger,
34522
- outcome: "ok"
34523
- }).catch(() => {
34524
- });
34525
- return;
34526
- }
34527
- const rolledBack = outcome.rolledBack === true ? " (swap rolled back)" : "";
34528
- if (outcome.phase === "drain") {
34529
- info(
34530
- `Upgrade deferred: --drain=defer and daemons are busy. Re-run when idle, or use --force to interrupt in-flight turns.`
34531
- );
34532
- return;
34533
- }
34534
- if (outcome.phase === "cleanup") {
33796
+ if (result.ok) {
33797
+ if (result.alreadyCurrent) {
33798
+ info(`Already at ${fromVersion} (channel ${channel2}). No upgrade target.`);
33799
+ await appendUpgradeLogEntry(slockHome, {
33800
+ fromBundle: { computerVersion: fromVersion },
33801
+ toBundle: { computerVersion: fromVersion },
33802
+ channel: channel2,
33803
+ trigger: logTrigger,
33804
+ outcome: "err",
33805
+ errorCode: "UPGRADE_NO_TARGET"
33806
+ }).catch(() => {
33807
+ });
33808
+ return;
33809
+ }
34535
33810
  info(
34536
- `Upgrade ${fromVersion} \u2192 ${targetVersion} succeeded; cleanup phase reported a non-fatal issue: ${outcome.reason ?? "unknown"}. Active layout + service are healthy.`
33811
+ `Upgrade staged: ${fromVersion} \u2192 ${targetVersion} downloaded, verified, and swapped. Restart the Computer service to run the new version (the managed supervisor does this automatically).`
34537
33812
  );
34538
33813
  await appendUpgradeLogEntry(slockHome, {
34539
- fromBundle,
34540
- toBundle,
33814
+ fromBundle: { computerVersion: fromVersion },
33815
+ toBundle: { computerVersion: targetVersion },
34541
33816
  channel: channel2,
34542
33817
  trigger: logTrigger,
34543
33818
  outcome: "ok"
@@ -34545,10 +33820,10 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
34545
33820
  });
34546
33821
  return;
34547
33822
  }
34548
- const code = mapFailurePhaseToCode(outcome);
33823
+ const code = mapSeaFailedPhaseToCode(result.failedPhase ?? "stage", result.reason);
34549
33824
  await appendUpgradeLogEntry(slockHome, {
34550
- fromBundle,
34551
- toBundle,
33825
+ fromBundle: { computerVersion: fromVersion },
33826
+ toBundle: { computerVersion: targetVersion },
34552
33827
  channel: channel2,
34553
33828
  trigger: logTrigger,
34554
33829
  outcome: "err",
@@ -34557,39 +33832,9 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
34557
33832
  });
34558
33833
  fail(
34559
33834
  code,
34560
- `Upgrade failed at phase=${outcome.phase}: ${outcome.reason ?? "unknown"}${rolledBack}.`
33835
+ `Upgrade failed at phase=${result.failedPhase ?? "stage"}: ${result.reason ?? "unknown"}.`
34561
33836
  );
34562
33837
  }
34563
- function mapFailurePhaseToCode(outcome) {
34564
- switch (outcome.phase) {
34565
- case "stage":
34566
- return "UPGRADE_NETWORK_FAILED";
34567
- case "verify":
34568
- return "UPGRADE_INTEGRITY_FAILED";
34569
- case "preflight":
34570
- return "UPGRADE_DEPS_CHANGED";
34571
- case "snapshot":
34572
- return "UPGRADE_SWAP_FAILED";
34573
- case "extract":
34574
- return "UPGRADE_INTEGRITY_FAILED";
34575
- case "hydrate":
34576
- if (outcome.hydratedDaemon?.ok === false) {
34577
- return "UPGRADE_INTEGRITY_FAILED";
34578
- }
34579
- return "UPGRADE_NETWORK_FAILED";
34580
- case "swap":
34581
- return "UPGRADE_SWAP_FAILED";
34582
- case "restart":
34583
- return "UPGRADE_RESTART_FAILED";
34584
- case "rolling_health":
34585
- return "UPGRADE_RESTART_FAILED";
34586
- case "drain":
34587
- case "cleanup":
34588
- throw new Error(
34589
- `mapFailurePhaseToCode: phase=${outcome.phase} should be handled by caller (silenced), not mapped to a closed-set code`
34590
- );
34591
- }
34592
- }
34593
33838
  async function defaultFetchDistTags() {
34594
33839
  const url = "https://registry.npmjs.org/@slock-ai/computer";
34595
33840
  const controller = new AbortController();
@@ -34613,9 +33858,10 @@ function defaultCurrentBinaryDir() {
34613
33858
  return dirname12(dirname12(here));
34614
33859
  }
34615
33860
  async function defaultCurrentVersion() {
34616
- const pkgPath = join7(defaultCurrentBinaryDir(), "package.json");
33861
+ if (isSeaBinary()) return COMPUTER_VERSION;
33862
+ const pkgPath = join6(defaultCurrentBinaryDir(), "package.json");
34617
33863
  try {
34618
- const raw = await readFile15(pkgPath, "utf8");
33864
+ const raw = await readFile14(pkgPath, "utf8");
34619
33865
  const parsed = JSON.parse(raw);
34620
33866
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
34621
33867
  return parsed.version;
@@ -34625,337 +33871,6 @@ async function defaultCurrentVersion() {
34625
33871
  throw new CliExit(1);
34626
33872
  }
34627
33873
 
34628
- // src/upgradeTestHarness.ts
34629
- init_esm_shims();
34630
- import { mkdir as mkdir15, readdir as readdir4, stat as stat4, writeFile as writeFile12 } from "fs/promises";
34631
- import { join as join8 } from "path";
34632
- import { createHash as createHash4 } from "crypto";
34633
- var PHASES = /* @__PURE__ */ new Set([
34634
- "stage",
34635
- "verify",
34636
- "snapshot",
34637
- "extract",
34638
- "swap",
34639
- "restart",
34640
- "rolling_health",
34641
- "cleanup"
34642
- ]);
34643
- function validateHarnessOptions(opts) {
34644
- if (opts.simulateFail !== void 0 && !PHASES.has(opts.simulateFail)) {
34645
- return `--simulate-fail: unknown phase "${opts.simulateFail}". Accepted: ${[...PHASES].join(" | ")}.`;
34646
- }
34647
- if (opts.simulateNetworkLossAt !== void 0 && opts.simulateNetworkLossAt !== "stage" && opts.simulateNetworkLossAt !== "verify") {
34648
- return `--simulate-network-loss-at: only "stage" or "verify" are network-bound phases (got "${opts.simulateNetworkLossAt}").`;
34649
- }
34650
- if (opts.simulateSleepWakeMs !== void 0) {
34651
- if (!Number.isFinite(opts.simulateSleepWakeMs) || opts.simulateSleepWakeMs < 0) {
34652
- return `--simulate-sleep-wake: must be a non-negative number of ms (got ${opts.simulateSleepWakeMs}).`;
34653
- }
34654
- if (opts.simulateSleepWakeMs > 6e4) {
34655
- return `--simulate-sleep-wake: max 60000ms to keep the harness bounded (got ${opts.simulateSleepWakeMs}).`;
34656
- }
34657
- }
34658
- return null;
34659
- }
34660
- function buildSimulatedDeps(slockHome, opts) {
34661
- const targetVersion = opts.targetVersion ?? "0.99.0";
34662
- const fromVersion = opts.fromVersion ?? "0.0.0-test";
34663
- const tarballBytes = Buffer.from(`harness-tarball-${targetVersion}`, "utf8");
34664
- const expectedSha = createHash4("sha1").update(tarballBytes).digest("hex");
34665
- const sleepMs = opts.simulateSleepWakeMs ?? 0;
34666
- const maybeSleep = async () => {
34667
- if (sleepMs > 0) {
34668
- await new Promise((r) => setTimeout(r, sleepMs));
34669
- }
34670
- };
34671
- let spawnCalls = 0;
34672
- let healthCalls = 0;
34673
- return {
34674
- opts: {
34675
- targetVersion,
34676
- fromVersion,
34677
- channel: "latest",
34678
- currentBinaryDir: join8(slockHome, "harness-current"),
34679
- drainMode: opts.drain,
34680
- deps: {
34681
- npmPack: async (cwd) => {
34682
- if (opts.simulateNetworkLossAt === "stage") {
34683
- throw new Error("simulated network loss during stage (npm pack)");
34684
- }
34685
- if (opts.simulateFail === "stage") {
34686
- return { tarballPath: "", exitCode: 1, stderr: "simulated stage failure" };
34687
- }
34688
- const filename = `slock-ai-computer-${targetVersion}.tgz`;
34689
- await writeFile12(join8(cwd, filename), tarballBytes);
34690
- return { tarballPath: join8(cwd, filename), exitCode: 0, stderr: "" };
34691
- },
34692
- fetchAdvertisedHash: async () => {
34693
- if (opts.simulateNetworkLossAt === "verify") return null;
34694
- if (opts.simulateFail === "verify") return "deadbeef";
34695
- return expectedSha;
34696
- },
34697
- tarSpawn: async (_t, destDir) => {
34698
- if (opts.simulateFail === "extract") {
34699
- return { exitCode: 1, stderr: "simulated extract failure" };
34700
- }
34701
- await mkdir15(join8(destDir, "package"), { recursive: true });
34702
- await writeFile12(join8(destDir, "package", "marker.txt"), `NEW@${targetVersion}`);
34703
- return { exitCode: 0, stderr: "" };
34704
- },
34705
- npmInstall: async () => ({ exitCode: 0, stderr: "" }),
34706
- fsRename: opts.simulateFail === "swap" ? async () => {
34707
- throw new Error("simulated swap failure: fsRename");
34708
- } : void 0,
34709
- readServicePid: async () => null,
34710
- spawnFreshService: async () => {
34711
- await maybeSleep();
34712
- spawnCalls += 1;
34713
- if (opts.simulateFail === "restart" && spawnCalls === 1) {
34714
- throw new Error("simulated spawn failure");
34715
- }
34716
- },
34717
- healthCheck: async () => {
34718
- await maybeSleep();
34719
- healthCalls += 1;
34720
- if (opts.simulateFail === "restart" && spawnCalls <= 1) return false;
34721
- return true;
34722
- },
34723
- healthTimeoutMs: opts.simulateFail === "restart" ? 200 : 5e3,
34724
- healthPollIntervalMs: 10,
34725
- listManagedServerIds: async () => {
34726
- if (opts.simulateFail === "rolling_health") {
34727
- return ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"];
34728
- }
34729
- return [];
34730
- },
34731
- readDaemonPid: async () => null,
34732
- perDaemonTimeoutMs: 50,
34733
- daemonPollIntervalMs: 10,
34734
- // PR-E 18/n phase 1.5 dep-drift preflight: the harness mocks
34735
- // `npmPack` to write fake tarball BYTES (not a real tar archive),
34736
- // so the production preflight's `tar -xzOf` would fail. Inject
34737
- // an always-passing predicate so the harness keeps exercising
34738
- // phases 3-6 (the surface it was built to cover). Dep-drift
34739
- // refusal semantics are covered by `preflightDepDrift.test.ts`
34740
- // and the new `runUpgrade: preflight fail …` integration tests
34741
- // in `upgrade.test.ts`, not here.
34742
- preflight: {
34743
- readTarballPackageJson: async () => ({
34744
- dependencies: { "@slock-ai/daemon": "^0.0.0-harness" }
34745
- }),
34746
- readCurrentPackageJson: async () => ({
34747
- dependencies: { "@slock-ai/daemon": "^0.0.0-harness" }
34748
- }),
34749
- readInstalledDaemonVersion: async () => "0.0.0-harness",
34750
- semverSatisfies: () => true,
34751
- allowUnsafeSpec: true
34752
- }
34753
- // snapshot-phase failure: caller arranges that `mkdir` on the
34754
- // snapshot path fails. We don't have a snapshot-phase dep hook,
34755
- // so we instead pre-create the snapshot file as a directory:
34756
- // that's handled outside `deps` via `arrangeSnapshotFailure`.
34757
- }
34758
- }
34759
- };
34760
- }
34761
- async function arrangeSnapshotFailure(slockHome) {
34762
- const snapshotPath = join8(slockHome, "computer", "upgrade-snapshot.json");
34763
- await mkdir15(snapshotPath, { recursive: true });
34764
- }
34765
- async function pathInfo(path3) {
34766
- try {
34767
- const s = await stat4(path3);
34768
- if (s.isDirectory()) return { kind: "dir", size: 0 };
34769
- if (s.isFile()) return { kind: "file", size: s.size };
34770
- return null;
34771
- } catch {
34772
- return null;
34773
- }
34774
- }
34775
- async function captureStateSnapshot(slockHome, version, currentBinaryDir) {
34776
- const stagingPath = upgradeStagingDir(slockHome, version);
34777
- const stagingExists = await pathInfo(stagingPath);
34778
- let stagingEntries = null;
34779
- if (stagingExists?.kind === "dir") {
34780
- try {
34781
- stagingEntries = (await readdir4(stagingPath)).sort();
34782
- } catch {
34783
- stagingEntries = null;
34784
- }
34785
- }
34786
- let servers = [];
34787
- try {
34788
- const ids = await readdir4(serversDir(slockHome));
34789
- servers = await Promise.all(
34790
- ids.map(async (serverId) => ({
34791
- serverId,
34792
- attachment: await pathInfo(serverAttachmentPath(slockHome, serverId)),
34793
- managed: await pathInfo(serverManagedFlagPath(slockHome, serverId))
34794
- }))
34795
- );
34796
- } catch {
34797
- }
34798
- return {
34799
- upgradeSnapshot: await pathInfo(upgradeSnapshotPath(slockHome)),
34800
- stagingDir: stagingExists,
34801
- stagingEntries,
34802
- servicePid: await pathInfo(servicePidPath(slockHome)),
34803
- channelFile: await pathInfo(join8(computerDir(slockHome), "channel")),
34804
- computerDir: await pathInfo(computerDir(slockHome)),
34805
- servers,
34806
- prevBinary: await pathInfo(currentBinaryDir + ".prev"),
34807
- currentBinary: await pathInfo(currentBinaryDir)
34808
- };
34809
- }
34810
- async function runUpgradeTestHarness(slockHome, opts, writer = (s) => process.stdout.write(s)) {
34811
- const err = validateHarnessOptions(opts);
34812
- if (err !== null) {
34813
- process.stderr.write(`__upgrade-test: ${err}
34814
- `);
34815
- process.exitCode = 1;
34816
- return;
34817
- }
34818
- await mkdir15(slockHome, { recursive: true });
34819
- if (opts.simulateFail === "snapshot") {
34820
- await arrangeSnapshotFailure(slockHome);
34821
- }
34822
- const { opts: upgradeOpts } = buildSimulatedDeps(slockHome, opts);
34823
- await mkdir15(upgradeOpts.currentBinaryDir, { recursive: true });
34824
- let outcome;
34825
- try {
34826
- outcome = await runUpgrade(slockHome, upgradeOpts);
34827
- } catch (e) {
34828
- outcome = {
34829
- ok: false,
34830
- phase: "stage",
34831
- reason: `harness_exception: ${e instanceof Error ? e.message : String(e)}`
34832
- };
34833
- }
34834
- const state = await captureStateSnapshot(
34835
- slockHome,
34836
- upgradeOpts.targetVersion,
34837
- upgradeOpts.currentBinaryDir
34838
- );
34839
- const payload = {
34840
- ok: outcome.ok,
34841
- phase: outcome.phase,
34842
- reason: outcome.reason ?? null,
34843
- rolledBack: outcome.rolledBack ?? false,
34844
- // Operational rollback per-step status, surfaced so QA can assert
34845
- // that disk + process both recovered (not just disk). null when no
34846
- // rollback was attempted (phases ≤ swap, or ok=true).
34847
- rollback: outcome.rollback ?? null,
34848
- simulated: {
34849
- fail: opts.simulateFail ?? null,
34850
- networkLossAt: opts.simulateNetworkLossAt ?? null,
34851
- sleepWakeMs: opts.simulateSleepWakeMs ?? null,
34852
- drain: opts.drain ?? null
34853
- },
34854
- state
34855
- };
34856
- writer(JSON.stringify(payload) + "\n");
34857
- }
34858
-
34859
- // src/upgradeInstallSmoke.ts
34860
- init_esm_shims();
34861
- import { copyFile, mkdir as mkdir16, readFile as readFile16 } from "fs/promises";
34862
- import { createHash as createHash5 } from "crypto";
34863
- import { dirname as dirname13, isAbsolute, join as join9, resolve as pathResolve } from "path";
34864
- import { fileURLToPath as fileURLToPath4 } from "url";
34865
- async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
34866
- if (typeof opts.packageTarball !== "string" || opts.packageTarball.trim().length === 0) {
34867
- throw new Error("__upgrade-install-smoke: --package-tarball is required.");
34868
- }
34869
- const tarballPath = isAbsolute(opts.packageTarball) ? opts.packageTarball : pathResolve(opts.packageTarball);
34870
- let tarballBytes;
34871
- try {
34872
- tarballBytes = await readFile16(tarballPath);
34873
- } catch (e) {
34874
- const msg = e instanceof Error ? e.message : String(e);
34875
- throw new Error(`__upgrade-install-smoke: cannot read --package-tarball ${tarballPath}: ${msg}`);
34876
- }
34877
- const tarballSha1 = createHash5("sha1").update(tarballBytes).digest("hex");
34878
- const currentBinaryDir = opts.currentBinaryDir ?? (deps.currentBinaryDir ?? defaultCurrentBinaryDirLocal)();
34879
- const fromVersion = opts.fromVersion ?? await (deps.currentVersion ?? defaultCurrentVersionLocal)();
34880
- const channel2 = opts.channel ?? "latest";
34881
- const spawnFreshService = deps.spawnFreshService ?? (async (h) => {
34882
- await spawnDetachedService(h);
34883
- });
34884
- await mkdir16(slockHome, { recursive: true });
34885
- const outcome = await runUpgrade(slockHome, {
34886
- targetVersion: opts.targetVersion,
34887
- fromVersion,
34888
- channel: channel2,
34889
- currentBinaryDir,
34890
- deps: {
34891
- // QA-only bypass of `npm pack <pkg>@<ver>`: copy the caller-supplied
34892
- // tarball into the staging dir under the expected filename. This is
34893
- // the ONLY deviation from production `runUpgrade` plumbing — every
34894
- // other dep falls back to its production default.
34895
- npmPack: async (cwd) => {
34896
- const filename = `slock-ai-computer-${opts.targetVersion}.tgz`;
34897
- const stagedTarball = join9(cwd, filename);
34898
- await copyFile(tarballPath, stagedTarball);
34899
- return { tarballPath: stagedTarball, exitCode: 0, stderr: "" };
34900
- },
34901
- // We trust the local tarball (caller built it from source via
34902
- // build-disposable-install.mjs). Return its sha1 as the
34903
- // "advertised" hash so verify is always satisfied.
34904
- fetchAdvertisedHash: async () => tarballSha1,
34905
- // Local QA tarballs are built from the workspace and may still
34906
- // carry workspace/file specs. The production upgrade path hydrates
34907
- // dependencies from the registry; this smoke isolates the swap /
34908
- // restart mechanics and keeps dependency hydration out of scope.
34909
- npmInstall: async () => ({ exitCode: 0, stderr: "" }),
34910
- spawnFreshService: () => spawnFreshService(slockHome),
34911
- // PR-E 18/n: the workspace-built tarball still carries
34912
- // `workspace:*` (or `file:` after pack-rewrite in some workflows)
34913
- // for `@slock-ai/daemon`, which production preflight rejects as
34914
- // unsafe. The harness opts in to `allowUnsafeSpec` so the smoke
34915
- // can run; production `runUpgrade` never sets this flag. The
34916
- // spec-equality + installed-satisfies checks still run on
34917
- // semver-shaped specs.
34918
- preflight: { allowUnsafeSpec: true }
34919
- }
34920
- });
34921
- const activeVersion = await readServiceVersionEvidence(slockHome);
34922
- return { outcome, activeVersion };
34923
- }
34924
- async function runUpgradeInstallSmokeCli(slockHome, opts, writer = (s) => process.stdout.write(s), deps = {}) {
34925
- let report;
34926
- try {
34927
- report = await runUpgradeInstallSmoke(slockHome, opts, deps);
34928
- } catch (e) {
34929
- const msg = e instanceof Error ? e.message : String(e);
34930
- writer(
34931
- JSON.stringify({
34932
- outcome: { ok: false, phase: "stage", reason: `harness_exception: ${msg}` },
34933
- activeVersion: null
34934
- }) + "\n"
34935
- );
34936
- return;
34937
- }
34938
- writer(JSON.stringify(report) + "\n");
34939
- }
34940
- function defaultCurrentBinaryDirLocal() {
34941
- const here = fileURLToPath4(import.meta.url);
34942
- return dirname13(dirname13(here));
34943
- }
34944
- async function defaultCurrentVersionLocal() {
34945
- const pkgPath = join9(defaultCurrentBinaryDirLocal(), "package.json");
34946
- try {
34947
- const raw = await readFile16(pkgPath, "utf8");
34948
- const parsed = JSON.parse(raw);
34949
- if (typeof parsed.version === "string" && parsed.version.length > 0) {
34950
- return parsed.version;
34951
- }
34952
- } catch {
34953
- }
34954
- throw new Error(
34955
- "__upgrade-install-smoke: cannot read current binary version from package.json; pass --from-version explicitly."
34956
- );
34957
- }
34958
-
34959
33874
  // src/index.ts
34960
33875
  function withCliExit(fn) {
34961
33876
  return async (...args) => {
@@ -34970,17 +33885,20 @@ function withCliExit(fn) {
34970
33885
  }
34971
33886
  };
34972
33887
  }
33888
+ var FOREGROUND_DESC = "run the service in this terminal instead of the background";
33889
+ var SERVER_SLUG_TARGET_DESC = "target Slock server slug (canonical form `/myserver`; bare `myserver` accepted)";
33890
+ var SERVER_SLUG_OPTIONAL_DESC = "optional: scope to one attached server (canonical `/myserver`; bare accepted; default: all attached)";
34973
33891
  var program2 = new Command();
34974
33892
  program2.name("slock-computer").description("Slock Computer \u2014 local-machine control plane (login + N per-server attachments).").version(COMPUTER_VERSION);
34975
33893
  program2.command("login").description("Log in via device-code (one user identity per Computer / SLOCK_HOME).").option("--server-url <url>", `Slock API base URL; defaults to SLOCK_SERVER_URL or ${DEFAULT_SLOCK_SERVER_URL}`).action(withCliExit(async (opts) => {
34976
33894
  await runLogin({ serverUrl: opts.serverUrl });
34977
33895
  }));
34978
- program2.command("attach").argument("<serverSlug>", "target Slock server slug (canonical form `/myserver`; bare `myserver` accepted)").description("Attach this Computer to one Slock server (add-not-replace; multi-server OK).").option("--server-url <url>", `Slock API base URL; defaults to the saved user session, SLOCK_SERVER_URL, or ${DEFAULT_SLOCK_SERVER_URL}`).option("--name <name>", "Computer display name; defaults to a sanitized hostname").option("--no-run", "only authorize + write local state; do not start the service").option("--foreground", "run the service in this terminal instead of the background").action(withCliExit(async (serverSlug, opts) => {
33896
+ program2.command("attach").argument("<serverSlug>", SERVER_SLUG_TARGET_DESC).description("Attach this Computer to one Slock server (add-not-replace; multi-server OK).").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; defaults to a sanitized hostname").option("--no-run", "only authorize + write local state; do not start the service").option("--foreground", FOREGROUND_DESC).action(withCliExit(async (serverSlug, opts) => {
34979
33897
  await withMutationLock(
34980
33898
  () => runAttach({ serverSlug, serverUrl: opts.serverUrl, name: opts.name, run: opts.run, foreground: opts.foreground })
34981
33899
  );
34982
33900
  }));
34983
- 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 service").option("--foreground", "run the service in this terminal instead of the background").option("-y, --yes", "allow non-interactive setup after confirming the planned actions").action(
33901
+ program2.command("setup").argument("<serverSlug>", SERVER_SLUG_TARGET_DESC).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 service").option("--foreground", FOREGROUND_DESC).option("-y, --yes", "allow non-interactive setup after confirming the planned actions").action(
34984
33902
  withCliExit(async (serverSlug, opts) => {
34985
33903
  await withMutationLock(
34986
33904
  () => runSetup({
@@ -34994,13 +33912,13 @@ program2.command("setup").argument("<serverSlug>", "target Slock server slug (ca
34994
33912
  );
34995
33913
  })
34996
33914
  );
34997
- 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) => {
33915
+ program2.command("detach").argument("<serverSlug>", `${SERVER_SLUG_TARGET_DESC} (the server to detach from this Computer)`).description("Remove ONE server's local attachment; never touches user-session or other servers.").action(withCliExit(async (serverSlug) => {
34998
33916
  await withMutationLock(async () => runDetach(await resolveTargetServerId({ server: serverSlug }), serverSlug));
34999
33917
  }));
35000
33918
  program2.command("status").description("Show this Computer's aggregate state (login + service + per-server server-runners).").option("--json", "emit the machine-readable report").action(withCliExit(async (opts) => {
35001
33919
  await runStatus({ json: opts.json });
35002
33920
  }));
35003
- program2.command("start").argument("[serverSlug]", "optional: verify this server is attached and ensure its server-runner is reconciled (default: ensure all attached)").description("Start/ensure the Computer service (manages all per-server server-runners).").option("--foreground", "stay in this terminal instead of detaching").action(withCliExit(async (serverSlug, opts) => {
33921
+ program2.command("start").argument("[serverSlug]", SERVER_SLUG_OPTIONAL_DESC).description("Start/ensure the Computer service (manages all per-server server-runners).").option("--foreground", FOREGROUND_DESC).action(withCliExit(async (serverSlug, opts) => {
35004
33922
  await withMutationLock(
35005
33923
  async () => runStart({
35006
33924
  foreground: opts.foreground,
@@ -35012,17 +33930,25 @@ program2.command("start").argument("[serverSlug]", "optional: verify this server
35012
33930
  program2.command("stop").description("Stop the Computer service (and all managed per-server server-runners).").action(withCliExit(async () => {
35013
33931
  await withMutationLock(() => runStop());
35014
33932
  }));
35015
- program2.command("doctor").argument("[serverSlug]", "optional: scope detail (recent crashes) to one server").description("Diagnose login + per-server attachments + per-server preflight (no secrets).").option("--json", "emit the machine-readable report").option("--cleanup", "after diagnosis, run the local residue cleanup pass").option("--fix", "alias for --cleanup (same behavior)").option("--reset-health", "clear <serverSlug>'s crash history so service resumes auto-restart").action(
33933
+ program2.command("restart").argument("[serverSlug]", SERVER_SLUG_OPTIONAL_DESC).description("Restart the Computer service (stop + start; all managed per-server server-runners).").option("--foreground", FOREGROUND_DESC).action(withCliExit(async (serverSlug, opts) => {
33934
+ await withMutationLock(async () => {
33935
+ await runStop();
33936
+ await runStart({
33937
+ foreground: opts.foreground,
33938
+ serverId: serverSlug ? await resolveTargetServerId({ server: serverSlug }) : null,
33939
+ serverLabel: serverSlug ?? null
33940
+ });
33941
+ });
33942
+ }));
33943
+ program2.command("doctor").argument("[serverSlug]", `${SERVER_SLUG_OPTIONAL_DESC} (scopes recent-crash detail to that server)`).description("Diagnose login + per-server attachments + per-server preflight (no secrets).").option("--json", "emit the machine-readable report").option("--fix", "after diagnosis, run the local residue cleanup pass").action(
35016
33944
  withCliExit(
35017
33945
  async (serverSlug, opts) => {
35018
33946
  const serverId = serverSlug ? await resolveTargetServerId({ server: serverSlug }) : void 0;
35019
33947
  await runDoctor({
35020
33948
  json: opts.json,
35021
- cleanup: opts.cleanup,
35022
- fix: opts.fix,
33949
+ cleanup: opts.fix,
35023
33950
  serverId,
35024
- serverLabel: serverSlug,
35025
- resetHealth: opts.resetHealth
33951
+ serverLabel: serverSlug
35026
33952
  });
35027
33953
  }
35028
33954
  )
@@ -35039,8 +33965,8 @@ program2.command("reset").description("Clear a degraded state and resume the ser
35039
33965
  );
35040
33966
  })
35041
33967
  );
35042
- program2.command("logs").description("Tail one server's server-runner log (or the service 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("--service", "tail the global service log instead of a per-server server-runner log").action(withCliExit(async (opts) => {
35043
- await runLogs({ lines: opts.lines, server: opts.server ?? null, service: !!opts.service });
33968
+ program2.command("logs").argument("[serverSlug]", `${SERVER_SLUG_OPTIONAL_DESC} (required when \u22652 attached; ignored with --service)`).description("Tail one server's server-runner log (or the service log); secrets redacted.").option("--lines <n>", "trailing lines to show (default 200)", (v) => Number.parseInt(v, 10)).option("--service", "tail the global service log instead of a per-server server-runner log").action(withCliExit(async (serverSlug, opts) => {
33969
+ await runLogs({ lines: opts.lines, server: serverSlug ?? null, service: !!opts.service });
35044
33970
  }));
35045
33971
  var runners = program2.command("runners").description("Computer runner control plane (per-server scoped; \xA712 whitelist server-side).");
35046
33972
  runners.command("list").description("List runners on one attached server.").option("--json", "emit the machine-readable list").option("--server <slug>", "select target server slug (required when \u22652 attached)").action(withCliExit(async (opts) => {
@@ -35062,26 +33988,16 @@ channel.command("set").argument("<channel>", "channel value: latest | alpha | pi
35062
33988
  );
35063
33989
  program2.command("upgrade").description(
35064
33990
  [
35065
- "Auto-upgrade this Computer to the latest version in its channel (or --target-version <semver>).",
33991
+ "Auto-upgrade this Computer (SEA single-binary) to the latest version in its channel (or --target-version <semver>).",
35066
33992
  "",
35067
- "Stages the target package, hydrates production dependencies, verifies the",
35068
- "hydrated @slock-ai/daemon version, then swaps the Computer package root."
33993
+ "Downloads + sha256-verifies the target-version binary for this platform,",
33994
+ "then atomically swaps the running single executable. The managed service",
33995
+ "supervisor restarts on the swapped binary. SEA-only: a non-SEA (npm/dev)",
33996
+ "run has no single binary to self-swap and is refused (UPGRADE_SEA_ONLY)."
35069
33997
  ].join("\n")
35070
- ).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(
33998
+ ).option("--dry-run", "download + sha256-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)").action(
35071
33999
  withCliExit(
35072
34000
  async (opts) => {
35073
- let drain;
35074
- if (opts.drain !== void 0) {
35075
- if (opts.drain !== "drain" && opts.drain !== "defer" && opts.drain !== "force") {
35076
- process.stderr.write(
35077
- `slock-computer: UPGRADE_DRAIN_INVALID: Invalid --drain "${opts.drain}". Accepted: drain | defer | force.
35078
- `
35079
- );
35080
- process.exitCode = 1;
35081
- return;
35082
- }
35083
- drain = opts.drain;
35084
- }
35085
34001
  const slockHome = resolveSlockHome();
35086
34002
  const trigger = resolveUpgradeTrigger(process.env.SLOCK_UPGRADE_TRIGGER);
35087
34003
  try {
@@ -35090,8 +34006,6 @@ program2.command("upgrade").description(
35090
34006
  dryRun: opts.dryRun,
35091
34007
  channel: opts.channel,
35092
34008
  targetVersion: opts.targetVersion,
35093
- drain,
35094
- force: opts.force,
35095
34009
  trigger
35096
34010
  })
35097
34011
  );
@@ -35128,43 +34042,29 @@ program2.command("__service", { hidden: true }).action(withCliExit(async () => {
35128
34042
  program2.command("__run", { hidden: true }).argument("<serverId>", "server id this daemon child is bound to").action(withCliExit(async (serverId) => {
35129
34043
  await runResident(serverId);
35130
34044
  }));
35131
- program2.command("__upgrade-test", { hidden: true }).option("--simulate-fail <phase>", "inject deterministic failure at named phase").option("--simulate-network-loss-at <phase>", "drop the fetch right when phase begins (stage|verify)").option("--simulate-sleep-wake <ms>", "insert pause inside spawn/health-poll (max 60000)").option("--drain <mode>", "drain mode forwarded to runUpgrade").option("--target-version <semver>", "label for the simulated upgrade (default 0.99.0)").option("--from-version <semver>", "label for the simulated from-version (default 0.0.0-test)").action(
35132
- withCliExit(async (opts) => {
35133
- const drain = opts.drain;
35134
- if (drain !== void 0 && drain !== "drain" && drain !== "defer" && drain !== "force") {
35135
- process.stderr.write(`__upgrade-test: invalid --drain "${drain}".
34045
+ async function runCli() {
34046
+ if (process.argv[2] === "__cli") {
34047
+ const { runBundledSlockCli } = await import("@slock-ai/daemon/core");
34048
+ await runBundledSlockCli(process.argv.slice(3));
34049
+ return;
34050
+ }
34051
+ await program2.parseAsync(process.argv);
34052
+ }
34053
+ var invokedAsMain = process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href;
34054
+ if (invokedAsMain) {
34055
+ runCli().catch((err) => {
34056
+ process.stderr.write(`slock-computer: ${err instanceof Error ? err.message : String(err)}
35136
34057
  `);
35137
- process.exitCode = 1;
35138
- return;
35139
- }
35140
- await runUpgradeTestHarness(resolveSlockHome(), {
35141
- simulateFail: opts.simulateFail,
35142
- simulateNetworkLossAt: opts.simulateNetworkLossAt,
35143
- simulateSleepWakeMs: opts.simulateSleepWake !== void 0 ? Number(opts.simulateSleepWake) : void 0,
35144
- drain,
35145
- targetVersion: opts.targetVersion,
35146
- fromVersion: opts.fromVersion
35147
- });
35148
- })
35149
- );
35150
- program2.command("__upgrade-install-smoke", { hidden: true }).requiredOption("--package-tarball <path>", "local tarball to swap in for stage (QA only)").option("--target-version <semver>", "target version label", "0.99.0-smoke").option("--from-version <semver>", "from-version label (defaults to current binary's package.json version)").option("--channel <name>", "channel label for snapshot", "latest").option("--current-binary-dir <path>", "override install root (defaults to currently-running binary)").action(
35151
- withCliExit(async (opts) => {
35152
- await withMutationLock(
35153
- () => runUpgradeInstallSmokeCli(resolveSlockHome(), {
35154
- packageTarball: opts.packageTarball,
35155
- targetVersion: opts.targetVersion,
35156
- fromVersion: opts.fromVersion,
35157
- channel: opts.channel,
35158
- currentBinaryDir: opts.currentBinaryDir
35159
- })
35160
- );
35161
- })
35162
- );
35163
- program2.parseAsync(process.argv).catch((err) => {
35164
- process.stderr.write(`slock-computer: ${err instanceof Error ? err.message : String(err)}
34058
+ if (process.env.SLOCK_COMPUTER_DEBUG_STACK && err instanceof Error && err.stack) {
34059
+ process.stderr.write(`${err.stack}
35165
34060
  `);
35166
- process.exitCode = 1;
35167
- });
34061
+ }
34062
+ process.exitCode = 1;
34063
+ });
34064
+ }
34065
+ export {
34066
+ program2 as program
34067
+ };
35168
34068
  /*! Bundled license information:
35169
34069
 
35170
34070
  undici/lib/web/fetch/body.js: