@slock-ai/computer 0.0.8 → 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 +163 -33
  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();
@@ -3396,7 +3407,49 @@ async function locateStagedTarball(stagedPath) {
3396
3407
  // src/upgradeLog.ts
3397
3408
  import { chmod as chmod5, mkdir as mkdir12, open as open2 } from "fs/promises";
3398
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
+ }
3399
3451
  async function appendUpgradeLogEntry(slockHome, entry) {
3452
+ assertUpgradeLogEntry(entry);
3400
3453
  await mkdir12(computerDir(slockHome), { recursive: true });
3401
3454
  const path2 = upgradeLogPath(slockHome);
3402
3455
  const at = entry.at ?? formatUpgradeLogTimestamp();
@@ -3417,6 +3470,19 @@ async function appendUpgradeLogEntry(slockHome, entry) {
3417
3470
  function isEphemeralNpxContext(binaryDir) {
3418
3471
  return binaryDir.split(/[\\/]/).includes("_npx");
3419
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
+ }
3420
3486
  async function defaultSpawnFreshSupervisor(slockHome) {
3421
3487
  await spawnDetachedSupervisor(slockHome);
3422
3488
  }
@@ -3470,16 +3536,20 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
3470
3536
  if (isEphemeralFn(currentBinaryDir)) {
3471
3537
  fail(
3472
3538
  "UPGRADE_EPHEMERAL_CONTEXT",
3473
- `Cannot upgrade from an ephemeral npx context (${currentBinaryDir}).
3474
- The currently installed Computer service runs from a different install root, which this command cannot reach. To upgrade, manually replace the persistent install:
3475
- npx -y @slock-ai/computer@latest stop
3476
- rm -rf ~/.npm/_npx/<persistent-hash>/
3477
- npx -y @slock-ai/computer@latest start
3478
- (Find the persistent hash via: ls -d ~/.npm/_npx/*/node_modules/@slock-ai/computer)`
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\`.`
3479
3540
  );
3480
3541
  }
3481
3542
  if (targetVersion === fromVersion) {
3482
- 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
+ });
3483
3553
  return;
3484
3554
  }
3485
3555
  info(`Planning upgrade: ${fromVersion} \u2192 ${targetVersion} (channel ${channel2}).`);
@@ -3530,9 +3600,9 @@ The currently installed Computer service runs from a different install root, whi
3530
3600
  spawnFreshSupervisor: () => spawnFreshSupervisor(slockHome)
3531
3601
  }
3532
3602
  });
3533
- const bundle = (version) => ({
3534
- computerVersion: version
3535
- });
3603
+ const readBundledFn = deps.readBundledDaemonVersion ?? readBundledDaemonVersion;
3604
+ const bundledDaemonVersion = await readBundledFn(currentBinaryDir);
3605
+ const bundle = (version) => bundledDaemonVersion !== null ? { computerVersion: version, bundledDaemonVersion } : { computerVersion: version };
3536
3606
  const logTrigger = opts.trigger ?? "cli";
3537
3607
  if (outcome.ok) {
3538
3608
  info(
@@ -3548,8 +3618,28 @@ The currently installed Computer service runs from a different install root, whi
3548
3618
  });
3549
3619
  return;
3550
3620
  }
3551
- const code = mapFailurePhaseToCode(outcome);
3552
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);
3553
3643
  await appendUpgradeLogEntry(slockHome, {
3554
3644
  fromBundle: bundle(fromVersion),
3555
3645
  toBundle: bundle(targetVersion),
@@ -3566,26 +3656,27 @@ The currently installed Computer service runs from a different install root, whi
3566
3656
  }
3567
3657
  function mapFailurePhaseToCode(outcome) {
3568
3658
  switch (outcome.phase) {
3569
- case "drain":
3570
- return "UPGRADE_DEFERRED";
3571
3659
  case "stage":
3572
- return "UPGRADE_DOWNLOAD_FAILED";
3660
+ return "UPGRADE_NETWORK_FAILED";
3573
3661
  case "verify":
3574
- return "UPGRADE_VERIFY_FAILED";
3662
+ return "UPGRADE_INTEGRITY_FAILED";
3575
3663
  case "preflight":
3576
3664
  return "UPGRADE_DEPS_CHANGED";
3577
3665
  case "snapshot":
3578
- return "UPGRADE_SNAPSHOT_FAILED";
3666
+ return "UPGRADE_SWAP_FAILED";
3579
3667
  case "extract":
3580
- return "UPGRADE_EXTRACT_FAILED";
3668
+ return "UPGRADE_INTEGRITY_FAILED";
3581
3669
  case "swap":
3582
3670
  return "UPGRADE_SWAP_FAILED";
3583
3671
  case "restart":
3584
3672
  return "UPGRADE_RESTART_FAILED";
3585
3673
  case "rolling_health":
3586
- return "UPGRADE_DAEMON_HEALTH_FAILED";
3674
+ return "UPGRADE_RESTART_FAILED";
3675
+ case "drain":
3587
3676
  case "cleanup":
3588
- 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
+ );
3589
3680
  }
3590
3681
  }
3591
3682
  async function defaultFetchDistTags() {
@@ -3946,6 +4037,19 @@ async function defaultCurrentVersionLocal() {
3946
4037
  );
3947
4038
  }
3948
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
+
3949
4053
  // src/index.ts
3950
4054
  function withCliExit(fn) {
3951
4055
  return async (...args) => {
@@ -3961,7 +4065,7 @@ function withCliExit(fn) {
3961
4065
  };
3962
4066
  }
3963
4067
  var program = new Command();
3964
- 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);
3965
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) => {
3966
4070
  await runLogin({ serverUrl: opts.serverUrl });
3967
4071
  }));
@@ -4081,15 +4185,41 @@ program.command("upgrade").description(
4081
4185
  }
4082
4186
  drain = opts.drain;
4083
4187
  }
4084
- await withMutationLock(
4085
- () => runUpgradeCli(resolveSlockHome(), {
4086
- dryRun: opts.dryRun,
4087
- channel: opts.channel,
4088
- targetVersion: opts.targetVersion,
4089
- drain,
4090
- force: opts.force
4091
- })
4092
- );
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
+ }
4093
4223
  }
4094
4224
  )
4095
4225
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/computer",
3
- "version": "0.0.8",
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": {