@slock-ai/computer 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +218 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -314,9 +314,16 @@ function formatUpgradeLogTimestamp(date = /* @__PURE__ */ new Date()) {
314
314
 
315
315
  // src/output.ts
316
316
  var CliExit = class extends Error {
317
- constructor(exitCode) {
318
- super(`CliExit(${exitCode})`);
317
+ /**
318
+ * v8.3.3 PR-2c — carry the closed-set / stderr token through the thrown
319
+ * error so callers can pattern-match without parsing stderr. Optional
320
+ * because some callsites throw `new CliExit(code)` directly without a
321
+ * named token (e.g. the harness EX_CONFIG paths).
322
+ */
323
+ constructor(exitCode, code) {
324
+ super(`CliExit(${exitCode}${code ? ` ${code}` : ""})`);
319
325
  this.exitCode = exitCode;
326
+ this.code = code;
320
327
  this.name = "CliExit";
321
328
  }
322
329
  };
@@ -327,7 +334,7 @@ function info(line) {
327
334
  function fail(code, message, exitCode = 1) {
328
335
  process.stderr.write(`${JSON.stringify({ ok: false, code, message })}
329
336
  `);
330
- throw new CliExit(exitCode);
337
+ throw new CliExit(exitCode, code);
331
338
  }
332
339
 
333
340
  // src/serverUrl.ts
@@ -351,7 +358,11 @@ async function runLogin(opts) {
351
358
  try {
352
359
  grant = await client.authorize("slock-computer");
353
360
  } catch (err) {
354
- fail("DEVICE_AUTHORIZE_FAILED", err instanceof Error ? err.message : String(err));
361
+ const reason = err instanceof Error ? err.message : String(err);
362
+ fail(
363
+ "DEVICE_AUTHORIZE_FAILED",
364
+ `Could not start device login at ${baseUrl}: ${reason}. Check that the server URL is correct and reachable.`
365
+ );
355
366
  }
356
367
  const verificationUri = grant.verificationUriComplete || grant.verificationUri;
357
368
  const verifyUrl = new URL(verificationUri, baseUrl).toString();
@@ -1692,6 +1703,49 @@ async function runStart(opts = {}, deps = {}) {
1692
1703
  info(`Per-server daemon logs: ~/.slock/computer/servers/<serverId>/daemon.log`);
1693
1704
  info(`Check state with \`slock-computer status\`.`);
1694
1705
  }
1706
+ async function runStop(deps = {}) {
1707
+ const slockHome = resolveSlockHome();
1708
+ const readPidfile = deps.readPidfile ?? readPidfileAt;
1709
+ const isAlive = deps.isProcessAlive ?? isProcessAlive2;
1710
+ const killer = deps.killSupervisor ?? ((pid2) => {
1711
+ process.kill(pid2, "SIGTERM");
1712
+ });
1713
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1714
+ const pollIntervalMs = deps.pollIntervalMs ?? 200;
1715
+ const timeoutMs = deps.timeoutMs ?? 5e3;
1716
+ const pidfilePath = supervisorPidPath(slockHome);
1717
+ const pid = await readPidfile(pidfilePath);
1718
+ if (pid === null) {
1719
+ info("Supervisor not running.");
1720
+ return;
1721
+ }
1722
+ if (!isAlive(pid)) {
1723
+ await clearPidfileAt(pidfilePath);
1724
+ info(`Supervisor not running (cleared stale pidfile for pid ${pid}).`);
1725
+ return;
1726
+ }
1727
+ try {
1728
+ killer(pid);
1729
+ } catch (err) {
1730
+ fail(
1731
+ "STOP_SIGNAL_FAILED",
1732
+ `Failed to send SIGTERM to supervisor (pid ${pid}): ${err instanceof Error ? err.message : String(err)}. Check process permissions or run: kill ${pid}`
1733
+ );
1734
+ }
1735
+ const deadline = Date.now() + timeoutMs;
1736
+ while (Date.now() < deadline) {
1737
+ if (!isAlive(pid)) {
1738
+ await clearPidfileAt(pidfilePath);
1739
+ info(`Stopped supervisor (pid ${pid}).`);
1740
+ return;
1741
+ }
1742
+ await sleep2(pollIntervalMs);
1743
+ }
1744
+ fail(
1745
+ "STOP_TIMEOUT",
1746
+ `Supervisor (pid ${pid}) did not exit within ${timeoutMs}ms after SIGTERM. Force-kill with: kill -9 ${pid}`
1747
+ );
1748
+ }
1695
1749
  async function runDetach(serverId, serverLabel = serverId) {
1696
1750
  assertValidServerId(serverId);
1697
1751
  const slockHome = resolveSlockHome();
@@ -3353,7 +3407,49 @@ async function locateStagedTarball(stagedPath) {
3353
3407
  // src/upgradeLog.ts
3354
3408
  import { chmod as chmod5, mkdir as mkdir12, open as open2 } from "fs/promises";
3355
3409
  var FILE_MODE = 384;
3410
+ var UPGRADE_ERROR_CODES = [
3411
+ "UPGRADE_DEPS_CHANGED",
3412
+ "UPGRADE_NETWORK_FAILED",
3413
+ "UPGRADE_INTEGRITY_FAILED",
3414
+ "UPGRADE_SWAP_FAILED",
3415
+ "UPGRADE_RESTART_FAILED",
3416
+ "UPGRADE_NO_TARGET",
3417
+ "UPGRADE_ALREADY_RUNNING"
3418
+ ];
3419
+ var UPGRADE_ERROR_CODE_SET = new Set(UPGRADE_ERROR_CODES);
3420
+ function assertUpgradeLogEntry(entry) {
3421
+ const e = entry;
3422
+ if (e.outcome === "ok") {
3423
+ if (e.errorCode !== void 0) {
3424
+ throw new TypeError(
3425
+ `UpgradeLogEntry contract violation: outcome="ok" must not carry errorCode (got "${e.errorCode}").`
3426
+ );
3427
+ }
3428
+ } else if (e.outcome === "err") {
3429
+ if (e.errorCode === void 0) {
3430
+ throw new TypeError(
3431
+ `UpgradeLogEntry contract violation: outcome="err" requires errorCode from the v8.3.3 closed-set (${UPGRADE_ERROR_CODES.join(" | ")}).`
3432
+ );
3433
+ }
3434
+ if (!UPGRADE_ERROR_CODE_SET.has(e.errorCode)) {
3435
+ throw new TypeError(
3436
+ `UpgradeLogEntry contract violation: errorCode "${e.errorCode}" is not in the v8.3.3 closed 7-value set (${UPGRADE_ERROR_CODES.join(" | ")}).`
3437
+ );
3438
+ }
3439
+ } else {
3440
+ throw new TypeError(
3441
+ `UpgradeLogEntry contract violation: outcome must be "ok" or "err" (got "${e.outcome}").`
3442
+ );
3443
+ }
3444
+ if (typeof e.fromBundle?.computerVersion !== "string" || e.fromBundle.computerVersion.length === 0) {
3445
+ throw new TypeError(`UpgradeLogEntry contract violation: fromBundle.computerVersion must be a non-empty string.`);
3446
+ }
3447
+ if (typeof e.toBundle?.computerVersion !== "string" || e.toBundle.computerVersion.length === 0) {
3448
+ throw new TypeError(`UpgradeLogEntry contract violation: toBundle.computerVersion must be a non-empty string.`);
3449
+ }
3450
+ }
3356
3451
  async function appendUpgradeLogEntry(slockHome, entry) {
3452
+ assertUpgradeLogEntry(entry);
3357
3453
  await mkdir12(computerDir(slockHome), { recursive: true });
3358
3454
  const path2 = upgradeLogPath(slockHome);
3359
3455
  const at = entry.at ?? formatUpgradeLogTimestamp();
@@ -3371,6 +3467,22 @@ async function appendUpgradeLogEntry(slockHome, entry) {
3371
3467
  }
3372
3468
 
3373
3469
  // src/upgradeCli.ts
3470
+ function isEphemeralNpxContext(binaryDir) {
3471
+ return binaryDir.split(/[\\/]/).includes("_npx");
3472
+ }
3473
+ async function readBundledDaemonVersion(binaryDir) {
3474
+ try {
3475
+ const pkgPath = join7(binaryDir, "package.json");
3476
+ const raw = await readFile12(pkgPath, "utf8");
3477
+ const parsed = JSON.parse(raw);
3478
+ const pinned = parsed.dependencies?.["@slock-ai/daemon"];
3479
+ if (typeof pinned !== "string" || pinned.length === 0) return null;
3480
+ if (pinned === "workspace:*") return null;
3481
+ return pinned;
3482
+ } catch {
3483
+ return null;
3484
+ }
3485
+ }
3374
3486
  async function defaultSpawnFreshSupervisor(slockHome) {
3375
3487
  await spawnDetachedSupervisor(slockHome);
3376
3488
  }
@@ -3420,8 +3532,24 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
3420
3532
  }
3421
3533
  const fromVersion = await (deps.currentVersion ?? defaultCurrentVersion)();
3422
3534
  const currentBinaryDir = (deps.currentBinaryDir ?? defaultCurrentBinaryDir)();
3535
+ const isEphemeralFn = deps.isEphemeralNpxContext ?? isEphemeralNpxContext;
3536
+ if (isEphemeralFn(currentBinaryDir)) {
3537
+ fail(
3538
+ "UPGRADE_EPHEMERAL_CONTEXT",
3539
+ `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\`.`
3540
+ );
3541
+ }
3423
3542
  if (targetVersion === fromVersion) {
3424
- info(`Already at ${fromVersion} (channel ${channel2}). Nothing to do.`);
3543
+ info(`Already at ${fromVersion} (channel ${channel2}). No upgrade target.`);
3544
+ await appendUpgradeLogEntry(slockHome, {
3545
+ fromBundle: { computerVersion: fromVersion },
3546
+ toBundle: { computerVersion: fromVersion },
3547
+ channel: channel2,
3548
+ trigger: opts.trigger ?? "cli",
3549
+ outcome: "err",
3550
+ errorCode: "UPGRADE_NO_TARGET"
3551
+ }).catch(() => {
3552
+ });
3425
3553
  return;
3426
3554
  }
3427
3555
  info(`Planning upgrade: ${fromVersion} \u2192 ${targetVersion} (channel ${channel2}).`);
@@ -3472,9 +3600,9 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
3472
3600
  spawnFreshSupervisor: () => spawnFreshSupervisor(slockHome)
3473
3601
  }
3474
3602
  });
3475
- const bundle = (version) => ({
3476
- computerVersion: version
3477
- });
3603
+ const readBundledFn = deps.readBundledDaemonVersion ?? readBundledDaemonVersion;
3604
+ const bundledDaemonVersion = await readBundledFn(currentBinaryDir);
3605
+ const bundle = (version) => bundledDaemonVersion !== null ? { computerVersion: version, bundledDaemonVersion } : { computerVersion: version };
3478
3606
  const logTrigger = opts.trigger ?? "cli";
3479
3607
  if (outcome.ok) {
3480
3608
  info(
@@ -3490,8 +3618,28 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
3490
3618
  });
3491
3619
  return;
3492
3620
  }
3493
- const code = mapFailurePhaseToCode(outcome);
3494
3621
  const rolledBack = outcome.rolledBack === true ? " (swap rolled back)" : "";
3622
+ if (outcome.phase === "drain") {
3623
+ info(
3624
+ `Upgrade deferred: --drain=defer and daemons are busy. Re-run when idle, or use --force to interrupt in-flight turns.`
3625
+ );
3626
+ return;
3627
+ }
3628
+ if (outcome.phase === "cleanup") {
3629
+ info(
3630
+ `Upgrade ${fromVersion} \u2192 ${targetVersion} succeeded; cleanup phase reported a non-fatal issue: ${outcome.reason ?? "unknown"}. Active layout + supervisor are healthy.`
3631
+ );
3632
+ await appendUpgradeLogEntry(slockHome, {
3633
+ fromBundle: bundle(fromVersion),
3634
+ toBundle: bundle(targetVersion),
3635
+ channel: channel2,
3636
+ trigger: logTrigger,
3637
+ outcome: "ok"
3638
+ }).catch(() => {
3639
+ });
3640
+ return;
3641
+ }
3642
+ const code = mapFailurePhaseToCode(outcome);
3495
3643
  await appendUpgradeLogEntry(slockHome, {
3496
3644
  fromBundle: bundle(fromVersion),
3497
3645
  toBundle: bundle(targetVersion),
@@ -3508,26 +3656,27 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
3508
3656
  }
3509
3657
  function mapFailurePhaseToCode(outcome) {
3510
3658
  switch (outcome.phase) {
3511
- case "drain":
3512
- return "UPGRADE_DEFERRED";
3513
3659
  case "stage":
3514
- return "UPGRADE_DOWNLOAD_FAILED";
3660
+ return "UPGRADE_NETWORK_FAILED";
3515
3661
  case "verify":
3516
- return "UPGRADE_VERIFY_FAILED";
3662
+ return "UPGRADE_INTEGRITY_FAILED";
3517
3663
  case "preflight":
3518
3664
  return "UPGRADE_DEPS_CHANGED";
3519
3665
  case "snapshot":
3520
- return "UPGRADE_SNAPSHOT_FAILED";
3666
+ return "UPGRADE_SWAP_FAILED";
3521
3667
  case "extract":
3522
- return "UPGRADE_EXTRACT_FAILED";
3668
+ return "UPGRADE_INTEGRITY_FAILED";
3523
3669
  case "swap":
3524
3670
  return "UPGRADE_SWAP_FAILED";
3525
3671
  case "restart":
3526
3672
  return "UPGRADE_RESTART_FAILED";
3527
3673
  case "rolling_health":
3528
- return "UPGRADE_DAEMON_HEALTH_FAILED";
3674
+ return "UPGRADE_RESTART_FAILED";
3675
+ case "drain":
3529
3676
  case "cleanup":
3530
- return "UPGRADE_CLEANUP_FAILED";
3677
+ throw new Error(
3678
+ `mapFailurePhaseToCode: phase=${outcome.phase} should be handled by caller (silenced), not mapped to a closed-set code`
3679
+ );
3531
3680
  }
3532
3681
  }
3533
3682
  async function defaultFetchDistTags() {
@@ -3888,6 +4037,19 @@ async function defaultCurrentVersionLocal() {
3888
4037
  );
3889
4038
  }
3890
4039
 
4040
+ // src/version.ts
4041
+ import { createRequire as createRequire2 } from "module";
4042
+ function readComputerVersion(moduleUrl = import.meta.url) {
4043
+ try {
4044
+ const require2 = createRequire2(moduleUrl);
4045
+ const pkg = require2("../package.json");
4046
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "0.0.0-dev";
4047
+ } catch {
4048
+ return "0.0.0-dev";
4049
+ }
4050
+ }
4051
+ var COMPUTER_VERSION = readComputerVersion();
4052
+
3891
4053
  // src/index.ts
3892
4054
  function withCliExit(fn) {
3893
4055
  return async (...args) => {
@@ -3903,7 +4065,7 @@ function withCliExit(fn) {
3903
4065
  };
3904
4066
  }
3905
4067
  var program = new Command();
3906
- program.name("slock-computer").description("Slock Computer \u2014 local-machine control plane (login + N per-server attachments).").version("0.0.3");
4068
+ program.name("slock-computer").description("Slock Computer \u2014 local-machine control plane (login + N per-server attachments).").version(COMPUTER_VERSION);
3907
4069
  program.command("login").description("Log in via device-code (one user identity per Computer / SLOCK_HOME).").option("--server-url <url>", `Slock API base URL; defaults to SLOCK_SERVER_URL or ${DEFAULT_SLOCK_SERVER_URL}`).action(withCliExit(async (opts) => {
3908
4070
  await runLogin({ serverUrl: opts.serverUrl });
3909
4071
  }));
@@ -3961,6 +4123,9 @@ program.command("start").argument("[serverSlug]", "optional: verify this server
3961
4123
  })
3962
4124
  );
3963
4125
  }));
4126
+ program.command("stop").description("Stop the Computer supervisor (and all managed per-server daemons).").action(withCliExit(async () => {
4127
+ await withMutationLock(() => runStop());
4128
+ }));
3964
4129
  program.command("doctor").argument("[serverSlug]", "optional: scope detail (recent crashes) to one server").description("Diagnose login + per-server attachments + per-server preflight (no secrets).").option("--json", "emit the machine-readable report").option("--cleanup", "after diagnosis, run the local residue cleanup pass").option("--fix", "alias for --cleanup (same behavior)").option("--reset-health", "clear <serverSlug>'s crash history so supervisor resumes auto-restart").action(
3965
4130
  withCliExit(
3966
4131
  async (serverSlug, opts) => {
@@ -4020,15 +4185,41 @@ program.command("upgrade").description(
4020
4185
  }
4021
4186
  drain = opts.drain;
4022
4187
  }
4023
- await withMutationLock(
4024
- () => runUpgradeCli(resolveSlockHome(), {
4025
- dryRun: opts.dryRun,
4026
- channel: opts.channel,
4027
- targetVersion: opts.targetVersion,
4028
- drain,
4029
- force: opts.force
4030
- })
4031
- );
4188
+ const slockHome = resolveSlockHome();
4189
+ try {
4190
+ await withMutationLock(
4191
+ () => runUpgradeCli(slockHome, {
4192
+ dryRun: opts.dryRun,
4193
+ channel: opts.channel,
4194
+ targetVersion: opts.targetVersion,
4195
+ drain,
4196
+ force: opts.force
4197
+ })
4198
+ );
4199
+ } catch (err) {
4200
+ if (err instanceof CliExit && err.code === "CONCURRENT_OPERATION") {
4201
+ try {
4202
+ const currentVersion = await defaultCurrentVersion();
4203
+ const channel2 = opts.channel ?? await readChannel(slockHome);
4204
+ await appendUpgradeLogEntry(slockHome, {
4205
+ fromBundle: { computerVersion: currentVersion },
4206
+ toBundle: { computerVersion: currentVersion },
4207
+ channel: channel2,
4208
+ trigger: "cli",
4209
+ outcome: "err",
4210
+ errorCode: "UPGRADE_ALREADY_RUNNING"
4211
+ }).catch(() => {
4212
+ });
4213
+ } catch {
4214
+ }
4215
+ process.stderr.write(
4216
+ `slock-computer: UPGRADE_ALREADY_RUNNING: Another upgrade attempt is currently holding the mutation lock. Wait for it to finish and retry, or check \`~/.slock/computer/upgrade.log\` for the in-flight attempt.
4217
+ `
4218
+ );
4219
+ throw new CliExit(1, "UPGRADE_ALREADY_RUNNING");
4220
+ }
4221
+ throw err;
4222
+ }
4032
4223
  }
4033
4224
  )
4034
4225
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/computer",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Slock Computer — standalone human/local-machine control-plane CLI (login + attach). Distinct from the agent-facing @slock-ai/cli.",
5
5
  "type": "module",
6
6
  "bin": {