@botiverse/raft-computer 0.0.59 → 0.0.61

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
@@ -29592,6 +29592,7 @@ var {
29592
29592
 
29593
29593
  // src/login.ts
29594
29594
  init_esm_shims();
29595
+ import { unlink } from "fs/promises";
29595
29596
 
29596
29597
  // src/output.ts
29597
29598
  init_esm_shims();
@@ -29619,6 +29620,124 @@ function fail(code, message, exitCode = 1) {
29619
29620
  throw new CliExit(exitCode, code);
29620
29621
  }
29621
29622
 
29623
+ // src/paths.ts
29624
+ init_esm_shims();
29625
+ import { createHash } from "crypto";
29626
+ import os from "os";
29627
+ import path2 from "path";
29628
+ var CURRENT_SCHEMA_VERSION = 1;
29629
+ function resolveSlockHome(env = process.env, homeDir = os.homedir()) {
29630
+ const configured = env.SLOCK_HOME?.trim();
29631
+ const raw = configured && configured.length > 0 ? configured : path2.join(homeDir, ".slock");
29632
+ return path2.resolve(expandHome(raw, homeDir));
29633
+ }
29634
+ function expandHome(input, homeDir) {
29635
+ if (input === "~") return homeDir;
29636
+ if (input.startsWith("~/")) return path2.join(homeDir, input.slice(2));
29637
+ return input;
29638
+ }
29639
+ function computerDir(slockHome) {
29640
+ return path2.join(slockHome, "computer");
29641
+ }
29642
+ function userSessionPath(slockHome) {
29643
+ return path2.join(computerDir(slockHome), "user-session.json");
29644
+ }
29645
+ var SERVER_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
29646
+ function isValidServerId(serverId) {
29647
+ return SERVER_ID_RE.test(serverId);
29648
+ }
29649
+ function assertValidServerId(serverId) {
29650
+ if (!isValidServerId(serverId)) {
29651
+ throw new Error(`invalid server id: ${JSON.stringify(serverId)} (expected a UUID)`);
29652
+ }
29653
+ return serverId;
29654
+ }
29655
+ function serversDir(slockHome) {
29656
+ return path2.join(computerDir(slockHome), "servers");
29657
+ }
29658
+ function serverDir(slockHome, serverId) {
29659
+ return path2.join(serversDir(slockHome), assertValidServerId(serverId));
29660
+ }
29661
+ function serverAttachmentPath(slockHome, serverId) {
29662
+ return path2.join(serverDir(slockHome, serverId), "runner.state.json");
29663
+ }
29664
+ function legacyServerAttachmentPath(slockHome, serverId) {
29665
+ return path2.join(serverDir(slockHome, serverId), "attachment.json");
29666
+ }
29667
+ function serverRunnerPidPath(slockHome, serverId) {
29668
+ return path2.join(serverDir(slockHome, serverId), "runner.pid");
29669
+ }
29670
+ function serverRunnerLogPath(slockHome, serverId) {
29671
+ return path2.join(serverDir(slockHome, serverId), "runner.log");
29672
+ }
29673
+ function serverManagedFlagPath(slockHome, serverId) {
29674
+ return path2.join(serverDir(slockHome, serverId), "managed.flag");
29675
+ }
29676
+ function serverHealthPath(slockHome, serverId) {
29677
+ return path2.join(serverDir(slockHome, serverId), "health.json");
29678
+ }
29679
+ function serviceRunDir(slockHome) {
29680
+ return path2.join(computerDir(slockHome), "run");
29681
+ }
29682
+ function serviceStatePath(slockHome) {
29683
+ return path2.join(serviceRunDir(slockHome), "service.state.json");
29684
+ }
29685
+ function servicePidPath(slockHome) {
29686
+ return path2.join(serviceRunDir(slockHome), "service.pid");
29687
+ }
29688
+ function servicePidReadFallback(slockHome) {
29689
+ return [servicePidPath(slockHome)];
29690
+ }
29691
+ function serviceLogPath(slockHome) {
29692
+ return path2.join(serviceRunDir(slockHome), "service.log");
29693
+ }
29694
+ function serviceSocketPath(slockHome) {
29695
+ return path2.join(computerDir(slockHome), "run", "service.sock");
29696
+ }
29697
+ function serviceWindowsPipeName(slockHome) {
29698
+ const hash = createHash("sha256").update(computerDir(slockHome)).digest("hex").slice(0, 16);
29699
+ return `\\\\.\\pipe\\slock-computer-${hash}`;
29700
+ }
29701
+ function serviceVersionPath(slockHome) {
29702
+ return path2.join(computerDir(slockHome), "service-version.json");
29703
+ }
29704
+ var HOSTNAME_SUFFIXES = [
29705
+ ".fritz.box",
29706
+ ".localdomain",
29707
+ ".local",
29708
+ ".lan",
29709
+ ".home"
29710
+ ];
29711
+ function shortHostnameHash(hostname) {
29712
+ return createHash("sha256").update(hostname).digest("hex").slice(0, 8);
29713
+ }
29714
+ function deriveDefaultComputerName(hostname = os.hostname()) {
29715
+ const original = hostname.trim();
29716
+ let name = original;
29717
+ let lower = name.toLowerCase();
29718
+ for (; ; ) {
29719
+ const suffix = HOSTNAME_SUFFIXES.find((candidate) => lower.endsWith(candidate));
29720
+ if (!suffix) break;
29721
+ name = name.slice(0, -suffix.length);
29722
+ lower = name.toLowerCase();
29723
+ }
29724
+ name = name.replace(/[\s'"]+/g, "-").replace(/-+$/g, "");
29725
+ return name.length > 0 ? name : `slock-computer-${shortHostnameHash(original)}`;
29726
+ }
29727
+ function adoptionLogPath(slockHome) {
29728
+ return path2.join(computerDir(slockHome), "adoption.log");
29729
+ }
29730
+ function channelPath(slockHome) {
29731
+ return path2.join(computerDir(slockHome), "channel");
29732
+ }
29733
+ function upgradeLogPath(slockHome) {
29734
+ return path2.join(computerDir(slockHome), "upgrade.log");
29735
+ }
29736
+ function formatUpgradeLogTimestamp(date = /* @__PURE__ */ new Date()) {
29737
+ const iso = date.toISOString();
29738
+ return iso.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
29739
+ }
29740
+
29622
29741
  // src/services/errors.ts
29623
29742
  init_esm_shims();
29624
29743
  var ComputerServiceError = class extends Error {
@@ -29931,120 +30050,6 @@ var RunnersClient = class {
29931
30050
  }
29932
30051
  };
29933
30052
 
29934
- // src/paths.ts
29935
- init_esm_shims();
29936
- import { createHash } from "crypto";
29937
- import os from "os";
29938
- import path2 from "path";
29939
- function resolveSlockHome(env = process.env, homeDir = os.homedir()) {
29940
- const configured = env.SLOCK_HOME?.trim();
29941
- const raw = configured && configured.length > 0 ? configured : path2.join(homeDir, ".slock");
29942
- return path2.resolve(expandHome(raw, homeDir));
29943
- }
29944
- function expandHome(input, homeDir) {
29945
- if (input === "~") return homeDir;
29946
- if (input.startsWith("~/")) return path2.join(homeDir, input.slice(2));
29947
- return input;
29948
- }
29949
- function computerDir(slockHome) {
29950
- return path2.join(slockHome, "computer");
29951
- }
29952
- function userSessionPath(slockHome) {
29953
- return path2.join(computerDir(slockHome), "user-session.json");
29954
- }
29955
- var SERVER_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
29956
- function isValidServerId(serverId) {
29957
- return SERVER_ID_RE.test(serverId);
29958
- }
29959
- function assertValidServerId(serverId) {
29960
- if (!isValidServerId(serverId)) {
29961
- throw new Error(`invalid server id: ${JSON.stringify(serverId)} (expected a UUID)`);
29962
- }
29963
- return serverId;
29964
- }
29965
- function serversDir(slockHome) {
29966
- return path2.join(computerDir(slockHome), "servers");
29967
- }
29968
- function serverDir(slockHome, serverId) {
29969
- return path2.join(serversDir(slockHome), assertValidServerId(serverId));
29970
- }
29971
- function serverAttachmentPath(slockHome, serverId) {
29972
- return path2.join(serverDir(slockHome, serverId), "runner.state.json");
29973
- }
29974
- function serverRunnerPidPath(slockHome, serverId) {
29975
- return path2.join(serverDir(slockHome, serverId), "server-runner.pid");
29976
- }
29977
- function serverRunnerLogPath(slockHome, serverId) {
29978
- return path2.join(serverDir(slockHome, serverId), "server-runner.log");
29979
- }
29980
- function serverManagedFlagPath(slockHome, serverId) {
29981
- return path2.join(serverDir(slockHome, serverId), "managed.flag");
29982
- }
29983
- function serverHealthPath(slockHome, serverId) {
29984
- return path2.join(serverDir(slockHome, serverId), "health.json");
29985
- }
29986
- function serviceRunDir(slockHome) {
29987
- return path2.join(computerDir(slockHome), "run");
29988
- }
29989
- function serviceStatePath(slockHome) {
29990
- return path2.join(serviceRunDir(slockHome), "service.state.json");
29991
- }
29992
- function servicePidPath(slockHome) {
29993
- return path2.join(serviceRunDir(slockHome), "service.pid");
29994
- }
29995
- function servicePidReadFallback(slockHome) {
29996
- return [servicePidPath(slockHome)];
29997
- }
29998
- function serviceLogPath(slockHome) {
29999
- return path2.join(serviceRunDir(slockHome), "service.log");
30000
- }
30001
- function serviceSocketPath(slockHome) {
30002
- return path2.join(computerDir(slockHome), "run", "service.sock");
30003
- }
30004
- function serviceWindowsPipeName(slockHome) {
30005
- const hash = createHash("sha256").update(computerDir(slockHome)).digest("hex").slice(0, 16);
30006
- return `\\\\.\\pipe\\slock-computer-${hash}`;
30007
- }
30008
- function serviceVersionPath(slockHome) {
30009
- return path2.join(computerDir(slockHome), "service-version.json");
30010
- }
30011
- var HOSTNAME_SUFFIXES = [
30012
- ".fritz.box",
30013
- ".localdomain",
30014
- ".local",
30015
- ".lan",
30016
- ".home"
30017
- ];
30018
- function shortHostnameHash(hostname) {
30019
- return createHash("sha256").update(hostname).digest("hex").slice(0, 8);
30020
- }
30021
- function deriveDefaultComputerName(hostname = os.hostname()) {
30022
- const original = hostname.trim();
30023
- let name = original;
30024
- let lower = name.toLowerCase();
30025
- for (; ; ) {
30026
- const suffix = HOSTNAME_SUFFIXES.find((candidate) => lower.endsWith(candidate));
30027
- if (!suffix) break;
30028
- name = name.slice(0, -suffix.length);
30029
- lower = name.toLowerCase();
30030
- }
30031
- name = name.replace(/[\s'"]+/g, "-").replace(/-+$/g, "");
30032
- return name.length > 0 ? name : `slock-computer-${shortHostnameHash(original)}`;
30033
- }
30034
- function adoptionLogPath(slockHome) {
30035
- return path2.join(computerDir(slockHome), "adoption.log");
30036
- }
30037
- function channelPath(slockHome) {
30038
- return path2.join(computerDir(slockHome), "channel");
30039
- }
30040
- function upgradeLogPath(slockHome) {
30041
- return path2.join(computerDir(slockHome), "upgrade.log");
30042
- }
30043
- function formatUpgradeLogTimestamp(date = /* @__PURE__ */ new Date()) {
30044
- const iso = date.toISOString();
30045
- return iso.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
30046
- }
30047
-
30048
30053
  // src/serverUrl.ts
30049
30054
  init_esm_shims();
30050
30055
  var DEFAULT_SLOCK_SERVER_URL = "https://api.raft.build";
@@ -30145,6 +30150,8 @@ async function login(input, options = {}) {
30145
30150
  JSON.stringify(
30146
30151
  {
30147
30152
  kind: "user-session",
30153
+ // On-disk schema version; readers tolerate a missing value.
30154
+ schemaVersion: CURRENT_SCHEMA_VERSION,
30148
30155
  userId: r.userId,
30149
30156
  accessToken: r.accessToken,
30150
30157
  refreshToken: r.refreshToken,
@@ -30193,21 +30200,36 @@ async function runLogin(opts) {
30193
30200
  throw err;
30194
30201
  }
30195
30202
  }
30203
+ async function runLogout() {
30204
+ const sessionPath = userSessionPath(resolveSlockHome());
30205
+ try {
30206
+ await unlink(sessionPath);
30207
+ info(`Logged out. Cleared user session at ${sessionPath}.`);
30208
+ } catch (err) {
30209
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
30210
+ info("Already logged out (no saved user session).");
30211
+ return;
30212
+ }
30213
+ throw err;
30214
+ }
30215
+ }
30196
30216
 
30197
30217
  // src/attach.ts
30198
30218
  init_esm_shims();
30199
30219
 
30200
30220
  // src/serverState.ts
30201
30221
  init_esm_shims();
30202
- import { readFile, readdir, writeFile as writeFile2, mkdir as mkdir2, unlink, access, chmod } from "fs/promises";
30222
+ import { readFile, readdir, writeFile as writeFile2, mkdir as mkdir2, unlink as unlink2, access, chmod } from "fs/promises";
30203
30223
  import { dirname as dirname2 } from "path";
30204
30224
  import { constants as fsConstants } from "fs";
30205
30225
  function parseAttachment(raw) {
30206
30226
  try {
30207
30227
  const a = JSON.parse(raw);
30208
30228
  if (a.kind === "computer-attachment" && typeof a.serverId === "string" && typeof a.serverMachineId === "string" && typeof a.apiKey === "string" && a.apiKey.length > 0 && typeof a.serverUrl === "string") {
30229
+ const schemaVersion = typeof a.schemaVersion === "number" ? a.schemaVersion : CURRENT_SCHEMA_VERSION;
30209
30230
  return {
30210
30231
  kind: "computer-attachment",
30232
+ schemaVersion,
30211
30233
  serverId: a.serverId,
30212
30234
  serverSlug: typeof a.serverSlug === "string" && a.serverSlug.length > 0 ? a.serverSlug : void 0,
30213
30235
  serverMachineId: a.serverMachineId,
@@ -30231,13 +30253,43 @@ async function readAttachmentAt(path3) {
30231
30253
  }
30232
30254
  async function readServerAttachment(slockHome, serverId) {
30233
30255
  if (!isValidServerId(serverId)) return null;
30234
- return readAttachmentAt(serverAttachmentPath(slockHome, serverId));
30256
+ const current = await readAttachmentAt(serverAttachmentPath(slockHome, serverId));
30257
+ const legacy = await readAttachmentAt(legacyServerAttachmentPath(slockHome, serverId));
30258
+ let effective;
30259
+ let decidedByLegacy;
30260
+ if (!current) {
30261
+ effective = legacy;
30262
+ decidedByLegacy = legacy !== null;
30263
+ } else if (!legacy) {
30264
+ effective = current;
30265
+ decidedByLegacy = false;
30266
+ } else if (
30267
+ // Adopted-machine-still-wins: when the legacy attachment is an adopted
30268
+ // (working) machine and the current one is a different, not-adopted machine
30269
+ // (e.g. a fresh attach that the server rejects), prefer the legacy creds.
30270
+ legacy.adoptedFromLegacy === true && current.adoptedFromLegacy !== true && legacy.serverMachineId !== current.serverMachineId
30271
+ ) {
30272
+ effective = legacy;
30273
+ decidedByLegacy = true;
30274
+ } else {
30275
+ effective = current;
30276
+ decidedByLegacy = false;
30277
+ }
30278
+ if (decidedByLegacy && effective) {
30279
+ try {
30280
+ await writeServerAttachment(slockHome, effective);
30281
+ await unlink2(legacyServerAttachmentPath(slockHome, serverId));
30282
+ } catch {
30283
+ }
30284
+ }
30285
+ return effective;
30235
30286
  }
30236
30287
  async function writeServerAttachment(slockHome, attachment) {
30237
30288
  if (!isValidServerId(attachment.serverId)) return;
30238
30289
  const path3 = serverAttachmentPath(slockHome, attachment.serverId);
30239
30290
  await mkdir2(dirname2(path3), { recursive: true });
30240
- await writeFile2(path3, JSON.stringify(attachment, null, 2), { mode: 384 });
30291
+ const stamped = { ...attachment, schemaVersion: CURRENT_SCHEMA_VERSION };
30292
+ await writeFile2(path3, JSON.stringify(stamped, null, 2), { mode: 384 });
30241
30293
  await chmod(path3, 384);
30242
30294
  }
30243
30295
  async function listAttachedServerIds(slockHome) {
@@ -30287,7 +30339,7 @@ async function setServerManaged(slockHome, serverId) {
30287
30339
  async function clearServerManaged(slockHome, serverId) {
30288
30340
  if (!isValidServerId(serverId)) return;
30289
30341
  try {
30290
- await unlink(serverManagedFlagPath(slockHome, serverId));
30342
+ await unlink2(serverManagedFlagPath(slockHome, serverId));
30291
30343
  } catch {
30292
30344
  }
30293
30345
  }
@@ -30482,7 +30534,7 @@ async function runAttach(opts) {
30482
30534
  if (opts.orchestrated) {
30483
30535
  return;
30484
30536
  }
30485
- if (opts.run === false) {
30537
+ if (opts.start === false) {
30486
30538
  info("Next: run `slock-computer start` to run it in the background.");
30487
30539
  } else {
30488
30540
  info("Next: run `slock-computer start` (background) \u2014 closing the terminal is fine.");
@@ -31025,13 +31077,13 @@ import { fileURLToPath as fileURLToPath2 } from "url";
31025
31077
 
31026
31078
  // src/cleanup.ts
31027
31079
  init_esm_shims();
31028
- import { readdir as readdir3, stat as stat3, unlink as unlink3, rm, rmdir, rename, mkdir as mkdir6 } from "fs/promises";
31080
+ import { readdir as readdir3, stat as stat3, unlink as unlink4, rm, rmdir, rename, mkdir as mkdir6 } from "fs/promises";
31029
31081
  import { spawn } from "child_process";
31030
31082
  import { join as join3 } from "path";
31031
31083
 
31032
31084
  // src/internal/process-primitives.ts
31033
31085
  init_esm_shims();
31034
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, unlink as unlink2 } from "fs/promises";
31086
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, unlink as unlink3 } from "fs/promises";
31035
31087
  import { dirname as dirname5 } from "path";
31036
31088
  async function readPidfileAt(pidfilePath) {
31037
31089
  try {
@@ -31057,7 +31109,7 @@ async function writePidfileAt(pidfilePath, pid) {
31057
31109
  }
31058
31110
  async function clearPidfileAt(pidfilePath) {
31059
31111
  try {
31060
- await unlink2(pidfilePath);
31112
+ await unlink3(pidfilePath);
31061
31113
  } catch {
31062
31114
  }
31063
31115
  }
@@ -31078,7 +31130,7 @@ async function cleanupStalePidfile(pidfilePath) {
31078
31130
  if (pid === null) return false;
31079
31131
  if (isProcessAlive2(pid)) return false;
31080
31132
  try {
31081
- await unlink3(pidfilePath);
31133
+ await unlink4(pidfilePath);
31082
31134
  return true;
31083
31135
  } catch {
31084
31136
  return false;
@@ -31226,7 +31278,7 @@ async function cleanupTmpFiles(slockHome) {
31226
31278
  try {
31227
31279
  const s = await stat3(snap);
31228
31280
  if (Date.now() - s.mtimeMs > TMP_MAX_AGE_MS) {
31229
- await unlink3(snap);
31281
+ await unlink4(snap);
31230
31282
  removed.push(snap);
31231
31283
  }
31232
31284
  } catch {
@@ -31270,7 +31322,7 @@ async function runFullCleanup(slockHome, options = {}) {
31270
31322
 
31271
31323
  // src/health.ts
31272
31324
  init_esm_shims();
31273
- import { readFile as readFile6, writeFile as writeFile6, unlink as unlink4, mkdir as mkdir7, appendFile as appendFile2 } from "fs/promises";
31325
+ import { readFile as readFile6, writeFile as writeFile6, unlink as unlink5, mkdir as mkdir7, appendFile as appendFile2 } from "fs/promises";
31274
31326
  import { dirname as dirname7 } from "path";
31275
31327
  var CRASH_WINDOW_MS = 6e4;
31276
31328
  var DEGRADED_THRESHOLD = 3;
@@ -31280,7 +31332,9 @@ async function readHealthFile(slockHome, serverId) {
31280
31332
  const raw = await readFile6(serverHealthPath(slockHome, serverId), "utf8");
31281
31333
  const parsed = JSON.parse(raw);
31282
31334
  if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
31283
- return parsed;
31335
+ const file = parsed;
31336
+ if (typeof file.schemaVersion !== "number") file.schemaVersion = CURRENT_SCHEMA_VERSION;
31337
+ return file;
31284
31338
  }
31285
31339
  } catch {
31286
31340
  }
@@ -31289,7 +31343,8 @@ async function readHealthFile(slockHome, serverId) {
31289
31343
  async function writeHealthFile(slockHome, serverId, file) {
31290
31344
  const path3 = serverHealthPath(slockHome, serverId);
31291
31345
  await mkdir7(dirname7(path3), { recursive: true });
31292
- await writeFile6(path3, JSON.stringify(file), { mode: 384 });
31346
+ const stamped = { ...file, schemaVersion: CURRENT_SCHEMA_VERSION };
31347
+ await writeFile6(path3, JSON.stringify(stamped), { mode: 384 });
31293
31348
  }
31294
31349
  async function recordCrash(slockHome, serverId, exitCode, signal, nowMs = Date.now()) {
31295
31350
  if (!isValidServerId(serverId)) return;
@@ -31338,7 +31393,7 @@ async function markFatalConfig(slockHome, serverId, exitCode, signal, nowMs = Da
31338
31393
  async function resetHealth(slockHome, serverId) {
31339
31394
  if (!isValidServerId(serverId)) return;
31340
31395
  try {
31341
- await unlink4(serverHealthPath(slockHome, serverId));
31396
+ await unlink5(serverHealthPath(slockHome, serverId));
31342
31397
  } catch {
31343
31398
  }
31344
31399
  }
@@ -31507,7 +31562,7 @@ async function waitForManagedDaemonPids(slockHome, serverIds, opts) {
31507
31562
  function buildTimeoutMessage(slockHome, serverIds, ready, input) {
31508
31563
  const missing = serverIds.filter((id) => !ready.has(id));
31509
31564
  const target = input.serverId && missing.length === 1 ? `${input.serverLabel ?? input.serverId}` : `${missing.length} server runner(s): ${missing.join(", ")}`;
31510
- return `Timed out waiting for ${target} to start. Run \`slock-computer status\` and inspect ${serviceLogPath(slockHome)} plus per-server server-runner logs under ~/.slock/computer/servers/<serverId>/server-runner.log.`;
31565
+ return `Timed out waiting for ${target} to start. Run \`slock-computer status\` and inspect ${serviceLogPath(slockHome)} plus per-server runner logs under ~/.slock/computer/servers/<serverId>/runner.log.`;
31511
31566
  }
31512
31567
  async function start(input, options = {}) {
31513
31568
  options.signal?.throwIfAborted?.();
@@ -31827,7 +31882,7 @@ async function detach(input, options = {}) {
31827
31882
  // src/upgradeSea.ts
31828
31883
  init_esm_shims();
31829
31884
  import { createHash as createHash3 } from "crypto";
31830
- import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename2, rm as rm2, writeFile as writeFile8 } from "fs/promises";
31885
+ import { access as access2, chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename2, rm as rm2, writeFile as writeFile8 } from "fs/promises";
31831
31886
  import { join as join4 } from "path";
31832
31887
 
31833
31888
  // src/channel.ts
@@ -31983,6 +32038,24 @@ async function swapSeaPhase(currentBinaryPath, stagedBinaryPath) {
31983
32038
  }
31984
32039
  return { prevBinaryPath, currentBinaryPath };
31985
32040
  }
32041
+ async function rollbackSeaSwap(swap) {
32042
+ await rm2(swap.currentBinaryPath, { force: true });
32043
+ await rename2(swap.prevBinaryPath, swap.currentBinaryPath);
32044
+ await chmod4(swap.currentBinaryPath, 493);
32045
+ }
32046
+ function seaPrevBinaryPath(currentBinaryPath) {
32047
+ return `${currentBinaryPath}.prev`;
32048
+ }
32049
+ async function rollbackToPrev(currentBinaryPath) {
32050
+ const prevBinaryPath = seaPrevBinaryPath(currentBinaryPath);
32051
+ try {
32052
+ await access2(prevBinaryPath);
32053
+ } catch {
32054
+ throw new Error("UPGRADE_NO_ROLLBACK");
32055
+ }
32056
+ await rollbackSeaSwap({ prevBinaryPath, currentBinaryPath });
32057
+ return { restoredFrom: prevBinaryPath, currentBinaryPath };
32058
+ }
31986
32059
  function pendingUpgradeMarkerPath(slockHome) {
31987
32060
  return join4(slockHome, "upgrade-pending.json");
31988
32061
  }
@@ -31990,7 +32063,8 @@ async function writePendingUpgradeMarker(slockHome, marker) {
31990
32063
  const path3 = pendingUpgradeMarkerPath(slockHome);
31991
32064
  const tmp = `${path3}.tmp`;
31992
32065
  await mkdir9(slockHome, { recursive: true });
31993
- await writeFile8(tmp, `${JSON.stringify(marker, null, 2)}
32066
+ const stamped = { ...marker, schemaVersion: CURRENT_SCHEMA_VERSION };
32067
+ await writeFile8(tmp, `${JSON.stringify(stamped, null, 2)}
31994
32068
  `, "utf8");
31995
32069
  await rename2(tmp, path3);
31996
32070
  }
@@ -31999,7 +32073,8 @@ async function readPendingUpgradeMarker(slockHome) {
31999
32073
  const raw = await readFile8(pendingUpgradeMarkerPath(slockHome), "utf8");
32000
32074
  const parsed = JSON.parse(raw);
32001
32075
  if (typeof parsed.requestId === "string" && typeof parsed.targetVersion === "string" && typeof parsed.fromVersion === "string" && typeof parsed.startedAt === "string") {
32002
- return parsed;
32076
+ const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : CURRENT_SCHEMA_VERSION;
32077
+ return { ...parsed, schemaVersion };
32003
32078
  }
32004
32079
  return null;
32005
32080
  } catch {
@@ -32118,7 +32193,7 @@ async function resolveAndRunSeaUpgrade(opts) {
32118
32193
  // src/internal/ipc-server.ts
32119
32194
  init_esm_shims();
32120
32195
  import { connect, createServer } from "net";
32121
- import { chmod as chmod5, mkdir as mkdir10, stat as stat4, unlink as unlink5 } from "fs/promises";
32196
+ import { chmod as chmod5, mkdir as mkdir10, stat as stat4, unlink as unlink6 } from "fs/promises";
32122
32197
  import { dirname as dirname9 } from "path";
32123
32198
 
32124
32199
  // src/internal/ipc-codec.ts
@@ -32229,7 +32304,7 @@ async function probeAndClearStaleSocket(socketPath) {
32229
32304
  return;
32230
32305
  }
32231
32306
  try {
32232
- await unlink5(socketPath);
32307
+ await unlink6(socketPath);
32233
32308
  } catch (err) {
32234
32309
  if (isErrnoException(err) && err.code === "ENOENT") return;
32235
32310
  throw err;
@@ -32495,12 +32570,8 @@ async function buildStatusReport(installRoot) {
32495
32570
  function pad(s, n) {
32496
32571
  return s.length >= n ? s : s + " ".repeat(n - s.length);
32497
32572
  }
32498
- async function runStatus(opts) {
32573
+ async function runStatus() {
32499
32574
  const report = await buildStatusReport(resolveSlockHome());
32500
- if (opts.json) {
32501
- info(JSON.stringify(report, null, 2));
32502
- return;
32503
- }
32504
32575
  info("");
32505
32576
  info(`SLOCK_HOME: ${report.slockHome}`);
32506
32577
  const loginDetail = report.userSessionError ? "no \u2014 user session file is invalid; re-run `slock-computer login`" : report.loggedIn ? `yes (user ${report.userId ?? "?"})` : "no \u2014 run `slock-computer login`";
@@ -32625,6 +32696,7 @@ function isServiceState(value) {
32625
32696
 
32626
32697
  // src/serviceState.ts
32627
32698
  var DEFAULT_STATE = {
32699
+ schemaVersion: CURRENT_SCHEMA_VERSION,
32628
32700
  state: "running",
32629
32701
  crashHistory: []
32630
32702
  };
@@ -32634,9 +32706,10 @@ async function readServiceState(slockHome) {
32634
32706
  const parsed = JSON.parse(raw);
32635
32707
  if (!parsed || typeof parsed !== "object") return { ...DEFAULT_STATE };
32636
32708
  const obj = parsed;
32709
+ const schemaVersion = typeof obj.schemaVersion === "number" ? obj.schemaVersion : CURRENT_SCHEMA_VERSION;
32637
32710
  const state = isServiceState(obj.state) ? obj.state : "running";
32638
32711
  const crashHistory = Array.isArray(obj.crashHistory) ? obj.crashHistory.filter(isCrashEntry) : [];
32639
- return { state, crashHistory };
32712
+ return { schemaVersion, state, crashHistory };
32640
32713
  } catch {
32641
32714
  return { ...DEFAULT_STATE };
32642
32715
  }
@@ -32644,7 +32717,8 @@ async function readServiceState(slockHome) {
32644
32717
  async function writeServiceState(slockHome, file) {
32645
32718
  const path3 = serviceStatePath(slockHome);
32646
32719
  await mkdir11(dirname10(path3), { recursive: true });
32647
- await writeFile9(path3, JSON.stringify(file), { mode: 384 });
32720
+ const stamped = { ...file, schemaVersion: CURRENT_SCHEMA_VERSION };
32721
+ await writeFile9(path3, JSON.stringify(stamped), { mode: 384 });
32648
32722
  }
32649
32723
  function isCrashEntry(value) {
32650
32724
  if (!value || typeof value !== "object") return false;
@@ -32728,7 +32802,7 @@ async function resolveTargetServerId(opts) {
32728
32802
  if (attachments.length === 1) return attachments[0].serverId;
32729
32803
  fail(
32730
32804
  "AMBIGUOUS_SERVER",
32731
- `Multiple servers attached (${attachments.map(attachmentLabel).join(", ")}). Pass \`--server /<serverSlug>\` (e.g. \`--server ${attachmentLabel(attachments[0])}\`) to choose one.`
32805
+ `Multiple servers attached (${attachments.map(attachmentLabel).join(", ")}). Pass the server slug positionally (e.g. \`${attachmentLabel(attachments[0])}\`) to choose one.`
32732
32806
  );
32733
32807
  }
32734
32808
  async function resolveTargetAttachment(opts) {
@@ -33018,17 +33092,13 @@ async function runReset(opts) {
33018
33092
  (client) => client.request("reset-service", void 0),
33019
33093
  () => resetService(slockHome)
33020
33094
  );
33021
- if (opts.json) {
33022
- info(JSON.stringify(result2));
33023
- return;
33024
- }
33025
33095
  info(
33026
33096
  `Reset service crash history (previous state: ${result2.previousState}; cleared ${result2.clearedCrashCount} entr${result2.clearedCrashCount === 1 ? "y" : "ies"}).`
33027
33097
  );
33028
33098
  info("Service state transitioned to `running`. Runners were not touched.");
33029
33099
  return;
33030
33100
  }
33031
- const serverId = await resolveTargetServerId({ server: opts.server });
33101
+ const serverId = await resolveTargetServerId({ server: opts.serverSlug });
33032
33102
  const result = await resetViaServiceOrDisk(
33033
33103
  slockHome,
33034
33104
  (client) => client.request("reset-runner", { serverId }),
@@ -33040,10 +33110,6 @@ async function runReset(opts) {
33040
33110
  `Runner for server ${serverId} was not found.`
33041
33111
  );
33042
33112
  }
33043
- if (opts.json) {
33044
- info(JSON.stringify(result));
33045
- return;
33046
- }
33047
33113
  info(
33048
33114
  `Reset runner crash history for server ${result.serverId} (previous state: ${result.previousState}; cleared ${result.clearedCrashCount} entr${result.clearedCrashCount === 1 ? "y" : "ies"}).`
33049
33115
  );
@@ -33616,7 +33682,7 @@ async function runStart(opts = {}, deps = {}) {
33616
33682
  info(
33617
33683
  `Managing ${sb.managedCount} of ${sb.attachedCount} attached server(s). Logs: ${sb.logPath}`
33618
33684
  );
33619
- info(`Per-server server-runner logs: ~/.slock/computer/servers/<serverId>/server-runner.log`);
33685
+ info(`Per-server runner logs: ~/.slock/computer/servers/<serverId>/runner.log`);
33620
33686
  info(`Check state with \`slock-computer status\`.`);
33621
33687
  }
33622
33688
  }
@@ -33737,6 +33803,8 @@ async function refreshUserSession(slockHome, serverUrl) {
33737
33803
  JSON.stringify(
33738
33804
  {
33739
33805
  kind: "user-session",
33806
+ // Stamp the current on-disk schema version on every write.
33807
+ schemaVersion: CURRENT_SCHEMA_VERSION,
33740
33808
  userId: session.userId,
33741
33809
  accessToken: body.accessToken,
33742
33810
  refreshToken: body.refreshToken,
@@ -34036,7 +34104,7 @@ async function runSetup(opts, deps = {}) {
34036
34104
  serverSlug: opts.serverSlug,
34037
34105
  serverUrl: opts.serverUrl,
34038
34106
  name: opts.name,
34039
- run: false,
34107
+ start: false,
34040
34108
  orchestrated: true
34041
34109
  });
34042
34110
  }
@@ -34079,16 +34147,6 @@ async function runRunnersList(opts) {
34079
34147
  `Could not list runners on ${label} (${block.code}). Check --server-url / server version.`
34080
34148
  );
34081
34149
  }
34082
- if (opts.json) {
34083
- info(
34084
- JSON.stringify(
34085
- { server: label, serverId: block.serverId, whitelist: block.whitelist, runners: block.runners },
34086
- null,
34087
- 2
34088
- )
34089
- );
34090
- return;
34091
- }
34092
34150
  if (block.runners.length === 0) {
34093
34151
  info(`No runners on server ${label}.`);
34094
34152
  return;
@@ -34105,7 +34163,7 @@ async function runRunnersList(opts) {
34105
34163
  }
34106
34164
  async function runRunnersStop(agentId, opts = {}) {
34107
34165
  if (!agentId || agentId.trim().length === 0) {
34108
- fail("AGENT_ID_REQUIRED", "Usage: slock-computer runners stop <agentId> [--server /<serverSlug>]");
34166
+ fail("AGENT_ID_REQUIRED", "Usage: slock-computer runners stop <agentId> [serverSlug]");
34109
34167
  }
34110
34168
  const a = await resolveTargetAttachment({ server: opts.server });
34111
34169
  const client = new RunnersClient(a.serverUrl, a.apiKey);
@@ -34207,14 +34265,6 @@ async function runDoctor(opts) {
34207
34265
  if (opts.serverId) {
34208
34266
  crashes = await readCrashHistory(slockHome, opts.serverId);
34209
34267
  }
34210
- if (opts.json) {
34211
- const payload = { ok: allOk, checks };
34212
- if (cleanupReport) payload.cleanup = cleanupReport;
34213
- if (crashes.length > 0) payload.crashes = crashes;
34214
- info(redactSecrets(JSON.stringify(payload, null, 2)));
34215
- process.exitCode = allOk ? 0 : 1;
34216
- return;
34217
- }
34218
34268
  info("");
34219
34269
  for (const c of checks) {
34220
34270
  info(redactSecrets(`${c.ok ? "\u2713" : "\u2717"} ${c.name.padEnd(48)} ${c.detail}`));
@@ -34230,7 +34280,7 @@ async function runDoctor(opts) {
34230
34280
  info(` - ${c.at}${code}${sig}`);
34231
34281
  }
34232
34282
  info(` \u2192 Once you fix the underlying issue, run`);
34233
- info(` \`slock-computer reset --runner --server ${opts.serverLabel ?? opts.serverId}\` to resume auto-restart.`);
34283
+ info(` \`slock-computer reset --runner ${opts.serverLabel ?? opts.serverId}\` to resume auto-restart.`);
34234
34284
  }
34235
34285
  if (cleanupReport) {
34236
34286
  info("");
@@ -34421,6 +34471,32 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
34421
34471
  "`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`."
34422
34472
  );
34423
34473
  }
34474
+ if (opts.rollback) {
34475
+ if (opts.targetVersion !== void 0 || opts.channel !== void 0 || opts.dryRun) {
34476
+ fail(
34477
+ "UPGRADE_ROLLBACK_CONFLICT",
34478
+ "`--rollback` cannot be combined with --target-version, --channel, or --dry-run. Rollback restores the previous binary and resolves/downloads nothing."
34479
+ );
34480
+ }
34481
+ const currentBinaryPath2 = (deps.currentBinaryPath ?? (() => process.execPath))();
34482
+ const rollbackFn = deps.rollbackToPrevFn ?? rollbackToPrev;
34483
+ try {
34484
+ const r = await rollbackFn(currentBinaryPath2);
34485
+ info(
34486
+ `Rolled back: restored the previous binary from ${r.restoredFrom}. Restart the Computer service to run the restored binary (the managed supervisor does this automatically).`
34487
+ );
34488
+ } catch (e) {
34489
+ const msg = e instanceof Error ? e.message : String(e);
34490
+ if (/UPGRADE_NO_ROLLBACK/.test(msg)) {
34491
+ fail(
34492
+ "UPGRADE_NO_ROLLBACK",
34493
+ "Nothing to roll back: no previous binary (`<exe>.prev`) exists. A rollback target is only kept after a successful upgrade swap."
34494
+ );
34495
+ }
34496
+ fail("UPGRADE_SWAP_FAILED", `Rollback failed: ${msg}.`);
34497
+ }
34498
+ return;
34499
+ }
34424
34500
  let channel2;
34425
34501
  if (opts.channel !== void 0) {
34426
34502
  const parsed = parseChannel(opts.channel);
@@ -34594,9 +34670,12 @@ program2.name("slock-computer").description("Slock Computer \u2014 local-machine
34594
34670
  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) => {
34595
34671
  await runLogin({ serverUrl: opts.serverUrl });
34596
34672
  }));
34597
- 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) => {
34673
+ program2.command("logout").description("Log out (clear the saved user session for this Computer); per-server attachments are untouched.").action(withCliExit(async () => {
34674
+ await runLogout();
34675
+ }));
34676
+ 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-start", "only authorize + write local state; do not start the service").option("--foreground", FOREGROUND_DESC).action(withCliExit(async (serverSlug, opts) => {
34598
34677
  await withMutationLock(
34599
- () => runAttach({ serverSlug, serverUrl: opts.serverUrl, name: opts.name, run: opts.run, foreground: opts.foreground })
34678
+ () => runAttach({ serverSlug, serverUrl: opts.serverUrl, name: opts.name, start: opts.start, foreground: opts.foreground })
34600
34679
  );
34601
34680
  }));
34602
34681
  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(
@@ -34616,10 +34695,10 @@ program2.command("setup").argument("<serverSlug>", SERVER_SLUG_TARGET_DESC).desc
34616
34695
  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) => {
34617
34696
  await withMutationLock(async () => runDetach(await resolveTargetServerId({ server: serverSlug }), serverSlug));
34618
34697
  }));
34619
- 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) => {
34620
- await runStatus({ json: opts.json });
34698
+ program2.command("status").description("Show this Computer's aggregate state (login + service + per-server runners).").action(withCliExit(async () => {
34699
+ await runStatus();
34621
34700
  }));
34622
- 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) => {
34701
+ program2.command("start").argument("[serverSlug]", SERVER_SLUG_OPTIONAL_DESC).description("Start/ensure the Computer service (manages all per-server runners).").option("--foreground", FOREGROUND_DESC).action(withCliExit(async (serverSlug, opts) => {
34623
34702
  await withMutationLock(
34624
34703
  async () => runStart({
34625
34704
  foreground: opts.foreground,
@@ -34628,10 +34707,10 @@ program2.command("start").argument("[serverSlug]", SERVER_SLUG_OPTIONAL_DESC).de
34628
34707
  })
34629
34708
  );
34630
34709
  }));
34631
- program2.command("stop").description("Stop the Computer service (and all managed per-server server-runners).").action(withCliExit(async () => {
34710
+ program2.command("stop").description("Stop the Computer service (and all managed per-server runners).").action(withCliExit(async () => {
34632
34711
  await withMutationLock(() => runStop());
34633
34712
  }));
34634
- 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) => {
34713
+ program2.command("restart").argument("[serverSlug]", SERVER_SLUG_OPTIONAL_DESC).description("Restart the Computer service (stop + start; all managed per-server runners).").option("--foreground", FOREGROUND_DESC).action(withCliExit(async (serverSlug, opts) => {
34635
34714
  await withMutationLock(async () => {
34636
34715
  await runStop();
34637
34716
  await runStart({
@@ -34641,12 +34720,11 @@ program2.command("restart").argument("[serverSlug]", SERVER_SLUG_OPTIONAL_DESC).
34641
34720
  });
34642
34721
  });
34643
34722
  }));
34644
- 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(
34723
+ 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("--fix", "after diagnosis, run the local residue cleanup pass").action(
34645
34724
  withCliExit(
34646
34725
  async (serverSlug, opts) => {
34647
34726
  const serverId = serverSlug ? await resolveTargetServerId({ server: serverSlug }) : void 0;
34648
34727
  await runDoctor({
34649
- json: opts.json,
34650
34728
  cleanup: opts.fix,
34651
34729
  serverId,
34652
34730
  serverLabel: serverSlug
@@ -34654,27 +34732,26 @@ program2.command("doctor").argument("[serverSlug]", `${SERVER_SLUG_OPTIONAL_DESC
34654
34732
  }
34655
34733
  )
34656
34734
  );
34657
- program2.command("reset").description("Clear a degraded state and resume the service's auto-restart loop.").option("--service", "clear the service-level crash history (cascade record)").option("--runner", "clear a runner's crash history (selected by `--server`)").option("--server <slug>", "with `--runner`, select target server slug (required when \u22652 attached)").option("--json", "emit the machine-readable result").action(
34658
- withCliExit(async (opts) => {
34735
+ program2.command("reset").argument("[serverSlug]", `${SERVER_SLUG_OPTIONAL_DESC} (with \`--runner\`, selects the target runner)`).description("Clear a degraded state and resume the service's auto-restart loop.").option("--service", "clear the service-level crash history (cascade record)").option("--runner", "clear a runner's crash history (selected by the [serverSlug] positional)").action(
34736
+ withCliExit(async (serverSlug, opts) => {
34659
34737
  await withMutationLock(
34660
34738
  () => runReset({
34661
34739
  service: opts.service,
34662
34740
  runner: opts.runner,
34663
- server: opts.server,
34664
- json: opts.json
34741
+ serverSlug
34665
34742
  })
34666
34743
  );
34667
34744
  })
34668
34745
  );
34669
- 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) => {
34746
+ program2.command("logs").argument("[serverSlug]", `${SERVER_SLUG_OPTIONAL_DESC} (required when \u22652 attached; ignored with --service)`).description("Tail one server's 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 runner log").action(withCliExit(async (serverSlug, opts) => {
34670
34747
  await runLogs({ lines: opts.lines, server: serverSlug ?? null, service: !!opts.service });
34671
34748
  }));
34672
34749
  var runners = program2.command("runners").description("Computer runner control plane (per-server scoped; \xA712 whitelist server-side).");
34673
- 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) => {
34674
- await runRunnersList({ json: opts.json, server: opts.server ?? null });
34750
+ runners.command("list").argument("[serverSlug]", `${SERVER_SLUG_OPTIONAL_DESC} (required when \u22652 attached)`).description("List runners on one attached server.").action(withCliExit(async (serverSlug) => {
34751
+ await runRunnersList({ server: serverSlug ?? null });
34675
34752
  }));
34676
- runners.command("stop").argument("<agentId>", "id of the runner to stop").option("--server <slug>", "select target server slug (required when \u22652 attached)").description("Stop a runner (server-mediated; reuses the orchestrator's agent stop).").action(withCliExit(async (agentId, opts) => {
34677
- await withMutationLock(() => runRunnersStop(agentId, { server: opts.server ?? null }));
34753
+ runners.command("stop").argument("<agentId>", "id of the runner to stop").argument("[serverSlug]", `${SERVER_SLUG_OPTIONAL_DESC} (required when \u22652 attached)`).description("Stop a runner (server-mediated; reuses the orchestrator's agent stop).").action(withCliExit(async (agentId, serverSlug) => {
34754
+ await withMutationLock(() => runRunnersStop(agentId, { server: serverSlug ?? null }));
34678
34755
  }));
34679
34756
  var channel = program2.command("channel").description("Show or set the Computer release channel (latest | alpha | pinned:<semver>).");
34680
34757
  channel.command("show").description("Show the current Computer release channel (default `latest` when unset).").action(
@@ -34696,12 +34773,15 @@ program2.command("upgrade").description(
34696
34773
  "supervisor restarts on the swapped binary. SEA-only: a non-SEA (npm/dev)",
34697
34774
  "run has no single binary to self-swap and is refused (UPGRADE_SEA_ONLY)."
34698
34775
  ].join("\n")
34699
- ).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(
34776
+ ).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)").option(
34777
+ "--rollback",
34778
+ "restore the previous binary (<exe>.prev) from the last successful upgrade; mutually exclusive with --target-version/--channel/--dry-run"
34779
+ ).action(
34700
34780
  withCliExit(
34701
34781
  async (opts) => {
34702
34782
  const slockHome = resolveSlockHome();
34703
34783
  const trigger = resolveUpgradeTrigger(process.env.SLOCK_UPGRADE_TRIGGER);
34704
- if (!opts.dryRun && !opts.channel) {
34784
+ if (!opts.dryRun && !opts.channel && !opts.rollback) {
34705
34785
  let client = null;
34706
34786
  try {
34707
34787
  client = await connectService(slockHome);
@@ -34732,6 +34812,7 @@ program2.command("upgrade").description(
34732
34812
  dryRun: opts.dryRun,
34733
34813
  channel: opts.channel,
34734
34814
  targetVersion: opts.targetVersion,
34815
+ rollback: opts.rollback,
34735
34816
  trigger
34736
34817
  })
34737
34818
  );
package/dist/lib/index.js CHANGED
@@ -333,6 +333,7 @@ import { constants as fsConstants } from "fs";
333
333
  import { createHash } from "crypto";
334
334
  import os from "os";
335
335
  import path from "path";
336
+ var CURRENT_SCHEMA_VERSION = 1;
336
337
  function computerDir(slockHome) {
337
338
  return path.join(slockHome, "computer");
338
339
  }
@@ -358,11 +359,14 @@ function serverDir(slockHome, serverId) {
358
359
  function serverAttachmentPath(slockHome, serverId) {
359
360
  return path.join(serverDir(slockHome, serverId), "runner.state.json");
360
361
  }
362
+ function legacyServerAttachmentPath(slockHome, serverId) {
363
+ return path.join(serverDir(slockHome, serverId), "attachment.json");
364
+ }
361
365
  function serverRunnerPidPath(slockHome, serverId) {
362
- return path.join(serverDir(slockHome, serverId), "server-runner.pid");
366
+ return path.join(serverDir(slockHome, serverId), "runner.pid");
363
367
  }
364
368
  function serverRunnerLogPath(slockHome, serverId) {
365
- return path.join(serverDir(slockHome, serverId), "server-runner.log");
369
+ return path.join(serverDir(slockHome, serverId), "runner.log");
366
370
  }
367
371
  function serverHealthPath(slockHome, serverId) {
368
372
  return path.join(serverDir(slockHome, serverId), "health.json");
@@ -385,8 +389,10 @@ function parseAttachment(raw) {
385
389
  try {
386
390
  const a = JSON.parse(raw);
387
391
  if (a.kind === "computer-attachment" && typeof a.serverId === "string" && typeof a.serverMachineId === "string" && typeof a.apiKey === "string" && a.apiKey.length > 0 && typeof a.serverUrl === "string") {
392
+ const schemaVersion = typeof a.schemaVersion === "number" ? a.schemaVersion : CURRENT_SCHEMA_VERSION;
388
393
  return {
389
394
  kind: "computer-attachment",
395
+ schemaVersion,
390
396
  serverId: a.serverId,
391
397
  serverSlug: typeof a.serverSlug === "string" && a.serverSlug.length > 0 ? a.serverSlug : void 0,
392
398
  serverMachineId: a.serverMachineId,
@@ -410,7 +416,44 @@ async function readAttachmentAt(path2) {
410
416
  }
411
417
  async function readServerAttachment(slockHome, serverId) {
412
418
  if (!isValidServerId(serverId)) return null;
413
- return readAttachmentAt(serverAttachmentPath(slockHome, serverId));
419
+ const current = await readAttachmentAt(serverAttachmentPath(slockHome, serverId));
420
+ const legacy = await readAttachmentAt(legacyServerAttachmentPath(slockHome, serverId));
421
+ let effective;
422
+ let decidedByLegacy;
423
+ if (!current) {
424
+ effective = legacy;
425
+ decidedByLegacy = legacy !== null;
426
+ } else if (!legacy) {
427
+ effective = current;
428
+ decidedByLegacy = false;
429
+ } else if (
430
+ // Adopted-machine-still-wins: when the legacy attachment is an adopted
431
+ // (working) machine and the current one is a different, not-adopted machine
432
+ // (e.g. a fresh attach that the server rejects), prefer the legacy creds.
433
+ legacy.adoptedFromLegacy === true && current.adoptedFromLegacy !== true && legacy.serverMachineId !== current.serverMachineId
434
+ ) {
435
+ effective = legacy;
436
+ decidedByLegacy = true;
437
+ } else {
438
+ effective = current;
439
+ decidedByLegacy = false;
440
+ }
441
+ if (decidedByLegacy && effective) {
442
+ try {
443
+ await writeServerAttachment(slockHome, effective);
444
+ await unlink(legacyServerAttachmentPath(slockHome, serverId));
445
+ } catch {
446
+ }
447
+ }
448
+ return effective;
449
+ }
450
+ async function writeServerAttachment(slockHome, attachment) {
451
+ if (!isValidServerId(attachment.serverId)) return;
452
+ const path2 = serverAttachmentPath(slockHome, attachment.serverId);
453
+ await mkdir(dirname(path2), { recursive: true });
454
+ const stamped = { ...attachment, schemaVersion: CURRENT_SCHEMA_VERSION };
455
+ await writeFile(path2, JSON.stringify(stamped, null, 2), { mode: 384 });
456
+ await chmod(path2, 384);
414
457
  }
415
458
  async function listAttachedServerIds(slockHome) {
416
459
  let entries;
@@ -440,6 +483,9 @@ async function listServerAttachments(slockHome) {
440
483
  import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
441
484
  import { dirname as dirname2 } from "path";
442
485
 
486
+ // src/login.ts
487
+ import { unlink as unlink2 } from "fs/promises";
488
+
443
489
  // src/services/login.ts
444
490
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
445
491
  import { dirname as dirname3 } from "path";
@@ -475,12 +521,12 @@ import { dirname as dirname10, join as joinPath } from "path";
475
521
  import { fileURLToPath } from "url";
476
522
 
477
523
  // src/cleanup.ts
478
- import { readdir as readdir3, stat as stat3, unlink as unlink3, rm, rmdir, rename, mkdir as mkdir6 } from "fs/promises";
524
+ import { readdir as readdir3, stat as stat3, unlink as unlink4, rm, rmdir, rename, mkdir as mkdir6 } from "fs/promises";
479
525
  import { spawn } from "child_process";
480
526
  import { join as join3 } from "path";
481
527
 
482
528
  // src/internal/process-primitives.ts
483
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, unlink as unlink2 } from "fs/promises";
529
+ import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, unlink as unlink3 } from "fs/promises";
484
530
  import { dirname as dirname5 } from "path";
485
531
  async function readPidfileAt(pidfilePath) {
486
532
  try {
@@ -505,7 +551,7 @@ function isProcessAlive(pid) {
505
551
  var TMP_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
506
552
 
507
553
  // src/health.ts
508
- import { readFile as readFile6, writeFile as writeFile6, unlink as unlink4, mkdir as mkdir7, appendFile as appendFile2 } from "fs/promises";
554
+ import { readFile as readFile6, writeFile as writeFile6, unlink as unlink5, mkdir as mkdir7, appendFile as appendFile2 } from "fs/promises";
509
555
  import { dirname as dirname6 } from "path";
510
556
  var CRASH_WINDOW_MS = 6e4;
511
557
  var DEGRADED_THRESHOLD = 3;
@@ -515,7 +561,9 @@ async function readHealthFile(slockHome, serverId) {
515
561
  const raw = await readFile6(serverHealthPath(slockHome, serverId), "utf8");
516
562
  const parsed = JSON.parse(raw);
517
563
  if (parsed && typeof parsed === "object" && Array.isArray(parsed.crashes)) {
518
- return parsed;
564
+ const file = parsed;
565
+ if (typeof file.schemaVersion !== "number") file.schemaVersion = CURRENT_SCHEMA_VERSION;
566
+ return file;
519
567
  }
520
568
  } catch {
521
569
  }
@@ -572,7 +620,7 @@ import { fetch as fetch3 } from "undici";
572
620
 
573
621
  // src/upgradeSea.ts
574
622
  import { createHash as createHash3 } from "crypto";
575
- import { chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename2, rm as rm2, writeFile as writeFile8 } from "fs/promises";
623
+ import { access as access2, chmod as chmod4, mkdir as mkdir9, readFile as readFile8, rename as rename2, rm as rm2, writeFile as writeFile8 } from "fs/promises";
576
624
  import { join as join4 } from "path";
577
625
 
578
626
  // src/channel.ts
@@ -581,7 +629,7 @@ import { dirname as dirname7 } from "path";
581
629
 
582
630
  // src/internal/ipc-server.ts
583
631
  import { connect, createServer } from "net";
584
- import { chmod as chmod5, mkdir as mkdir10, stat as stat4, unlink as unlink5 } from "fs/promises";
632
+ import { chmod as chmod5, mkdir as mkdir10, stat as stat4, unlink as unlink6 } from "fs/promises";
585
633
  import { dirname as dirname8 } from "path";
586
634
 
587
635
  // src/internal/ipc-codec.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-computer",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "Canonical Raft Computer — standalone human/local-machine control-plane CLI (login + attach). Provides raft-computer plus the legacy slock-computer alias; distinct from the agent-facing @botiverse/raft CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "commander": "^12.1.0",
32
32
  "proper-lockfile": "^4.1.2",
33
33
  "undici": "^7.24.7",
34
- "@botiverse/raft-daemon": "0.62.0"
34
+ "@botiverse/raft-daemon": "0.63.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.5.0",