aicomputer 0.1.17 → 0.1.19

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
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  formatMountHostInstallGuidance,
4
4
  getMountHostValidationIssues
5
- } from "./chunk-OWK5N76S.js";
5
+ } from "./chunk-JMRAYXUO.js";
6
6
  import {
7
7
  ApiError,
8
8
  api,
@@ -33,7 +33,10 @@ import {
33
33
  timeAgo,
34
34
  vncURL,
35
35
  webURL
36
- } from "./chunk-5IEWKH52.js";
36
+ } from "./chunk-5JYMQXKN.js";
37
+ import {
38
+ isAbortError
39
+ } from "./chunk-NN4GECN6.js";
37
40
  import {
38
41
  defaultMountServiceConfig,
39
42
  ensureMountDirectories,
@@ -46,10 +49,19 @@ import {
46
49
  writeMountControllerLock,
47
50
  writeMountStatusSnapshot
48
51
  } from "./chunk-KXLTHWW3.js";
52
+ import {
53
+ AGENTCOMPUTER_MUTAGEN_PATH_ENV,
54
+ ensureBundledMutagenInstalled,
55
+ ensureMutagenCommandPath
56
+ } from "./chunk-MDSPJ57B.js";
57
+ import {
58
+ compareVersions,
59
+ resolveLatestPublishedVersion
60
+ } from "./chunk-GGBVVRLL.js";
49
61
 
50
62
  // src/index.ts
51
- import { Command as Command14 } from "commander";
52
- import chalk12 from "chalk";
63
+ import { Command as Command15 } from "commander";
64
+ import chalk13 from "chalk";
53
65
  import { readFileSync as readFileSync3 } from "fs";
54
66
  import { basename as basename2 } from "path";
55
67
 
@@ -3561,9 +3573,35 @@ async function confirmDeletion(sourceID) {
3561
3573
  });
3562
3574
  }
3563
3575
 
3564
- // src/commands/login.ts
3576
+ // src/commands/internal-install-mutagen.ts
3565
3577
  import { Command as Command9 } from "commander";
3566
3578
  import chalk7 from "chalk";
3579
+ var internalInstallMutagenCommand = new Command9("internal-install-mutagen").option("--quiet", "Suppress output unless installation fails").action(async (options) => {
3580
+ try {
3581
+ const executablePath = await ensureBundledMutagenInstalled();
3582
+ if (!options.quiet) {
3583
+ console.log();
3584
+ console.log(
3585
+ chalk7.green(
3586
+ ` Bundled Mutagen ready at ${executablePath}.`
3587
+ )
3588
+ );
3589
+ console.log();
3590
+ }
3591
+ } catch (error) {
3592
+ if (!options.quiet) {
3593
+ const message = error instanceof Error ? error.message : "failed to install bundled Mutagen";
3594
+ console.error();
3595
+ console.error(chalk7.red(` ${message}`));
3596
+ console.error();
3597
+ }
3598
+ process.exit(1);
3599
+ }
3600
+ });
3601
+
3602
+ // src/commands/login.ts
3603
+ import { Command as Command10 } from "commander";
3604
+ import chalk8 from "chalk";
3567
3605
  import ora8 from "ora";
3568
3606
 
3569
3607
  // src/lib/browser-login.ts
@@ -3831,12 +3869,12 @@ function escapeHTML(value) {
3831
3869
  }
3832
3870
 
3833
3871
  // src/commands/login.ts
3834
- var loginCommand = new Command9("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
3872
+ var loginCommand = new Command10("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
3835
3873
  const existingKey = getStoredAPIKey();
3836
3874
  if (existingKey && !options.force) {
3837
3875
  console.log();
3838
3876
  console.log(
3839
- chalk7.yellow(" Already logged in. Use --force to overwrite.")
3877
+ chalk8.yellow(" Already logged in. Use --force to overwrite.")
3840
3878
  );
3841
3879
  console.log();
3842
3880
  return;
@@ -3845,8 +3883,8 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
3845
3883
  const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
3846
3884
  if (!apiKey && wantsManualLogin) {
3847
3885
  console.log();
3848
- console.log(chalk7.dim(" Usage: computer login --api-key <ac_live_...>"));
3849
- console.log(chalk7.dim(` API: ${getBaseURL()}`));
3886
+ console.log(chalk8.dim(" Usage: computer login --api-key <ac_live_...>"));
3887
+ console.log(chalk8.dim(` API: ${getBaseURL()}`));
3850
3888
  console.log();
3851
3889
  process.exit(1);
3852
3890
  }
@@ -3856,7 +3894,7 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
3856
3894
  }
3857
3895
  if (!apiKey.startsWith("ac_live_")) {
3858
3896
  console.log();
3859
- console.log(chalk7.red(" API key must start with ac_live_"));
3897
+ console.log(chalk8.red(" API key must start with ac_live_"));
3860
3898
  console.log();
3861
3899
  process.exit(1);
3862
3900
  }
@@ -3864,7 +3902,7 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
3864
3902
  try {
3865
3903
  const me = await apiWithKey(apiKey, "/v1/me");
3866
3904
  setAPIKey(apiKey);
3867
- spinner.succeed(`Logged in as ${chalk7.bold(me.user.email)}`);
3905
+ spinner.succeed(`Logged in as ${chalk8.bold(me.user.email)}`);
3868
3906
  } catch (error) {
3869
3907
  spinner.fail(
3870
3908
  error instanceof Error ? error.message : "Failed to validate API key"
@@ -3884,15 +3922,15 @@ async function runBrowserLogin() {
3884
3922
  spinner.stop();
3885
3923
  console.log();
3886
3924
  console.log(
3887
- chalk7.yellow(" Browser auto-open failed. Open this URL to continue:")
3925
+ chalk8.yellow(" Browser auto-open failed. Open this URL to continue:")
3888
3926
  );
3889
- console.log(chalk7.dim(` ${attempt.loginURL}`));
3927
+ console.log(chalk8.dim(` ${attempt.loginURL}`));
3890
3928
  console.log();
3891
3929
  spinner.start("Waiting for browser login...");
3892
3930
  }
3893
3931
  spinner.text = "Waiting for browser login...";
3894
3932
  const result = await attempt.waitForResult();
3895
- spinner.succeed(`Logged in as ${chalk7.bold(result.me.user.email)}`);
3933
+ spinner.succeed(`Logged in as ${chalk8.bold(result.me.user.email)}`);
3896
3934
  await continueFirstLoginFlow(result);
3897
3935
  } catch (error) {
3898
3936
  spinner.fail(
@@ -3926,8 +3964,8 @@ async function continueFirstLoginFlow(result) {
3926
3964
  }
3927
3965
  console.log();
3928
3966
  console.log(
3929
- chalk7.cyan(
3930
- `Continuing first-time setup for ${chalk7.bold(machineHandle)}...
3967
+ chalk8.cyan(
3968
+ `Continuing first-time setup for ${chalk8.bold(machineHandle)}...
3931
3969
  `
3932
3970
  )
3933
3971
  );
@@ -3940,8 +3978,8 @@ async function continueFirstLoginFlow(result) {
3940
3978
  const spinner = ora8(`Preparing SSH access for ${machineHandle}...`).start();
3941
3979
  try {
3942
3980
  const connection = await prepareSSHConnectionByIdentifier(machineHandle);
3943
- spinner.succeed(`Connecting to ${chalk7.bold(machineHandle)}`);
3944
- console.log(chalk7.dim(` ${connection.command}`));
3981
+ spinner.succeed(`Connecting to ${chalk8.bold(machineHandle)}`);
3982
+ console.log(chalk8.dim(` ${connection.command}`));
3945
3983
  console.log();
3946
3984
  await openSSHConnection(connection);
3947
3985
  } catch (error) {
@@ -3952,19 +3990,19 @@ async function continueFirstLoginFlow(result) {
3952
3990
  }
3953
3991
  } catch (error) {
3954
3992
  const message = error instanceof Error ? error.message : "Failed to finish first-time setup";
3955
- console.error(chalk7.red(`
3993
+ console.error(chalk8.red(`
3956
3994
  ${message}`));
3957
3995
  console.log();
3958
3996
  if (result.provider === "claude") {
3959
3997
  console.log(
3960
- chalk7.dim(` computer claude-login --machine ${machineHandle}`)
3998
+ chalk8.dim(` computer claude-login --machine ${machineHandle}`)
3961
3999
  );
3962
4000
  } else if (result.provider === "codex") {
3963
4001
  console.log(
3964
- chalk7.dim(` computer codex-login --machine ${machineHandle}`)
4002
+ chalk8.dim(` computer codex-login --machine ${machineHandle}`)
3965
4003
  );
3966
4004
  }
3967
- console.log(chalk7.dim(` computer ssh ${machineHandle}`));
4005
+ console.log(chalk8.dim(` computer ssh ${machineHandle}`));
3968
4006
  console.log();
3969
4007
  process.exit(1);
3970
4008
  }
@@ -3978,19 +4016,19 @@ async function runSelectedProvider(provider, machineHandle) {
3978
4016
  await runCodexLogin({ machine: machineHandle });
3979
4017
  return;
3980
4018
  }
3981
- console.log(chalk7.green(`Sandbox ${chalk7.bold(machineHandle)} is ready.`));
4019
+ console.log(chalk8.green(`Sandbox ${chalk8.bold(machineHandle)} is ready.`));
3982
4020
  console.log();
3983
4021
  }
3984
4022
  function printNextStep(machineHandle) {
3985
- console.log(chalk7.green(`Sandbox ${chalk7.bold(machineHandle)} is ready.`));
3986
- console.log(chalk7.dim(` computer ssh ${machineHandle}`));
4023
+ console.log(chalk8.green(`Sandbox ${chalk8.bold(machineHandle)} is ready.`));
4024
+ console.log(chalk8.dim(` computer ssh ${machineHandle}`));
3987
4025
  console.log();
3988
4026
  }
3989
4027
 
3990
4028
  // src/commands/mount.ts
3991
4029
  import { spawn as spawn4 } from "child_process";
3992
- import { Command as Command10, Option } from "commander";
3993
- import chalk8 from "chalk";
4030
+ import { Command as Command11, Option } from "commander";
4031
+ import chalk9 from "chalk";
3994
4032
  import ora9 from "ora";
3995
4033
 
3996
4034
  // src/lib/mount-daemon.ts
@@ -4007,7 +4045,7 @@ function getMountControllerState(rootPath = defaultMountServiceConfig().rootPath
4007
4045
  return { running: true, pid: lock.pid };
4008
4046
  }
4009
4047
  async function runMountDaemon(config, options = {}) {
4010
- const { onStarted } = options;
4048
+ const { onReady, onStarted } = options;
4011
4049
  const paths = getMountPaths(config.rootPath);
4012
4050
  ensureMountDirectories(paths);
4013
4051
  await mkdir3(paths.rootPath, { recursive: true });
@@ -4017,16 +4055,35 @@ async function runMountDaemon(config, options = {}) {
4017
4055
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4018
4056
  controllerPid: process.pid,
4019
4057
  running: true,
4058
+ startupPhase: "starting",
4059
+ startupMessage: "Starting mount controller...",
4020
4060
  mounts: []
4021
4061
  },
4022
4062
  config.rootPath
4023
4063
  );
4024
4064
  onStarted?.();
4065
+ writeMountStatusSnapshot(
4066
+ {
4067
+ ...readMountStatusSnapshot(config.rootPath) ?? {
4068
+ controllerPid: process.pid,
4069
+ running: true,
4070
+ mounts: []
4071
+ },
4072
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4073
+ controllerPid: process.pid,
4074
+ running: true,
4075
+ startupPhase: "cleaning",
4076
+ startupMessage: "Cleaning stale mount state..."
4077
+ },
4078
+ config.rootPath
4079
+ );
4025
4080
  await teardownManagedSessions(config, paths);
4081
+ await mkdir3(paths.rootPath, { recursive: true });
4026
4082
  let running = false;
4027
4083
  let queued = false;
4028
4084
  let shuttingDown = false;
4029
4085
  let activeRun = null;
4086
+ let activeRunController = null;
4030
4087
  const runOnce = async () => {
4031
4088
  if (shuttingDown) {
4032
4089
  return;
@@ -4035,12 +4092,19 @@ async function runMountDaemon(config, options = {}) {
4035
4092
  queued = true;
4036
4093
  return activeRun ?? void 0;
4037
4094
  }
4095
+ const controller = new AbortController();
4096
+ activeRunController = controller;
4038
4097
  activeRun = (async () => {
4039
4098
  running = true;
4040
4099
  try {
4041
- await reconcileMounts(config, paths, process.pid);
4100
+ await reconcileMounts(
4101
+ config,
4102
+ paths,
4103
+ process.pid,
4104
+ controller.signal
4105
+ );
4042
4106
  } catch (error) {
4043
- if (shuttingDown) {
4107
+ if (shuttingDown && isAbortError(error)) {
4044
4108
  return;
4045
4109
  }
4046
4110
  const previous = readMountStatusSnapshot(config.rootPath);
@@ -4049,6 +4113,8 @@ async function runMountDaemon(config, options = {}) {
4049
4113
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4050
4114
  controllerPid: process.pid,
4051
4115
  running: true,
4116
+ startupPhase: previous?.startupPhase === "ready" ? "ready" : "syncing",
4117
+ startupMessage: previous?.startupPhase === "ready" ? void 0 : "Waiting for initial sync...",
4052
4118
  lastHealthySyncAt: previous?.lastHealthySyncAt,
4053
4119
  lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
4054
4120
  lastIssueAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4064,6 +4130,7 @@ async function runMountDaemon(config, options = {}) {
4064
4130
  } finally {
4065
4131
  running = false;
4066
4132
  activeRun = null;
4133
+ activeRunController = null;
4067
4134
  if (queued && !shuttingDown) {
4068
4135
  queued = false;
4069
4136
  void runOnce();
@@ -4079,6 +4146,21 @@ async function runMountDaemon(config, options = {}) {
4079
4146
  server.once("error", reject);
4080
4147
  server.listen(paths.socketPath, () => resolve());
4081
4148
  });
4149
+ writeMountStatusSnapshot(
4150
+ {
4151
+ ...readMountStatusSnapshot(config.rootPath) ?? {
4152
+ controllerPid: process.pid,
4153
+ running: true,
4154
+ mounts: []
4155
+ },
4156
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4157
+ controllerPid: process.pid,
4158
+ running: true,
4159
+ startupPhase: "syncing",
4160
+ startupMessage: "Waiting for initial sync..."
4161
+ },
4162
+ config.rootPath
4163
+ );
4082
4164
  const interval = setInterval(() => {
4083
4165
  void runOnce();
4084
4166
  }, config.pollIntervalMs);
@@ -4089,6 +4171,7 @@ async function runMountDaemon(config, options = {}) {
4089
4171
  shuttingDown = true;
4090
4172
  clearInterval(interval);
4091
4173
  server.close();
4174
+ activeRunController?.abort();
4092
4175
  if (activeRun) {
4093
4176
  await activeRun.catch(() => {
4094
4177
  });
@@ -4100,6 +4183,8 @@ async function runMountDaemon(config, options = {}) {
4100
4183
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4101
4184
  controllerPid: previous?.controllerPid,
4102
4185
  running: false,
4186
+ startupPhase: void 0,
4187
+ startupMessage: void 0,
4103
4188
  lastHealthySyncAt: previous?.lastHealthySyncAt,
4104
4189
  lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
4105
4190
  lastIssueAt: previous?.lastIssueAt,
@@ -4119,6 +4204,8 @@ async function runMountDaemon(config, options = {}) {
4119
4204
  void shutdown();
4120
4205
  });
4121
4206
  await runOnce();
4207
+ writeMountStatusSnapshot(markMountStartupReady(readMountStatusSnapshot(config.rootPath), process.pid), config.rootPath);
4208
+ onReady?.();
4122
4209
  await new Promise(() => {
4123
4210
  });
4124
4211
  }
@@ -4147,41 +4234,69 @@ function processExists(pid) {
4147
4234
  return false;
4148
4235
  }
4149
4236
  }
4237
+ function markMountStartupReady(snapshot, controllerPid) {
4238
+ return {
4239
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4240
+ controllerPid,
4241
+ running: true,
4242
+ startupPhase: "ready",
4243
+ startupMessage: void 0,
4244
+ lastHealthySyncAt: snapshot?.lastHealthySyncAt,
4245
+ lastSuccessfulSyncAt: snapshot?.lastSuccessfulSyncAt,
4246
+ lastIssueAt: snapshot?.lastIssueAt,
4247
+ lastIssue: snapshot?.lastIssue,
4248
+ lastError: snapshot?.lastError,
4249
+ mounts: snapshot?.mounts ?? []
4250
+ };
4251
+ }
4150
4252
 
4151
4253
  // src/commands/mount.ts
4152
- var mountCommand = new Command10("mount").description("Mirror SSH-ready machines under ~/agentcomputer with a local mount controller").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").option("--poll-interval <ms>", "Reconcile interval in milliseconds", "5000").option("--connect-timeout <seconds>", "SSH connect timeout for Mutagen", "5").option(
4254
+ var MOUNT_START_SPINNER = {
4255
+ interval: 90,
4256
+ frames: ["\u25F0", "\u25F3", "\u25F2", "\u25F1"]
4257
+ };
4258
+ var mountCommand = new Command11("mount").description("Mirror SSH-ready machines under ~/agentcomputer with a local mount controller").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").option("--poll-interval <ms>", "Reconcile interval in milliseconds", "5000").option("--connect-timeout <seconds>", "SSH connect timeout for Mutagen", "5").option(
4153
4259
  "--background",
4154
4260
  "Run the mount controller in the background and print its PID"
4155
- ).addOption(new Option("--daemonized").hideHelp()).action(async (options) => {
4156
- const spinner = ora9(
4157
- options.background ? "Starting machine mount controller in background..." : "Starting machine mount controller..."
4158
- ).start();
4261
+ ).addOption(new Option("--daemonized").hideHelp()).addOption(new Option("--open-root-when-ready").hideHelp()).action(async (options) => {
4262
+ const spinner = ora9({
4263
+ spinner: MOUNT_START_SPINNER,
4264
+ text: options.background ? "Starting machine mount controller in background..." : "Starting machine mount controller..."
4265
+ }).start();
4159
4266
  try {
4160
4267
  if (options.daemonized) {
4161
- const config2 = readMountConfig() ?? defaultMountServiceConfig();
4162
- await runMountDaemon(config2, {
4268
+ const config = readMountConfig() ?? defaultMountServiceConfig();
4269
+ await runMountDaemon(config, {
4163
4270
  onStarted: () => {
4164
4271
  spinner.succeed("Machine mount controller running");
4165
- printMountStartSummary(config2, process.pid, "foreground");
4272
+ printMountStartSummary(config, process.pid, "foreground");
4273
+ },
4274
+ onReady: () => {
4275
+ if (options.openRootWhenReady) {
4276
+ revealMountRootInFinder(config.rootPath);
4277
+ }
4166
4278
  }
4167
4279
  });
4168
4280
  return;
4169
4281
  }
4170
- const config = await resolveMountServiceConfig(options);
4171
- writeMountConfig(config);
4282
+ const resolved = await resolveMountServiceConfig(options);
4283
+ writeMountConfig(resolved.config);
4284
+ process.env[AGENTCOMPUTER_MUTAGEN_PATH_ENV] = resolved.mutagenPath;
4172
4285
  if (options.background) {
4173
- const pid = await startMountControllerInBackground(config.rootPath);
4286
+ const pid = await startMountControllerInBackground(resolved.config.rootPath);
4174
4287
  spinner.succeed("Machine mount controller running in background");
4175
- printMountStartSummary(config, pid, "background");
4288
+ printMountStartSummary(resolved.config, pid, "background");
4176
4289
  return;
4177
4290
  }
4178
- await runMountDaemon(config, {
4179
- onStarted: () => {
4180
- spinner.succeed("Machine mount controller running");
4181
- printMountStartSummary(config, process.pid, "foreground");
4182
- revealMountRootInFinder(config.rootPath);
4183
- }
4184
- });
4291
+ const child = await startMountControllerInForeground(resolved.config.rootPath);
4292
+ await waitForMountControllerReady(resolved.config.rootPath, child.pid, spinner);
4293
+ spinner.succeed("Machine mount controller running");
4294
+ printMountStartSummary(
4295
+ resolved.config,
4296
+ child.pid ?? process.pid,
4297
+ "foreground"
4298
+ );
4299
+ await superviseForegroundMountController(child);
4185
4300
  } catch (error) {
4186
4301
  spinner.fail(
4187
4302
  error instanceof Error ? error.message : "Failed to start machine mount controller"
@@ -4189,40 +4304,40 @@ var mountCommand = new Command10("mount").description("Mirror SSH-ready machines
4189
4304
  process.exit(1);
4190
4305
  }
4191
4306
  }).addCommand(
4192
- new Command10("status").description("Show machine mount controller status").action(() => {
4307
+ new Command11("status").description("Show machine mount controller status").action(() => {
4193
4308
  const config = readMountConfig() ?? defaultMountServiceConfig();
4194
4309
  const controller = getMountControllerState(config.rootPath);
4195
4310
  const snapshot = readMountStatusSnapshot(config.rootPath);
4196
4311
  console.log();
4197
- console.log(` ${chalk8.bold("Machine Mounts")}`);
4312
+ console.log(` ${chalk9.bold("Machine Mounts")}`);
4198
4313
  console.log();
4199
4314
  console.log(
4200
- ` ${chalk8.dim("Running")} ${controller.running ? chalk8.green("yes") : chalk8.dim("no")}`
4315
+ ` ${chalk9.dim("Running")} ${controller.running ? chalk9.green("yes") : chalk9.dim("no")}`
4201
4316
  );
4202
4317
  if (controller.pid) {
4203
- console.log(` ${chalk8.dim("PID")} ${controller.pid}`);
4318
+ console.log(` ${chalk9.dim("PID")} ${controller.pid}`);
4204
4319
  }
4205
- console.log(` ${chalk8.dim("Root")} ${config.rootPath}`);
4206
- console.log(` ${chalk8.dim("Alias")} ${config.alias}`);
4320
+ console.log(` ${chalk9.dim("Root")} ${config.rootPath}`);
4321
+ console.log(` ${chalk9.dim("Alias")} ${config.alias}`);
4207
4322
  console.log(
4208
- ` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk8.dim("never")}`
4323
+ ` ${chalk9.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk9.dim("never")}`
4209
4324
  );
4210
4325
  console.log(
4211
- ` ${chalk8.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk8.dim("never")}`
4326
+ ` ${chalk9.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk9.dim("never")}`
4212
4327
  );
4213
4328
  if (controller.running && snapshot?.mounts.length) {
4214
4329
  console.log();
4215
4330
  for (const mount of snapshot.mounts) {
4216
4331
  const state = formatMountState(mount.state);
4217
- console.log(` ${chalk8.white(mount.handle)} ${state} ${chalk8.dim(mount.mountPath)}`);
4332
+ console.log(` ${chalk9.white(mount.handle)} ${state} ${chalk9.dim(mount.mountPath)}`);
4218
4333
  if (mount.message) {
4219
- console.log(` ${chalk8.dim(mount.message)}`);
4334
+ console.log(` ${chalk9.dim(mount.message)}`);
4220
4335
  }
4221
4336
  }
4222
4337
  }
4223
4338
  if (snapshot?.lastIssue) {
4224
4339
  console.log();
4225
- console.log(` ${chalk8.dim("Last issue")} ${chalk8.yellow(snapshot.lastIssue)}`);
4340
+ console.log(` ${chalk9.dim("Last issue")} ${chalk9.yellow(snapshot.lastIssue)}`);
4226
4341
  }
4227
4342
  console.log();
4228
4343
  })
@@ -4244,20 +4359,35 @@ async function resolveMountServiceConfig(options) {
4244
4359
  ].join("\n")
4245
4360
  );
4246
4361
  }
4362
+ const mutagenPath = await ensureMutagenCommandPath();
4247
4363
  const sshSetup = await ensureSSHAccessConfigured(options);
4248
4364
  return {
4249
- ...defaultMountServiceConfig(),
4250
- alias: sshSetup.alias,
4251
- host: sshSetup.host,
4252
- port: sshSetup.port,
4253
- pollIntervalMs: parsePositiveInt(options.pollInterval, "poll interval"),
4254
- connectTimeoutSeconds: parsePositiveInt(
4255
- options.connectTimeout,
4256
- "connect timeout"
4257
- )
4365
+ config: {
4366
+ ...defaultMountServiceConfig(),
4367
+ alias: sshSetup.alias,
4368
+ host: sshSetup.host,
4369
+ port: sshSetup.port,
4370
+ pollIntervalMs: parsePositiveInt(options.pollInterval, "poll interval"),
4371
+ connectTimeoutSeconds: parsePositiveInt(
4372
+ options.connectTimeout,
4373
+ "connect timeout"
4374
+ )
4375
+ },
4376
+ mutagenPath
4258
4377
  };
4259
4378
  }
4260
4379
  async function startMountControllerInBackground(rootPath) {
4380
+ const child = startMountControllerProcess(rootPath, true, false);
4381
+ child.unref();
4382
+ await waitForMountControllerRunning(rootPath, child.pid);
4383
+ return child.pid;
4384
+ }
4385
+ async function startMountControllerInForeground(rootPath) {
4386
+ const child = startMountControllerProcess(rootPath, false, true);
4387
+ await waitForMountControllerRunning(rootPath, child.pid);
4388
+ return child;
4389
+ }
4390
+ function startMountControllerProcess(rootPath, detached, openRootWhenReady) {
4261
4391
  const controller = getMountControllerState(rootPath);
4262
4392
  if (controller.running) {
4263
4393
  throw new Error(`computer mount is already running (pid ${controller.pid})`);
@@ -4266,20 +4396,22 @@ async function startMountControllerInBackground(rootPath) {
4266
4396
  if (!entrypoint) {
4267
4397
  throw new Error("unable to determine CLI entrypoint for background mount");
4268
4398
  }
4269
- const child = spawn4(process.execPath, [entrypoint, "mount", "--daemonized"], {
4399
+ const args = ["mount", "--daemonized"];
4400
+ if (openRootWhenReady) {
4401
+ args.push("--open-root-when-ready");
4402
+ }
4403
+ const child = spawn4(process.execPath, [entrypoint, ...args], {
4270
4404
  cwd: process.cwd(),
4271
- detached: true,
4405
+ detached,
4272
4406
  env: process.env,
4273
4407
  stdio: "ignore"
4274
4408
  });
4275
4409
  if (!child.pid) {
4276
- throw new Error("failed to start machine mount controller in background");
4410
+ throw new Error("failed to start machine mount controller");
4277
4411
  }
4278
- child.unref();
4279
- await waitForBackgroundMountController(rootPath, child.pid);
4280
- return child.pid;
4412
+ return child;
4281
4413
  }
4282
- async function waitForBackgroundMountController(rootPath, pid) {
4414
+ async function waitForMountControllerRunning(rootPath, pid) {
4283
4415
  const deadline = Date.now() + 3e3;
4284
4416
  while (Date.now() < deadline) {
4285
4417
  const controller = getMountControllerState(rootPath);
@@ -4293,15 +4425,77 @@ async function waitForBackgroundMountController(rootPath, pid) {
4293
4425
  }
4294
4426
  throw new Error("failed to start machine mount controller in background");
4295
4427
  }
4428
+ async function waitForMountControllerReady(rootPath, pid, spinner) {
4429
+ const deadline = Date.now() + 12e4;
4430
+ while (Date.now() < deadline) {
4431
+ const controller = getMountControllerState(rootPath);
4432
+ const snapshot = readMountStatusSnapshot(rootPath);
4433
+ if (snapshot?.controllerPid === pid && snapshot.startupMessage) {
4434
+ spinner.text = snapshot.startupMessage;
4435
+ }
4436
+ if (snapshot?.controllerPid === pid && snapshot.startupPhase === "ready") {
4437
+ return;
4438
+ }
4439
+ if (!controller.running && !processExists2(pid)) {
4440
+ break;
4441
+ }
4442
+ await new Promise((resolve) => setTimeout(resolve, 100));
4443
+ }
4444
+ throw new Error("mount controller did not finish startup");
4445
+ }
4446
+ async function superviseForegroundMountController(child) {
4447
+ await new Promise((resolve, reject) => {
4448
+ const cleanup = () => {
4449
+ process.off("SIGINT", onSigint);
4450
+ process.off("SIGTERM", onSigterm);
4451
+ child.off("error", onError);
4452
+ child.off("exit", onExit);
4453
+ };
4454
+ const stopAndExit = (signal) => {
4455
+ cleanup();
4456
+ try {
4457
+ child.kill(signal);
4458
+ } catch {
4459
+ }
4460
+ process.exit(0);
4461
+ };
4462
+ const onSigint = () => {
4463
+ stopAndExit("SIGINT");
4464
+ };
4465
+ const onSigterm = () => {
4466
+ stopAndExit("SIGTERM");
4467
+ };
4468
+ const onError = (error) => {
4469
+ cleanup();
4470
+ reject(error);
4471
+ };
4472
+ const onExit = (code, signal) => {
4473
+ cleanup();
4474
+ if (code === 0) {
4475
+ resolve();
4476
+ return;
4477
+ }
4478
+ reject(
4479
+ new Error(
4480
+ signal ? `machine mount controller exited from ${signal}` : `machine mount controller exited with code ${code ?? "unknown"}`
4481
+ )
4482
+ );
4483
+ };
4484
+ process.once("SIGINT", onSigint);
4485
+ process.once("SIGTERM", onSigterm);
4486
+ child.once("error", onError);
4487
+ child.once("exit", onExit);
4488
+ });
4489
+ }
4296
4490
  function printMountStartSummary(config, pid, mode) {
4297
4491
  console.log();
4298
- console.log(chalk8.dim(` PID: ${pid}`));
4299
- console.log(chalk8.dim(` Root: ${config.rootPath}`));
4300
- console.log(chalk8.dim(` SSH alias: ${config.alias}`));
4301
- console.log(chalk8.dim(` Poll: ${config.pollIntervalMs}ms`));
4492
+ console.log(chalk9.dim(` PID: ${pid}`));
4493
+ console.log(chalk9.dim(` Root: ${config.rootPath}`));
4494
+ console.log(chalk9.dim(` SSH alias: ${config.alias}`));
4495
+ console.log(chalk9.dim(` Poll: ${config.pollIntervalMs}ms`));
4302
4496
  console.log();
4303
4497
  console.log(
4304
- chalk8.dim(
4498
+ chalk9.dim(
4305
4499
  mode === "background" ? " Use `computer mount status` to inspect sync state." : " Press Ctrl-C to stop syncing."
4306
4500
  )
4307
4501
  );
@@ -4333,35 +4527,35 @@ function revealMountRootInFinder(rootPath) {
4333
4527
  function formatMountState(state) {
4334
4528
  switch (state) {
4335
4529
  case "mounted":
4336
- return chalk8.green(state);
4530
+ return chalk9.green(state);
4337
4531
  case "reconnecting":
4338
- return chalk8.cyan(state);
4532
+ return chalk9.cyan(state);
4339
4533
  case "degraded":
4340
4534
  case "pending":
4341
- return chalk8.yellow(state);
4535
+ return chalk9.yellow(state);
4342
4536
  default:
4343
- return chalk8.red(state);
4537
+ return chalk9.red(state);
4344
4538
  }
4345
4539
  }
4346
4540
 
4347
4541
  // src/commands/logout.ts
4348
- import { Command as Command11 } from "commander";
4349
- import chalk9 from "chalk";
4350
- var logoutCommand = new Command11("logout").description("Remove stored API key").action(() => {
4542
+ import { Command as Command12 } from "commander";
4543
+ import chalk10 from "chalk";
4544
+ var logoutCommand = new Command12("logout").description("Remove stored API key").action(() => {
4351
4545
  if (!getStoredAPIKey()) {
4352
4546
  console.log();
4353
- console.log(chalk9.dim(" Not logged in."));
4547
+ console.log(chalk10.dim(" Not logged in."));
4354
4548
  if (hasEnvAPIKey()) {
4355
- console.log(chalk9.dim(" Environment API key is still active in this shell."));
4549
+ console.log(chalk10.dim(" Environment API key is still active in this shell."));
4356
4550
  }
4357
4551
  console.log();
4358
4552
  return;
4359
4553
  }
4360
4554
  clearAPIKey();
4361
4555
  console.log();
4362
- console.log(chalk9.green(" Logged out."));
4556
+ console.log(chalk10.green(" Logged out."));
4363
4557
  if (hasEnvAPIKey()) {
4364
- console.log(chalk9.dim(" Environment API key is still active in this shell."));
4558
+ console.log(chalk10.dim(" Environment API key is still active in this shell."));
4365
4559
  }
4366
4560
  console.log();
4367
4561
  });
@@ -4369,27 +4563,12 @@ var logoutCommand = new Command11("logout").description("Remove stored API key")
4369
4563
  // src/commands/upgrade.ts
4370
4564
  import { spawnSync } from "child_process";
4371
4565
  import { readFileSync as readFileSync2, realpathSync } from "fs";
4372
- import { Command as Command12 } from "commander";
4373
- import chalk10 from "chalk";
4566
+ import { Command as Command13 } from "commander";
4567
+ import chalk11 from "chalk";
4374
4568
  import ora10 from "ora";
4375
4569
  var pkg2 = JSON.parse(
4376
4570
  readFileSync2(new URL("../package.json", import.meta.url), "utf8")
4377
4571
  );
4378
- function normalizeVersion(version) {
4379
- return version.split("-")[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isNaN(part) ? 0 : part);
4380
- }
4381
- function compareVersions(a, b) {
4382
- const left = normalizeVersion(a);
4383
- const right = normalizeVersion(b);
4384
- const size = Math.max(left.length, right.length);
4385
- for (let index = 0; index < size; index += 1) {
4386
- const diff = (left[index] ?? 0) - (right[index] ?? 0);
4387
- if (diff !== 0) {
4388
- return diff;
4389
- }
4390
- }
4391
- return 0;
4392
- }
4393
4572
  function resolveExecutablePath() {
4394
4573
  const candidate = process.argv[1] || process.execPath;
4395
4574
  try {
@@ -4437,8 +4616,9 @@ function findNixProfileElement(executablePath) {
4437
4616
  }
4438
4617
  return null;
4439
4618
  }
4440
- function resolveUpgradeCommand(method, executablePath) {
4619
+ function resolveUpgradeCommand(method, executablePath, targetVersion) {
4441
4620
  const packageName = pkg2.name ?? "aicomputer";
4621
+ const packageSpec = `${packageName}@${targetVersion}`;
4442
4622
  switch (method) {
4443
4623
  case "nix": {
4444
4624
  const element = findNixProfileElement(executablePath);
@@ -4456,36 +4636,52 @@ function resolveUpgradeCommand(method, executablePath) {
4456
4636
  case "pnpm":
4457
4637
  return {
4458
4638
  command: "pnpm",
4459
- args: ["add", "-g", `${packageName}@latest`],
4460
- label: `pnpm add -g ${packageName}@latest`
4639
+ args: ["add", "-g", packageSpec],
4640
+ label: `pnpm add -g ${packageSpec}`
4461
4641
  };
4462
4642
  case "yarn":
4463
4643
  return {
4464
4644
  command: "yarn",
4465
- args: ["global", "add", `${packageName}@latest`],
4466
- label: `yarn global add ${packageName}@latest`
4645
+ args: ["global", "add", packageSpec],
4646
+ label: `yarn global add ${packageSpec}`
4467
4647
  };
4468
4648
  case "npm":
4469
4649
  case "unknown":
4470
4650
  return {
4471
4651
  command: "npm",
4472
- args: ["install", "-g", `${packageName}@latest`],
4473
- label: `npm install -g ${packageName}@latest`
4652
+ args: ["install", "-g", packageSpec],
4653
+ label: `npm install -g ${packageSpec}`
4474
4654
  };
4475
4655
  }
4476
4656
  }
4477
4657
  async function getLatestVersion(packageName) {
4478
- const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
4658
+ const response = await fetch(`https://registry.npmjs.org/${packageName}`);
4479
4659
  if (!response.ok) {
4480
4660
  throw new Error(`Failed to check npm registry (${response.status})`);
4481
4661
  }
4482
- const payload = await response.json();
4483
- if (!payload.version) {
4484
- throw new Error("Registry response missing version");
4662
+ return resolveLatestPublishedVersion(
4663
+ await response.json()
4664
+ );
4665
+ }
4666
+ function attemptBundledMutagenRefresh(executablePath) {
4667
+ const install = spawnSync(
4668
+ process.execPath,
4669
+ [executablePath, "internal-install-mutagen", "--quiet"],
4670
+ {
4671
+ stdio: "ignore"
4672
+ }
4673
+ );
4674
+ if (install.status === 0) {
4675
+ return;
4485
4676
  }
4486
- return payload.version;
4677
+ console.log();
4678
+ console.log(
4679
+ chalk11.yellow(
4680
+ " CLI updated, but bundled Mutagen did not finish installing. `computer mount` will retry on first use."
4681
+ )
4682
+ );
4487
4683
  }
4488
- var upgradeCommand = new Command12("upgrade").description("Update the CLI to the latest version").action(async () => {
4684
+ var upgradeCommand = new Command13("upgrade").description("Update the CLI to the latest version").action(async () => {
4489
4685
  const currentVersion = pkg2.version ?? "0.0.0";
4490
4686
  const packageName = pkg2.name ?? "aicomputer";
4491
4687
  const spinner = ora10("Checking for updates...").start();
@@ -4507,7 +4703,7 @@ var upgradeCommand = new Command12("upgrade").description("Update the CLI to the
4507
4703
  const method = detectInstallMethod(executablePath);
4508
4704
  let upgrade;
4509
4705
  try {
4510
- upgrade = resolveUpgradeCommand(method, executablePath);
4706
+ upgrade = resolveUpgradeCommand(method, executablePath, latestVersion);
4511
4707
  } catch (error) {
4512
4708
  spinner.fail(
4513
4709
  error instanceof Error ? error.message : "Failed to prepare upgrade"
@@ -4518,16 +4714,19 @@ var upgradeCommand = new Command12("upgrade").description("Update the CLI to the
4518
4714
  spinner.stop();
4519
4715
  console.log();
4520
4716
  console.log(
4521
- chalk10.dim(` Updating ${chalk10.bold(`v${currentVersion}`)} -> ${chalk10.bold(`v${latestVersion}`)}`)
4717
+ chalk11.dim(` Updating ${chalk11.bold(`v${currentVersion}`)} -> ${chalk11.bold(`v${latestVersion}`)}`)
4522
4718
  );
4523
- console.log(chalk10.dim(` ${upgrade.label}`));
4719
+ console.log(chalk11.dim(` ${upgrade.label}`));
4524
4720
  console.log();
4525
4721
  const result = spawnSync(upgrade.command, upgrade.args, {
4526
4722
  stdio: "inherit"
4527
4723
  });
4528
4724
  if (result.status === 0) {
4725
+ if (method !== "nix") {
4726
+ attemptBundledMutagenRefresh(executablePath);
4727
+ }
4529
4728
  console.log();
4530
- console.log(chalk10.green(` Updated to v${latestVersion}.`));
4729
+ console.log(chalk11.green(` Updated to v${latestVersion}.`));
4531
4730
  console.log();
4532
4731
  return;
4533
4732
  }
@@ -4535,10 +4734,10 @@ var upgradeCommand = new Command12("upgrade").description("Update the CLI to the
4535
4734
  });
4536
4735
 
4537
4736
  // src/commands/whoami.ts
4538
- import { Command as Command13 } from "commander";
4539
- import chalk11 from "chalk";
4737
+ import { Command as Command14 } from "commander";
4738
+ import chalk12 from "chalk";
4540
4739
  import ora11 from "ora";
4541
- var whoamiCommand = new Command13("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
4740
+ var whoamiCommand = new Command14("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
4542
4741
  const spinner = options.json ? null : ora11("Loading user...").start();
4543
4742
  try {
4544
4743
  const me = await api("/v1/me");
@@ -4548,14 +4747,14 @@ var whoamiCommand = new Command13("whoami").description("Show current user").opt
4548
4747
  return;
4549
4748
  }
4550
4749
  console.log();
4551
- console.log(` ${chalk11.bold.white(me.user.display_name || me.user.email)}`);
4750
+ console.log(` ${chalk12.bold.white(me.user.display_name || me.user.email)}`);
4552
4751
  if (me.user.display_name) {
4553
- console.log(` ${chalk11.dim(me.user.email)}`);
4752
+ console.log(` ${chalk12.dim(me.user.email)}`);
4554
4753
  }
4555
4754
  if (me.api_key.name) {
4556
- console.log(` ${chalk11.dim("Key:")} ${me.api_key.name}`);
4755
+ console.log(` ${chalk12.dim("Key:")} ${me.api_key.name}`);
4557
4756
  }
4558
- console.log(` ${chalk11.dim("API:")} ${chalk11.dim(getBaseURL())}`);
4757
+ console.log(` ${chalk12.dim("API:")} ${chalk12.dim(getBaseURL())}`);
4559
4758
  console.log();
4560
4759
  } catch (error) {
4561
4760
  if (spinner) {
@@ -4572,15 +4771,15 @@ var pkg3 = JSON.parse(
4572
4771
  readFileSync3(new URL("../package.json", import.meta.url), "utf8")
4573
4772
  );
4574
4773
  var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
4575
- var program = new Command14();
4774
+ var program = new Command15();
4576
4775
  function appendTextSection(lines, title, values) {
4577
4776
  if (values.length === 0) {
4578
4777
  return;
4579
4778
  }
4580
- lines.push(` ${chalk12.dim(title)}`);
4779
+ lines.push(` ${chalk13.dim(title)}`);
4581
4780
  lines.push("");
4582
4781
  for (const value of values) {
4583
- lines.push(` ${chalk12.white(value)}`);
4782
+ lines.push(` ${chalk13.white(value)}`);
4584
4783
  }
4585
4784
  lines.push("");
4586
4785
  }
@@ -4589,10 +4788,10 @@ function appendTableSection(lines, title, entries) {
4589
4788
  return;
4590
4789
  }
4591
4790
  const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
4592
- lines.push(` ${chalk12.dim(title)}`);
4791
+ lines.push(` ${chalk13.dim(title)}`);
4593
4792
  lines.push("");
4594
4793
  for (const entry of entries) {
4595
- lines.push(` ${chalk12.white(padEnd(entry.term, width))}${chalk12.dim(entry.desc)}`);
4794
+ lines.push(` ${chalk13.white(padEnd(entry.term, width))}${chalk13.dim(entry.desc)}`);
4596
4795
  }
4597
4796
  lines.push("");
4598
4797
  }
@@ -4618,10 +4817,10 @@ function formatRootHelp(cmd) {
4618
4817
  ["Other", []]
4619
4818
  ];
4620
4819
  const otherGroup = groups.find(([name]) => name === "Other")[1];
4621
- lines.push(`${chalk12.bold(cliName)} ${chalk12.dim(`v${version}`)}`);
4820
+ lines.push(`${chalk13.bold(cliName)} ${chalk13.dim(`v${version}`)}`);
4622
4821
  lines.push("");
4623
4822
  if (cmd.description()) {
4624
- lines.push(` ${chalk12.dim(cmd.description())}`);
4823
+ lines.push(` ${chalk13.dim(cmd.description())}`);
4625
4824
  lines.push("");
4626
4825
  }
4627
4826
  appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
@@ -4674,10 +4873,10 @@ function formatSubcommandHelp(cmd, helper) {
4674
4873
  term: helper.optionTerm(option),
4675
4874
  desc: helper.optionDescription(option)
4676
4875
  }));
4677
- lines.push(chalk12.bold(commandPath(cmd)));
4876
+ lines.push(chalk13.bold(commandPath(cmd)));
4678
4877
  lines.push("");
4679
4878
  if (description) {
4680
- lines.push(` ${chalk12.dim(description)}`);
4879
+ lines.push(` ${chalk13.dim(description)}`);
4681
4880
  lines.push("");
4682
4881
  }
4683
4882
  appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
@@ -4703,6 +4902,7 @@ function applyHelpFormatting(cmd) {
4703
4902
  program.name(cliName).description("Agent Computer CLI").version(pkg3.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts");
4704
4903
  program.addCommand(loginCommand);
4705
4904
  program.addCommand(upgradeCommand);
4905
+ program.addCommand(internalInstallMutagenCommand, { hidden: true });
4706
4906
  program.addCommand(logoutCommand);
4707
4907
  program.addCommand(whoamiCommand);
4708
4908
  program.addCommand(claudeLoginCommand);