@slock-ai/computer 0.0.6 → 0.0.8

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 +166 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -304,6 +304,13 @@ function upgradeStagingDir(slockHome, version) {
304
304
  function upgradeSnapshotPath(slockHome) {
305
305
  return path.join(computerDir(slockHome), "upgrade-snapshot.json");
306
306
  }
307
+ function upgradeLogPath(slockHome) {
308
+ return path.join(computerDir(slockHome), "upgrade.log");
309
+ }
310
+ function formatUpgradeLogTimestamp(date = /* @__PURE__ */ new Date()) {
311
+ const iso = date.toISOString();
312
+ return iso.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
313
+ }
307
314
 
308
315
  // src/output.ts
309
316
  var CliExit = class extends Error {
@@ -1411,6 +1418,44 @@ async function runResident(serverId, deps = {}) {
1411
1418
  }
1412
1419
  var RECONCILE_INTERVAL_MS = 5e3;
1413
1420
  var CHILD_RESTART_BACKOFF_MS = 2e3;
1421
+ var START_ENSURE_TIMEOUT_MS = 15e3;
1422
+ var START_ENSURE_POLL_INTERVAL_MS = 100;
1423
+ async function waitForManagedDaemonPids(slockHome, serverIds, deps) {
1424
+ const readPidfile = deps.readPidfile ?? readPidfileAt;
1425
+ const isAlive = deps.isProcessAlive ?? isProcessAlive2;
1426
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1427
+ const timeoutMs = deps.ensureTimeoutMs ?? START_ENSURE_TIMEOUT_MS;
1428
+ const pollIntervalMs = deps.ensurePollIntervalMs ?? START_ENSURE_POLL_INTERVAL_MS;
1429
+ const deadline = Date.now() + timeoutMs;
1430
+ const ready = /* @__PURE__ */ new Map();
1431
+ while (ready.size < serverIds.length) {
1432
+ for (const serverId of serverIds) {
1433
+ if (ready.has(serverId)) continue;
1434
+ const pid = await readPidfile(serverDaemonPidPath(slockHome, serverId));
1435
+ if (pid && isAlive(pid)) ready.set(serverId, pid);
1436
+ }
1437
+ if (ready.size === serverIds.length) return ready;
1438
+ const remaining = deadline - Date.now();
1439
+ if (remaining <= 0) return ready;
1440
+ await sleep2(Math.min(pollIntervalMs, remaining));
1441
+ }
1442
+ return ready;
1443
+ }
1444
+ function formatReadySummary(ready, serverIds, opts) {
1445
+ if (opts.serverId && serverIds.length === 1) {
1446
+ const pid = ready.get(opts.serverId);
1447
+ return `Daemon for server ${opts.serverLabel ?? opts.serverId} is running${pid ? ` (pid ${pid})` : ""}.`;
1448
+ }
1449
+ return `Daemons for ${serverIds.length} managed server(s) are running.`;
1450
+ }
1451
+ function failStartEnsureTimeout(slockHome, serverIds, ready, opts) {
1452
+ const missing = serverIds.filter((id) => !ready.has(id));
1453
+ const target = opts.serverId && missing.length === 1 ? `${opts.serverLabel ?? opts.serverId}` : `${missing.length} daemon(s): ${missing.join(", ")}`;
1454
+ fail(
1455
+ "START_DAEMON_TIMEOUT",
1456
+ `Timed out waiting for ${target} to start. Run \`slock-computer status\` and inspect ${supervisorLogPath(slockHome)} plus per-server daemon logs under ~/.slock/computer/servers/<serverId>/daemon.log.`
1457
+ );
1458
+ }
1414
1459
  async function runSupervisorStartupRecovery(slockHome) {
1415
1460
  const parentHoldsLock = process.env[PARENT_LOCK_HELD_ENV_VAR] === "1";
1416
1461
  try {
@@ -1592,7 +1637,7 @@ async function runSupervise() {
1592
1637
  await new Promise(() => {
1593
1638
  });
1594
1639
  }
1595
- async function runStart(opts = {}, _deps = {}) {
1640
+ async function runStart(opts = {}, deps = {}) {
1596
1641
  const slockHome = resolveSlockHome();
1597
1642
  const attached = await listAttachedServerIds(slockHome);
1598
1643
  if (attached.length === 0) {
@@ -1614,9 +1659,11 @@ async function runStart(opts = {}, _deps = {}) {
1614
1659
  const existing = await readPidfileAt(supervisorPidPath(slockHome));
1615
1660
  if (existing && isProcessAlive2(existing)) {
1616
1661
  info(`Supervisor already running (pid ${existing}).`);
1617
- info(
1618
- opts.serverId ? `Marked server ${opts.serverLabel ?? opts.serverId} as managed; its daemon will be ensured on next reconcile tick.` : `Marked all ${attached.length} attached server(s) as managed; their daemons will be ensured on next reconcile tick.`
1619
- );
1662
+ const ready2 = await waitForManagedDaemonPids(slockHome, managedTargets, deps);
1663
+ if (ready2.size !== managedTargets.length) {
1664
+ failStartEnsureTimeout(slockHome, managedTargets, ready2, opts);
1665
+ }
1666
+ info(formatReadySummary(ready2, managedTargets, opts));
1620
1667
  return;
1621
1668
  }
1622
1669
  if (opts.foreground) {
@@ -1628,18 +1675,66 @@ async function runStart(opts = {}, _deps = {}) {
1628
1675
  }
1629
1676
  let pid;
1630
1677
  try {
1631
- pid = await spawnDetachedSupervisor(slockHome);
1678
+ pid = await (deps.spawnDetachedSupervisor ?? spawnDetachedSupervisor)(slockHome);
1632
1679
  } catch (err) {
1633
1680
  const msg = err instanceof Error ? err.message : String(err);
1634
1681
  fail("SUPERVISOR_SPAWN_FAILED", msg);
1635
1682
  }
1636
1683
  info(`Supervisor started (pid ${pid}); keeps running after this terminal closes.`);
1684
+ const ready = await waitForManagedDaemonPids(slockHome, managedTargets, deps);
1685
+ if (ready.size !== managedTargets.length) {
1686
+ failStartEnsureTimeout(slockHome, managedTargets, ready, opts);
1687
+ }
1688
+ info(formatReadySummary(ready, managedTargets, opts));
1637
1689
  info(
1638
1690
  `Managing ${managedTargets.length} of ${attached.length} attached server(s). Logs: ${supervisorLogPath(slockHome)}`
1639
1691
  );
1640
1692
  info(`Per-server daemon logs: ~/.slock/computer/servers/<serverId>/daemon.log`);
1641
1693
  info(`Check state with \`slock-computer status\`.`);
1642
1694
  }
1695
+ async function runStop(deps = {}) {
1696
+ const slockHome = resolveSlockHome();
1697
+ const readPidfile = deps.readPidfile ?? readPidfileAt;
1698
+ const isAlive = deps.isProcessAlive ?? isProcessAlive2;
1699
+ const killer = deps.killSupervisor ?? ((pid2) => {
1700
+ process.kill(pid2, "SIGTERM");
1701
+ });
1702
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1703
+ const pollIntervalMs = deps.pollIntervalMs ?? 200;
1704
+ const timeoutMs = deps.timeoutMs ?? 5e3;
1705
+ const pidfilePath = supervisorPidPath(slockHome);
1706
+ const pid = await readPidfile(pidfilePath);
1707
+ if (pid === null) {
1708
+ info("Supervisor not running.");
1709
+ return;
1710
+ }
1711
+ if (!isAlive(pid)) {
1712
+ await clearPidfileAt(pidfilePath);
1713
+ info(`Supervisor not running (cleared stale pidfile for pid ${pid}).`);
1714
+ return;
1715
+ }
1716
+ try {
1717
+ killer(pid);
1718
+ } catch (err) {
1719
+ fail(
1720
+ "STOP_SIGNAL_FAILED",
1721
+ `Failed to send SIGTERM to supervisor (pid ${pid}): ${err instanceof Error ? err.message : String(err)}. Check process permissions or run: kill ${pid}`
1722
+ );
1723
+ }
1724
+ const deadline = Date.now() + timeoutMs;
1725
+ while (Date.now() < deadline) {
1726
+ if (!isAlive(pid)) {
1727
+ await clearPidfileAt(pidfilePath);
1728
+ info(`Stopped supervisor (pid ${pid}).`);
1729
+ return;
1730
+ }
1731
+ await sleep2(pollIntervalMs);
1732
+ }
1733
+ fail(
1734
+ "STOP_TIMEOUT",
1735
+ `Supervisor (pid ${pid}) did not exit within ${timeoutMs}ms after SIGTERM. Force-kill with: kill -9 ${pid}`
1736
+ );
1737
+ }
1643
1738
  async function runDetach(serverId, serverLabel = serverId) {
1644
1739
  assertValidServerId(serverId);
1645
1740
  const slockHome = resolveSlockHome();
@@ -3298,7 +3393,30 @@ async function locateStagedTarball(stagedPath) {
3298
3393
  return join6(stagedPath, tgz);
3299
3394
  }
3300
3395
 
3396
+ // src/upgradeLog.ts
3397
+ import { chmod as chmod5, mkdir as mkdir12, open as open2 } from "fs/promises";
3398
+ var FILE_MODE = 384;
3399
+ async function appendUpgradeLogEntry(slockHome, entry) {
3400
+ await mkdir12(computerDir(slockHome), { recursive: true });
3401
+ const path2 = upgradeLogPath(slockHome);
3402
+ const at = entry.at ?? formatUpgradeLogTimestamp();
3403
+ const fullEntry = { ...entry, at };
3404
+ const line = JSON.stringify(fullEntry) + "\n";
3405
+ const handle = await open2(path2, "a", FILE_MODE);
3406
+ try {
3407
+ await handle.write(line);
3408
+ } finally {
3409
+ await handle.close();
3410
+ }
3411
+ if (process.platform !== "win32") {
3412
+ await chmod5(path2, FILE_MODE);
3413
+ }
3414
+ }
3415
+
3301
3416
  // src/upgradeCli.ts
3417
+ function isEphemeralNpxContext(binaryDir) {
3418
+ return binaryDir.split(/[\\/]/).includes("_npx");
3419
+ }
3302
3420
  async function defaultSpawnFreshSupervisor(slockHome) {
3303
3421
  await spawnDetachedSupervisor(slockHome);
3304
3422
  }
@@ -3348,6 +3466,18 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
3348
3466
  }
3349
3467
  const fromVersion = await (deps.currentVersion ?? defaultCurrentVersion)();
3350
3468
  const currentBinaryDir = (deps.currentBinaryDir ?? defaultCurrentBinaryDir)();
3469
+ const isEphemeralFn = deps.isEphemeralNpxContext ?? isEphemeralNpxContext;
3470
+ if (isEphemeralFn(currentBinaryDir)) {
3471
+ fail(
3472
+ "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)`
3479
+ );
3480
+ }
3351
3481
  if (targetVersion === fromVersion) {
3352
3482
  info(`Already at ${fromVersion} (channel ${channel2}). Nothing to do.`);
3353
3483
  return;
@@ -3400,14 +3530,35 @@ async function runUpgradeCli(slockHome, opts, deps = {}) {
3400
3530
  spawnFreshSupervisor: () => spawnFreshSupervisor(slockHome)
3401
3531
  }
3402
3532
  });
3533
+ const bundle = (version) => ({
3534
+ computerVersion: version
3535
+ });
3536
+ const logTrigger = opts.trigger ?? "cli";
3403
3537
  if (outcome.ok) {
3404
3538
  info(
3405
3539
  `Upgrade ${fromVersion} \u2192 ${targetVersion} succeeded (health-OK in ${outcome.restart?.healthAfterMs ?? 0}ms).`
3406
3540
  );
3541
+ await appendUpgradeLogEntry(slockHome, {
3542
+ fromBundle: bundle(fromVersion),
3543
+ toBundle: bundle(targetVersion),
3544
+ channel: channel2,
3545
+ trigger: logTrigger,
3546
+ outcome: "ok"
3547
+ }).catch(() => {
3548
+ });
3407
3549
  return;
3408
3550
  }
3409
3551
  const code = mapFailurePhaseToCode(outcome);
3410
3552
  const rolledBack = outcome.rolledBack === true ? " (swap rolled back)" : "";
3553
+ await appendUpgradeLogEntry(slockHome, {
3554
+ fromBundle: bundle(fromVersion),
3555
+ toBundle: bundle(targetVersion),
3556
+ channel: channel2,
3557
+ trigger: logTrigger,
3558
+ outcome: "err",
3559
+ errorCode: code
3560
+ }).catch(() => {
3561
+ });
3411
3562
  fail(
3412
3563
  code,
3413
3564
  `Upgrade failed at phase=${outcome.phase}: ${outcome.reason ?? "unknown"}${rolledBack}.`
@@ -3473,7 +3624,7 @@ async function defaultCurrentVersion() {
3473
3624
  }
3474
3625
 
3475
3626
  // src/upgradeTestHarness.ts
3476
- import { mkdir as mkdir12, readdir as readdir4, stat as stat3, writeFile as writeFile10 } from "fs/promises";
3627
+ import { mkdir as mkdir13, readdir as readdir4, stat as stat3, writeFile as writeFile10 } from "fs/promises";
3477
3628
  import { join as join8 } from "path";
3478
3629
  import { createHash as createHash4 } from "crypto";
3479
3630
  var PHASES = /* @__PURE__ */ new Set([
@@ -3544,7 +3695,7 @@ function buildSimulatedDeps(slockHome, opts) {
3544
3695
  if (opts.simulateFail === "extract") {
3545
3696
  return { exitCode: 1, stderr: "simulated extract failure" };
3546
3697
  }
3547
- await mkdir12(join8(destDir, "package"), { recursive: true });
3698
+ await mkdir13(join8(destDir, "package"), { recursive: true });
3548
3699
  await writeFile10(join8(destDir, "package", "marker.txt"), `NEW@${targetVersion}`);
3549
3700
  return { exitCode: 0, stderr: "" };
3550
3701
  },
@@ -3605,7 +3756,7 @@ function buildSimulatedDeps(slockHome, opts) {
3605
3756
  }
3606
3757
  async function arrangeSnapshotFailure(slockHome) {
3607
3758
  const snapshotPath = join8(slockHome, "computer", "upgrade-snapshot.json");
3608
- await mkdir12(snapshotPath, { recursive: true });
3759
+ await mkdir13(snapshotPath, { recursive: true });
3609
3760
  }
3610
3761
  async function pathInfo(path2) {
3611
3762
  try {
@@ -3660,12 +3811,12 @@ async function runUpgradeTestHarness(slockHome, opts, writer = (s) => process.st
3660
3811
  process.exitCode = 1;
3661
3812
  return;
3662
3813
  }
3663
- await mkdir12(slockHome, { recursive: true });
3814
+ await mkdir13(slockHome, { recursive: true });
3664
3815
  if (opts.simulateFail === "snapshot") {
3665
3816
  await arrangeSnapshotFailure(slockHome);
3666
3817
  }
3667
3818
  const { opts: upgradeOpts } = buildSimulatedDeps(slockHome, opts);
3668
- await mkdir12(upgradeOpts.currentBinaryDir, { recursive: true });
3819
+ await mkdir13(upgradeOpts.currentBinaryDir, { recursive: true });
3669
3820
  let outcome;
3670
3821
  try {
3671
3822
  outcome = await runUpgrade(slockHome, upgradeOpts);
@@ -3702,7 +3853,7 @@ async function runUpgradeTestHarness(slockHome, opts, writer = (s) => process.st
3702
3853
  }
3703
3854
 
3704
3855
  // src/upgradeInstallSmoke.ts
3705
- import { copyFile, mkdir as mkdir13, readFile as readFile13 } from "fs/promises";
3856
+ import { copyFile, mkdir as mkdir14, readFile as readFile13 } from "fs/promises";
3706
3857
  import { createHash as createHash5 } from "crypto";
3707
3858
  import { dirname as dirname11, isAbsolute, join as join9, resolve as pathResolve } from "path";
3708
3859
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -3725,7 +3876,7 @@ async function runUpgradeInstallSmoke(slockHome, opts, deps = {}) {
3725
3876
  const spawnFreshSupervisor = deps.spawnFreshSupervisor ?? (async (h) => {
3726
3877
  await spawnDetachedSupervisor(h);
3727
3878
  });
3728
- await mkdir13(slockHome, { recursive: true });
3879
+ await mkdir14(slockHome, { recursive: true });
3729
3880
  const outcome = await runUpgrade(slockHome, {
3730
3881
  targetVersion: opts.targetVersion,
3731
3882
  fromVersion,
@@ -3868,6 +4019,9 @@ program.command("start").argument("[serverSlug]", "optional: verify this server
3868
4019
  })
3869
4020
  );
3870
4021
  }));
4022
+ program.command("stop").description("Stop the Computer supervisor (and all managed per-server daemons).").action(withCliExit(async () => {
4023
+ await withMutationLock(() => runStop());
4024
+ }));
3871
4025
  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(
3872
4026
  withCliExit(
3873
4027
  async (serverSlug, opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/computer",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
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": {