@mifune/agro 0.11.0 → 0.12.1

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 (3) hide show
  1. package/dist/agro.js +1083 -151
  2. package/dist/oh.js +1083 -151
  3. package/package.json +1 -1
package/dist/oh.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { fileURLToPath } from "node:url";
5
- import { dirname as dirname8, join as join10, resolve as resolve10 } from "node:path";
6
- import { existsSync as existsSync16, mkdtempSync as mkdtempSync3, readFileSync as readFileSync11, rmSync as rmSync3 } from "node:fs";
5
+ import { dirname as dirname10, join as join14, resolve as resolve14 } from "node:path";
6
+ import { existsSync as existsSync20, mkdtempSync as mkdtempSync4, readFileSync as readFileSync12, rmSync as rmSync4 } from "node:fs";
7
7
  import { tmpdir as tmpdir3 } from "node:os";
8
8
 
9
9
  // src/commands/update.ts
@@ -230,6 +230,17 @@ function resolveRegistryHome(env = process.env, home = homedir(), warn) {
230
230
  if (configured !== void 0) return { path: resolve(configured), configured: true };
231
231
  return { path: home, configured: false };
232
232
  }
233
+ function generationUserStateHome(generation, env, home, warn) {
234
+ const configured = aliasedEnvValue(env, "HOME", warn);
235
+ if (configured !== void 0) return resolve(configured);
236
+ return join(home, GENERATIONS[generation].userStateDir);
237
+ }
238
+ function resolveAgroUserStateHome(env = process.env, home = homedir(), warn) {
239
+ return generationUserStateHome("agro", env, home, warn);
240
+ }
241
+ function resolveLegacyUserStateHome(env = process.env, home = homedir(), warn) {
242
+ return generationUserStateHome("legacy", env, home, warn);
243
+ }
233
244
  function resolveUserStateHome(env = process.env, home = homedir(), warn) {
234
245
  const configured = aliasedEnvValue(env, "HOME", warn);
235
246
  if (configured !== void 0) return resolve(configured);
@@ -1959,8 +1970,8 @@ function redact(token) {
1959
1970
  async function ask(question) {
1960
1971
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1961
1972
  try {
1962
- return await new Promise((resolve11) => {
1963
- rl.question(` ${question} `, (answer) => resolve11(answer.trim()));
1973
+ return await new Promise((resolve15) => {
1974
+ rl.question(` ${question} `, (answer) => resolve15(answer.trim()));
1964
1975
  });
1965
1976
  } finally {
1966
1977
  rl.close();
@@ -1972,7 +1983,7 @@ async function askSecret(question) {
1972
1983
  process.stdin.setRawMode(true);
1973
1984
  process.stdin.resume();
1974
1985
  const bytes = [];
1975
- return await new Promise((resolve11) => {
1986
+ return await new Promise((resolve15) => {
1976
1987
  const cleanup = () => {
1977
1988
  try {
1978
1989
  process.stdin.setRawMode(false);
@@ -1990,7 +2001,7 @@ async function askSecret(question) {
1990
2001
  } else if (byte === 13 || byte === 10) {
1991
2002
  cleanup();
1992
2003
  process.stdout.write("\n");
1993
- resolve11(Buffer.from(bytes).toString("utf8").trim());
2004
+ resolve15(Buffer.from(bytes).toString("utf8").trim());
1994
2005
  return;
1995
2006
  } else if (byte === 127 || byte === 8) {
1996
2007
  if (bytes.length > 0) {
@@ -2040,10 +2051,10 @@ async function askSecretPiped(question) {
2040
2051
  };
2041
2052
  process.on("SIGINT", onSigint);
2042
2053
  try {
2043
- return await new Promise((resolve11) => {
2054
+ return await new Promise((resolve15) => {
2044
2055
  rl.question(` ${question} `, (answer) => {
2045
2056
  process.stdout.write("\n");
2046
- resolve11(answer.trim());
2057
+ resolve15(answer.trim());
2047
2058
  });
2048
2059
  });
2049
2060
  } finally {
@@ -2976,6 +2987,20 @@ function defaultIdentity() {
2976
2987
  return { name: info.username, uid: info.uid };
2977
2988
  }
2978
2989
 
2990
+ // src/lib/execution/status.ts
2991
+ var RUNTIME_ABSENT_STATUS = "no container runtime";
2992
+ async function resolveTargetStatus(target) {
2993
+ try {
2994
+ return await target.status();
2995
+ } catch (err) {
2996
+ if (err instanceof ExecutionSpawnError) return RUNTIME_ABSENT_STATUS;
2997
+ throw err;
2998
+ }
2999
+ }
3000
+ function runtimeIsAbsent(status) {
3001
+ return status === RUNTIME_ABSENT_STATUS;
3002
+ }
3003
+
2979
3004
  // src/lib/execution/index.ts
2980
3005
  function resolveExecutionTarget(opts) {
2981
3006
  if (runningInsideSandbox(opts.env ?? process.env)) {
@@ -3984,6 +4009,11 @@ async function runSandboxList(opts, io) {
3984
4009
  return 0;
3985
4010
  }
3986
4011
 
4012
+ // src/commands/harness.ts
4013
+ import { existsSync as existsSync18 } from "node:fs";
4014
+ import { homedir as homedir2 } from "node:os";
4015
+ import { delimiter as delimiter2, join as join12, resolve as resolve12 } from "node:path";
4016
+
3987
4017
  // src/lib/docs.ts
3988
4018
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
3989
4019
  function sourceDocsUrl(docsPath) {
@@ -3994,7 +4024,194 @@ function sourceDocsUrl(docsPath) {
3994
4024
  return new URL(segments.map(encodeURIComponent).join("/"), SOURCE_DOCS_BASE).toString();
3995
4025
  }
3996
4026
 
4027
+ // src/lib/host-config.ts
4028
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4029
+ import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute2, join as join10, resolve as resolve10 } from "node:path";
4030
+ var HOST_CONFIG_MODE = 420;
4031
+ function hostConfigPath(env = process.env, home) {
4032
+ return join10(resolveAgroUserStateHome(env, home), GENERATIONS.agro.configFile);
4033
+ }
4034
+ function legacyHostConfigPath(env = process.env, home) {
4035
+ return join10(resolveLegacyUserStateHome(env, home), GENERATIONS.legacy.configFile);
4036
+ }
4037
+ function readableHostConfigPath(env, home) {
4038
+ const path4 = hostConfigPath(env, home);
4039
+ if (existsSync16(path4)) return path4;
4040
+ const legacy = legacyHostConfigPath(env, home);
4041
+ return existsSync16(legacy) ? legacy : path4;
4042
+ }
4043
+ function defaultHarnessRoot(env = process.env, home) {
4044
+ return resolveAgroUserStateHome(env, home);
4045
+ }
4046
+ function validateHostConfig(value) {
4047
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4048
+ throw new Error(`${GENERATIONS.agro.configFile}: must contain a JSON object`);
4049
+ }
4050
+ const record = value;
4051
+ if (record.version !== void 0 && record.version !== 1) {
4052
+ throw fieldError2("version", "must be 1");
4053
+ }
4054
+ const harnessRoot = record.harnessRoot;
4055
+ if (harnessRoot !== void 0) {
4056
+ if (typeof harnessRoot !== "string" || harnessRoot === "") {
4057
+ throw fieldError2("harnessRoot", "must be a non-empty string");
4058
+ }
4059
+ if (!isAbsolute2(harnessRoot)) {
4060
+ throw fieldError2("harnessRoot", "must be an absolute path");
4061
+ }
4062
+ }
4063
+ const hostHarnesses = record.hostHarnesses;
4064
+ if (hostHarnesses !== void 0) validateReceipts(hostHarnesses, "hostHarnesses", "harness");
4065
+ const hostTools = record.hostTools;
4066
+ if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool");
4067
+ return { ...record, version: 1 };
4068
+ }
4069
+ function absoluteField(value, path4) {
4070
+ if (typeof value !== "string" || value === "") {
4071
+ throw fieldError2(path4, "must be a non-empty string");
4072
+ }
4073
+ if (!isAbsolute2(value)) throw fieldError2(path4, "must be an absolute path");
4074
+ return value;
4075
+ }
4076
+ function validateReceipts(value, field, noun) {
4077
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4078
+ throw fieldError2(field, `must be a JSON object keyed by ${noun} id`);
4079
+ }
4080
+ for (const [id, receipt] of Object.entries(value)) {
4081
+ if (id === "") throw fieldError2(field, `must use a non-empty ${noun} id as each key`);
4082
+ const at = `${field}.${id}`;
4083
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
4084
+ throw fieldError2(at, "must be a JSON object");
4085
+ }
4086
+ const entry = receipt;
4087
+ absoluteField(entry.prefix, `${at}.prefix`);
4088
+ absoluteField(entry.binPath, `${at}.binPath`);
4089
+ if (typeof entry.binary !== "string" || entry.binary === "") {
4090
+ throw fieldError2(`${at}.binary`, "must be a non-empty string");
4091
+ }
4092
+ if (typeof entry.installedAt !== "string" || entry.installedAt === "") {
4093
+ throw fieldError2(`${at}.installedAt`, "must be a non-empty string");
4094
+ }
4095
+ if (entry.workspaceRoot !== void 0) {
4096
+ absoluteField(entry.workspaceRoot, `${at}.workspaceRoot`);
4097
+ }
4098
+ }
4099
+ }
4100
+ function readHostConfig(env = process.env, home) {
4101
+ const path4 = readableHostConfigPath(env, home);
4102
+ if (!existsSync16(path4)) return { version: 1 };
4103
+ let raw;
4104
+ try {
4105
+ raw = readFileSync11(path4, "utf8");
4106
+ } catch (error) {
4107
+ const detail = error instanceof Error ? error.message : String(error);
4108
+ throw new Error(`could not read ${path4}: ${detail}`);
4109
+ }
4110
+ let parsed;
4111
+ try {
4112
+ parsed = JSON.parse(raw);
4113
+ } catch {
4114
+ throw new Error(`${basename6(path4)} is not valid JSON: ${path4}`);
4115
+ }
4116
+ return validateHostConfig(parsed);
4117
+ }
4118
+ function writeHostConfig(config, env = process.env, home) {
4119
+ const path4 = hostConfigPath(env, home);
4120
+ const validated = validateHostConfig({ ...config, version: 1 });
4121
+ const body = `${JSON.stringify(validated, null, 2)}
4122
+ `;
4123
+ const tmp = `${path4}.tmp.${process.pid}`;
4124
+ mkdirSync7(dirname8(path4), { recursive: true });
4125
+ try {
4126
+ writeFileSync8(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4127
+ renameSync5(tmp, path4);
4128
+ retireLegacyHostConfig(env, home, path4);
4129
+ } catch (error) {
4130
+ try {
4131
+ unlinkSync5(tmp);
4132
+ } catch {
4133
+ }
4134
+ throw error;
4135
+ }
4136
+ }
4137
+ function resolveHarnessRoot(explicit, env = process.env, home) {
4138
+ if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4139
+ const configured = readHostConfig(env, home).harnessRoot;
4140
+ if (typeof configured === "string" && configured !== "") return resolve10(configured);
4141
+ return defaultHarnessRoot(env, home);
4142
+ }
4143
+ function retireLegacyHostConfig(env, home, written) {
4144
+ const legacy = legacyHostConfigPath(env, home);
4145
+ if (legacy === written || !existsSync16(legacy)) return;
4146
+ unlinkSync5(legacy);
4147
+ }
4148
+ function fieldError2(path4, requirement) {
4149
+ return new Error(`${GENERATIONS.agro.configFile}: ${path4} ${requirement}`);
4150
+ }
4151
+
4152
+ // src/lib/host-workspace.ts
4153
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, readdirSync as readdirSync4, renameSync as renameSync6, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
4154
+ import { dirname as dirname9, join as join11, resolve as resolve11 } from "node:path";
4155
+ var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4156
+ var IGNORABLE_ENTRIES = [
4157
+ REGISTRY_SUBDIR,
4158
+ "escalate",
4159
+ GENERATIONS.agro.configFile,
4160
+ GENERATIONS.legacy.configFile
4161
+ ];
4162
+ function isIgnorableEntry(name) {
4163
+ return name.startsWith(".") || IGNORABLE_ENTRIES.includes(name);
4164
+ }
4165
+ function assertCloned(result, root) {
4166
+ const status = result.error ? result.error.code ?? "spawn failed" : result.status;
4167
+ if (result.error || result.status !== 0) {
4168
+ throw new Error(`could not clone ${AGRO_REPO_URL} into ${root} (exit status ${String(status)})`);
4169
+ }
4170
+ }
4171
+ function cloneInto(target, run, root) {
4172
+ assertCloned(run("git", ["clone", AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4173
+ }
4174
+ function cloneThroughStaging(root, run) {
4175
+ const staging = mkdtempSync3(join11(dirname9(root), "agro-clone-"));
4176
+ try {
4177
+ const checkout = join11(staging, "agro");
4178
+ cloneInto(checkout, run, root);
4179
+ for (const name of readdirSync4(checkout)) {
4180
+ const destination = join11(root, name);
4181
+ if (existsSync17(destination)) {
4182
+ throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4183
+ }
4184
+ renameSync6(join11(checkout, name), destination);
4185
+ }
4186
+ } finally {
4187
+ rmSync3(staging, { recursive: true, force: true });
4188
+ }
4189
+ }
4190
+ function ensureHostWorkspace(path4, run = spawnRunner) {
4191
+ const root = resolve11(path4);
4192
+ if (existsSync17(join11(root, ".git"))) return { root, action: "reused" };
4193
+ const stats = statSync5(root, { throwIfNoEntry: false });
4194
+ if (stats === void 0) {
4195
+ mkdirSync8(dirname9(root), { recursive: true });
4196
+ cloneInto(root, run, root);
4197
+ return { root, action: "cloned" };
4198
+ }
4199
+ if (!stats.isDirectory()) {
4200
+ throw new Error(`${root} exists and is not a directory \u2014 choose another harness root`);
4201
+ }
4202
+ const blocking = readdirSync4(root).filter((name) => !isIgnorableEntry(name));
4203
+ if (blocking.length > 0) {
4204
+ throw new Error(
4205
+ `${root} holds files but no git checkout (${blocking.sort().join(", ")}) \u2014 choose an empty directory or an existing AGRO checkout`
4206
+ );
4207
+ }
4208
+ cloneThroughStaging(root, run);
4209
+ return { root, action: "cloned" };
4210
+ }
4211
+
3997
4212
  // src/lib/harnesses/catalog.ts
4213
+ var SANDBOX_HARNESS_PREFIX = "/home/sandbox/.local";
4214
+ var HARNESS_PREFIX_TOKEN = "{{prefix}}";
3998
4215
  var HARNESS_CATALOG = [
3999
4216
  {
4000
4217
  id: "claude-code",
@@ -4003,13 +4220,14 @@ var HARNESS_CATALOG = [
4003
4220
  installArgv: [
4004
4221
  "npm",
4005
4222
  "--prefix",
4006
- "/home/sandbox/.local",
4223
+ HARNESS_PREFIX_TOKEN,
4007
4224
  "install",
4008
4225
  "-g",
4009
4226
  "@anthropic-ai/claude-code"
4010
4227
  ],
4011
4228
  installUser: "sandbox",
4012
4229
  verifyArgv: ["claude", "--version"],
4230
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@anthropic-ai/claude-code"],
4013
4231
  docsPath: "docs/harnesses/claude-code.md",
4014
4232
  kind: "installable"
4015
4233
  },
@@ -4020,13 +4238,14 @@ var HARNESS_CATALOG = [
4020
4238
  installArgv: [
4021
4239
  "npm",
4022
4240
  "--prefix",
4023
- "/home/sandbox/.local",
4241
+ HARNESS_PREFIX_TOKEN,
4024
4242
  "install",
4025
4243
  "-g",
4026
4244
  "@openai/codex"
4027
4245
  ],
4028
4246
  installUser: "sandbox",
4029
4247
  verifyArgv: ["codex", "--version"],
4248
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@openai/codex"],
4030
4249
  docsPath: "docs/harnesses/codex.md",
4031
4250
  kind: "installable"
4032
4251
  },
@@ -4037,7 +4256,7 @@ var HARNESS_CATALOG = [
4037
4256
  installArgv: [
4038
4257
  "npm",
4039
4258
  "--prefix",
4040
- "/home/sandbox/.local",
4259
+ HARNESS_PREFIX_TOKEN,
4041
4260
  "install",
4042
4261
  "-g",
4043
4262
  "--ignore-scripts",
@@ -4045,6 +4264,7 @@ var HARNESS_CATALOG = [
4045
4264
  ],
4046
4265
  installUser: "sandbox",
4047
4266
  verifyArgv: ["pi", "--version"],
4267
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@earendil-works/pi-coding-agent"],
4048
4268
  docsPath: "docs/harnesses/pi.md",
4049
4269
  kind: "installable"
4050
4270
  },
@@ -4055,13 +4275,14 @@ var HARNESS_CATALOG = [
4055
4275
  installArgv: [
4056
4276
  "npm",
4057
4277
  "--prefix",
4058
- "/home/sandbox/.local",
4278
+ HARNESS_PREFIX_TOKEN,
4059
4279
  "install",
4060
4280
  "-g",
4061
4281
  "opencode-ai"
4062
4282
  ],
4063
4283
  installUser: "sandbox",
4064
4284
  verifyArgv: ["opencode", "--version"],
4285
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "opencode-ai"],
4065
4286
  docsPath: "docs/harnesses/opencode.md",
4066
4287
  kind: "installable"
4067
4288
  },
@@ -4072,10 +4293,11 @@ var HARNESS_CATALOG = [
4072
4293
  installArgv: [
4073
4294
  "bash",
4074
4295
  "-lc",
4075
- 'curl -fsSL https://x.ai/cli/install.sh | GROK_BIN_DIR="$HOME/.local/bin" bash -s 0.2.39 && rm -f "$HOME/.local/bin/agent"'
4296
+ `curl -fsSL https://x.ai/cli/install.sh | GROK_BIN_DIR="${HARNESS_PREFIX_TOKEN}/bin" bash -s 0.2.39 && rm -f "${HARNESS_PREFIX_TOKEN}/bin/agent"`
4076
4297
  ],
4077
4298
  installUser: "sandbox",
4078
4299
  verifyArgv: ["grok", "--version"],
4300
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/grok`],
4079
4301
  docsPath: "docs/harnesses/grok-build.md",
4080
4302
  kind: "installable"
4081
4303
  },
@@ -4086,10 +4308,11 @@ var HARNESS_CATALOG = [
4086
4308
  installArgv: [
4087
4309
  "bash",
4088
4310
  "-lc",
4089
- `curl -fsSL https://hermes-agent.nousresearch.com/install.sh | HERMES_INSTALL_DIR="$HOME/.local/lib/hermes-agent" bash -s -- --skip-setup --skip-browser && uv pip install --python "$HOME/.local/lib/hermes-agent/venv/bin/python" 'hermes-agent[slack,teams,web,pty]'`
4311
+ `curl -fsSL https://hermes-agent.nousresearch.com/install.sh | HERMES_INSTALL_DIR="${HARNESS_PREFIX_TOKEN}/lib/hermes-agent" bash -s -- --skip-setup --skip-browser && uv pip install --python "${HARNESS_PREFIX_TOKEN}/lib/hermes-agent/venv/bin/python" 'hermes-agent[slack,teams,web,pty]'`
4090
4312
  ],
4091
4313
  installUser: "sandbox",
4092
4314
  verifyArgv: ["hermes", "--version"],
4315
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/hermes`, `${HARNESS_PREFIX_TOKEN}/lib/hermes-agent`],
4093
4316
  docsPath: "docs/harnesses/hermes.md",
4094
4317
  kind: "installable"
4095
4318
  },
@@ -4100,10 +4323,11 @@ var HARNESS_CATALOG = [
4100
4323
  installArgv: [
4101
4324
  "bash",
4102
4325
  "-lc",
4103
- 'set -o pipefail; curl -fsSL https://dev.meta.ai/install.sh | MUSE_INSTALL_DIR="$HOME/.local/bin" MUSE_NO_MODIFY_PATH=1 MUSE_LOGIN=0 bash'
4326
+ `set -o pipefail; curl -fsSL https://dev.meta.ai/install.sh | MUSE_INSTALL_DIR="${HARNESS_PREFIX_TOKEN}/bin" MUSE_NO_MODIFY_PATH=1 MUSE_LOGIN=0 bash`
4104
4327
  ],
4105
4328
  installUser: "sandbox",
4106
4329
  verifyArgv: ["muse", "--version"],
4330
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/muse`],
4107
4331
  docsPath: "docs/harnesses/muse-code.md",
4108
4332
  kind: "installable"
4109
4333
  },
@@ -4114,10 +4338,11 @@ var HARNESS_CATALOG = [
4114
4338
  installArgv: [
4115
4339
  "bash",
4116
4340
  "-lc",
4117
- 'set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir "$HOME/.local/bin"'
4341
+ `set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir "${HARNESS_PREFIX_TOKEN}/bin"`
4118
4342
  ],
4119
4343
  installUser: "sandbox",
4120
4344
  verifyArgv: ["agy", "--version"],
4345
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/agy`],
4121
4346
  docsPath: "docs/harnesses/antigravity-cli.md",
4122
4347
  kind: "installable"
4123
4348
  },
@@ -4128,6 +4353,7 @@ var HARNESS_CATALOG = [
4128
4353
  installArgv: ["npx", "--yes", "t3", "--version"],
4129
4354
  installUser: "sandbox",
4130
4355
  verifyArgv: ["npx", "--no-install", "t3", "--version"],
4356
+ uninstallArgv: null,
4131
4357
  docsPath: "docs/harnesses/t3code.md",
4132
4358
  kind: "on-demand"
4133
4359
  }
@@ -4138,6 +4364,21 @@ function findHarness(id) {
4138
4364
  function harnessIds() {
4139
4365
  return HARNESS_CATALOG.map((h) => h.id);
4140
4366
  }
4367
+ function substitute(argv, prefix) {
4368
+ return argv.map((part) => part.split(HARNESS_PREFIX_TOKEN).join(prefix));
4369
+ }
4370
+ function resolveInstallArgv(entry, prefix) {
4371
+ return substitute(entry.installArgv, prefix);
4372
+ }
4373
+ function resolveVerifyArgv(entry, prefix) {
4374
+ return substitute(entry.verifyArgv, prefix);
4375
+ }
4376
+ function resolveUninstallArgv(entry, prefix) {
4377
+ return entry.uninstallArgv === null ? null : substitute(entry.uninstallArgv, prefix);
4378
+ }
4379
+ function harnessBinPath(prefix) {
4380
+ return `${prefix}/bin`;
4381
+ }
4141
4382
 
4142
4383
  // src/commands/harness.ts
4143
4384
  var PROBE_TIMEOUT_MS = 15e3;
@@ -4153,11 +4394,31 @@ function targetFor(root, run, env) {
4153
4394
  ...env ? { env } : {}
4154
4395
  });
4155
4396
  }
4156
- async function probeInstalled(target, entry, env) {
4397
+ function hostPrefix(home) {
4398
+ return join12(home, ".local");
4399
+ }
4400
+ function pathEntries(env) {
4401
+ return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
4402
+ }
4403
+ function onPath(prefix, env) {
4404
+ return pathEntries(env).includes(harnessBinPath(prefix));
4405
+ }
4406
+ function hostTargetFor(root, prefix, run, env) {
4407
+ const path4 = [harnessBinPath(prefix), ...pathEntries(env)].join(delimiter2);
4408
+ return new LocalExecutionTarget({
4409
+ projectRoot: root,
4410
+ run,
4411
+ env: { ...env, PATH: path4 }
4412
+ });
4413
+ }
4414
+ function messageOf(error) {
4415
+ return error instanceof Error ? error.message : String(error);
4416
+ }
4417
+ async function probeInstalled(target, entry, prefix, user, env) {
4157
4418
  try {
4158
4419
  const r = await target.exec({
4159
- argv: [...entry.verifyArgv],
4160
- user: "sandbox",
4420
+ argv: resolveVerifyArgv(entry, prefix),
4421
+ ...user ? { user } : {},
4161
4422
  stdio: "capture",
4162
4423
  timeoutMs: PROBE_TIMEOUT_MS,
4163
4424
  ...env ? { env } : {}
@@ -4168,33 +4429,58 @@ async function probeInstalled(target, entry, env) {
4168
4429
  throw err;
4169
4430
  }
4170
4431
  }
4171
- async function collectStates(root, run, env, only) {
4432
+ function stateOf(entry, installed, location) {
4433
+ return {
4434
+ id: entry.id,
4435
+ title: entry.title,
4436
+ binary: entry.binary,
4437
+ kind: entry.kind,
4438
+ installed,
4439
+ location,
4440
+ docs: sourceDocsUrl(entry.docsPath)
4441
+ };
4442
+ }
4443
+ function probeableHost(env, home) {
4444
+ let root;
4445
+ try {
4446
+ root = resolveHarnessRoot(void 0, env);
4447
+ } catch {
4448
+ return void 0;
4449
+ }
4450
+ const prefix = hostPrefix(home);
4451
+ if (!existsSync18(join12(root, ".git")) && !existsSync18(prefix)) return void 0;
4452
+ return { root, prefix };
4453
+ }
4454
+ async function collectStates(root, run, home, env, only) {
4172
4455
  const entries = only ? [...only] : [...HARNESS_CATALOG];
4173
4456
  const target = targetFor(root, run, env);
4174
- let reachable = false;
4175
- try {
4176
- reachable = isReachable(await target.status());
4177
- } catch (err) {
4178
- if (!(err instanceof ExecutionSpawnError)) throw err;
4457
+ const reachable = isReachable(await resolveTargetStatus(target));
4458
+ if (reachable) {
4459
+ const states2 = [];
4460
+ for (const entry of entries) {
4461
+ const installed = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox");
4462
+ states2.push(stateOf(entry, installed, installed === null ? "unknown" : "sandbox"));
4463
+ }
4464
+ return { states: states2 };
4465
+ }
4466
+ const host = probeableHost(env ?? process.env, home);
4467
+ if (host === void 0) {
4468
+ return { states: entries.map((entry) => stateOf(entry, null, "unknown")) };
4179
4469
  }
4470
+ const hostTarget = hostTargetFor(host.root, host.prefix, run, env ?? process.env);
4180
4471
  const states = [];
4181
4472
  for (const entry of entries) {
4182
- states.push({
4183
- id: entry.id,
4184
- title: entry.title,
4185
- binary: entry.binary,
4186
- kind: entry.kind,
4187
- installed: reachable ? await probeInstalled(target, entry) : null,
4188
- docs: sourceDocsUrl(entry.docsPath)
4189
- });
4473
+ const installed = await probeInstalled(hostTarget, entry, host.prefix, void 0);
4474
+ states.push(stateOf(entry, installed, installed === null ? "unknown" : "host"));
4190
4475
  }
4191
- return states;
4476
+ return { states, hostPrefix: host.prefix };
4192
4477
  }
4193
4478
  function cell(value, absent) {
4194
4479
  if (value === null) return absent;
4195
4480
  return value ? "yes" : "no";
4196
4481
  }
4197
- function renderTable(states, io, bin) {
4482
+ function renderTable(collected, io, bin) {
4483
+ const states = collected.states;
4198
4484
  const header2 = ["HARNESS", "KIND", "INSTALLED"];
4199
4485
  const rows = states.map((s) => [s.id, s.kind, cell(s.installed, "?")]);
4200
4486
  const widths = header2.map(
@@ -4203,6 +4489,14 @@ function renderTable(states, io, bin) {
4203
4489
  const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
4204
4490
  io.stdout(line(header2));
4205
4491
  for (const row of rows) io.stdout(line(row));
4492
+ if (states.some((s) => s.location === "host")) {
4493
+ io.stdout(
4494
+ `
4495
+ INSTALLED reports the host prefix ${collected.hostPrefix} \u2014 the sandbox is not running.
4496
+ `
4497
+ );
4498
+ return;
4499
+ }
4206
4500
  if (states.some((s) => s.installed === null)) {
4207
4501
  io.stdout(`
4208
4502
  INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
@@ -4212,12 +4506,12 @@ INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} san
4212
4506
  async function runHarnessList(opts, io) {
4213
4507
  const run = opts.run ?? spawnRunner;
4214
4508
  const root = resolveProjectRoot(opts.cwd);
4215
- const states = await collectStates(root, run, opts.env);
4509
+ const collected = await collectStates(root, run, homeOf(opts), opts.env);
4216
4510
  if (opts.json) {
4217
- io.stdout(`${JSON.stringify(states, null, 2)}
4511
+ io.stdout(`${JSON.stringify(collected.states, null, 2)}
4218
4512
  `);
4219
4513
  } else {
4220
- renderTable(states, io, opts.bin);
4514
+ renderTable(collected, io, opts.bin);
4221
4515
  }
4222
4516
  return 0;
4223
4517
  }
@@ -4238,24 +4532,23 @@ async function runHarnessStatus(name, opts, io) {
4238
4532
  only = findHarness(name);
4239
4533
  if (!only) return unknownHarness(name, io, opts.bin);
4240
4534
  }
4241
- const states = await collectStates(root, run, opts.env, only ? [only] : void 0);
4535
+ const collected = await collectStates(root, run, homeOf(opts), opts.env, only ? [only] : void 0);
4242
4536
  if (opts.json) {
4243
- io.stdout(`${JSON.stringify(only ? states[0] : states, null, 2)}
4537
+ io.stdout(`${JSON.stringify(only ? collected.states[0] : collected.states, null, 2)}
4244
4538
  `);
4245
4539
  } else {
4246
- renderTable(states, io, opts.bin);
4540
+ renderTable(collected, io, opts.bin);
4247
4541
  }
4248
4542
  return 0;
4249
4543
  }
4250
4544
  function hermesTargetRoot(target) {
4251
4545
  return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
4252
4546
  }
4253
- async function reconcileHermes(target, io, bin) {
4254
- const root = hermesTargetRoot(target);
4547
+ async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
4255
4548
  const result = await target.exec({
4256
4549
  argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
4257
4550
  env: aliasedEnvPair("PROJECT_ROOT", root),
4258
- user: "sandbox",
4551
+ ...user ? { user } : {},
4259
4552
  stdio: "inherit"
4260
4553
  });
4261
4554
  if (result.exitCode !== 0) {
@@ -4264,6 +4557,245 @@ async function reconcileHermes(target, io, bin) {
4264
4557
  }
4265
4558
  return result.exitCode;
4266
4559
  }
4560
+ function unreachableReason(status) {
4561
+ return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
4562
+ }
4563
+ function sandboxRefusal(bin, status) {
4564
+ if (runtimeIsAbsent(status)) {
4565
+ return `${bin} harness: no container runtime is on PATH.
4566
+ `;
4567
+ }
4568
+ return `${bin} harness: the sandbox is not running (${status}).
4569
+ Start it with \`${bin} sandbox\`, then re-run this command.
4570
+ `;
4571
+ }
4572
+ function homeOf(opts) {
4573
+ return (opts.homedir ?? homedir2)();
4574
+ }
4575
+ function isInteractive(opts) {
4576
+ return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
4577
+ }
4578
+ async function installOnHost(entry, opts, io, run, status) {
4579
+ const bin = opts.bin;
4580
+ const env = opts.env ?? process.env;
4581
+ const flagged = opts.host === true || opts.path !== void 0;
4582
+ if (!flagged && !isInteractive(opts)) {
4583
+ io.stderr(
4584
+ sandboxRefusal(bin, status) + `Or install on the host with \`${bin} harness install ${entry.id} --host\`.
4585
+ `
4586
+ );
4587
+ return 1;
4588
+ }
4589
+ if (entry.kind === "on-demand") {
4590
+ io.stdout(`${entry.id}: no installation is needed \u2014 npx fetches it at each run
4591
+ `);
4592
+ return 0;
4593
+ }
4594
+ let root;
4595
+ let recorded;
4596
+ try {
4597
+ recorded = readHostConfig(env).harnessRoot;
4598
+ root = resolveHarnessRoot(opts.path, env);
4599
+ } catch (err) {
4600
+ io.stderr(`${bin} harness: ${messageOf(err)}
4601
+ `);
4602
+ return 1;
4603
+ }
4604
+ const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
4605
+ if (!flagged) {
4606
+ const ask2 = io.ask ?? ask;
4607
+ const answer = (await ask2(
4608
+ `${unreachableReason(status)} Install ${entry.title} on the host? [y/N]`
4609
+ )).trim().toLowerCase();
4610
+ if (!/^y/.test(answer)) {
4611
+ io.stderr(sandboxRefusal(bin, status));
4612
+ return 1;
4613
+ }
4614
+ if (!sticky) {
4615
+ const chosen = (await ask2(`Harness root [${root}]:`)).trim();
4616
+ if (chosen !== "") root = resolve12(chosen);
4617
+ }
4618
+ }
4619
+ if (sticky) io.stdout(`using the recorded harness root ${root}
4620
+ `);
4621
+ try {
4622
+ if (!existsSync18(join12(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4623
+ `);
4624
+ const workspace = ensureHostWorkspace(root, run);
4625
+ root = workspace.root;
4626
+ io.stdout(
4627
+ workspace.action === "cloned" ? `host workspace cloned into ${root}
4628
+ ` : `host workspace reused at ${root}
4629
+ `
4630
+ );
4631
+ } catch (err) {
4632
+ io.stderr(`${bin} harness: ${messageOf(err)}
4633
+ `);
4634
+ return 1;
4635
+ }
4636
+ const prefix = hostPrefix(homeOf(opts));
4637
+ const target = hostTargetFor(root, prefix, run, env);
4638
+ const hermes = entry.id === "hermes";
4639
+ const installEnv = hermes ? {
4640
+ ...aliasedEnvPair("PROJECT_ROOT", root),
4641
+ HERMES_HOME: `${root}/.hermes`
4642
+ } : void 0;
4643
+ if (hermes) {
4644
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4645
+ if (code !== 0) return code;
4646
+ }
4647
+ if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true) {
4648
+ io.stdout(`${entry.id}: already installed (${entry.binary})
4649
+ `);
4650
+ return 0;
4651
+ }
4652
+ io.stdout(`installing ${entry.title} on the host\u2026
4653
+ `);
4654
+ const r = await target.exec({
4655
+ argv: resolveInstallArgv(entry, prefix),
4656
+ stdio: "inherit",
4657
+ ...installEnv ? { env: installEnv } : {}
4658
+ });
4659
+ if (r.exitCode !== 0) {
4660
+ io.stderr(`${bin} harness: installing ${entry.id} failed (exit ${r.exitCode}).
4661
+ `);
4662
+ return r.exitCode;
4663
+ }
4664
+ if (hermes) {
4665
+ if (await probeInstalled(target, entry, prefix, void 0, installEnv) !== true) {
4666
+ io.stderr(`${bin} harness: Hermes installation finished but executable verification failed.
4667
+ `);
4668
+ return 1;
4669
+ }
4670
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4671
+ if (code !== 0) return code;
4672
+ }
4673
+ const receipt = {
4674
+ prefix,
4675
+ binary: entry.binary,
4676
+ binPath: harnessBinPath(prefix),
4677
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4678
+ workspaceRoot: root
4679
+ };
4680
+ try {
4681
+ const config = readHostConfig(env);
4682
+ writeHostConfig(
4683
+ {
4684
+ ...config,
4685
+ harnessRoot: root,
4686
+ hostHarnesses: { ...config.hostHarnesses ?? {}, [entry.id]: receipt }
4687
+ },
4688
+ env
4689
+ );
4690
+ } catch (err) {
4691
+ io.stderr(`${bin} harness: could not record the install: ${messageOf(err)}
4692
+ `);
4693
+ return 1;
4694
+ }
4695
+ io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
4696
+ `);
4697
+ if (!onPath(prefix, env)) {
4698
+ io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
4699
+ `);
4700
+ }
4701
+ io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
4702
+ `);
4703
+ return 0;
4704
+ }
4705
+ async function removeHarness(entry, target, prefix, user, opts, io) {
4706
+ const argv = resolveUninstallArgv(entry, prefix);
4707
+ if (argv === null) {
4708
+ io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4709
+ `);
4710
+ return { code: 0, dropReceipt: false };
4711
+ }
4712
+ if (await probeInstalled(target, entry, prefix, user) !== true) {
4713
+ io.stdout(`${entry.id}: not installed (${entry.binary})
4714
+ `);
4715
+ return { code: 0, dropReceipt: true };
4716
+ }
4717
+ if (isInteractive(opts)) {
4718
+ const ask2 = io.ask ?? ask;
4719
+ const answer = (await ask2(`Remove ${entry.title} from ${prefix}? [y/N]`)).trim().toLowerCase();
4720
+ if (!/^y/.test(answer)) {
4721
+ io.stderr(`${opts.bin} harness: removed nothing.
4722
+ `);
4723
+ return { code: 1, dropReceipt: false };
4724
+ }
4725
+ }
4726
+ io.stdout(`removing ${entry.title} from ${prefix}\u2026
4727
+ `);
4728
+ const r = await target.exec({
4729
+ argv,
4730
+ ...user ? { user } : {},
4731
+ stdio: "inherit"
4732
+ });
4733
+ if (r.exitCode !== 0) {
4734
+ io.stderr(`${opts.bin} harness: removing ${entry.id} failed (exit ${r.exitCode}).
4735
+ `);
4736
+ return { code: r.exitCode, dropReceipt: false };
4737
+ }
4738
+ io.stdout(`${entry.id}: removed from ${prefix}
4739
+ `);
4740
+ return { code: 0, dropReceipt: true };
4741
+ }
4742
+ async function uninstallOnHost(entry, opts, io, run) {
4743
+ const bin = opts.bin;
4744
+ const env = opts.env ?? process.env;
4745
+ const computed = hostPrefix(homeOf(opts));
4746
+ if (resolveUninstallArgv(entry, computed) === null) {
4747
+ io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4748
+ `);
4749
+ return 0;
4750
+ }
4751
+ let config;
4752
+ let workspace;
4753
+ try {
4754
+ config = readHostConfig(env);
4755
+ workspace = resolveHarnessRoot(void 0, env);
4756
+ } catch (err) {
4757
+ io.stderr(`${bin} harness: ${messageOf(err)}
4758
+ `);
4759
+ return 1;
4760
+ }
4761
+ const receipt = config.hostHarnesses?.[entry.id];
4762
+ if (receipt === void 0 && opts.force !== true) {
4763
+ io.stderr(
4764
+ `${bin} harness: no record of installing ${entry.id} on this host.
4765
+ Removing it could delete a harness you installed yourself. Re-run with \`--force\` to remove it from ${computed}.
4766
+ `
4767
+ );
4768
+ return 1;
4769
+ }
4770
+ const prefix = receipt?.prefix ?? computed;
4771
+ const target = hostTargetFor(receipt?.workspaceRoot ?? workspace, prefix, run, env);
4772
+ const outcome = await removeHarness(entry, target, prefix, void 0, opts, io);
4773
+ if (outcome.dropReceipt && receipt !== void 0) {
4774
+ const remaining = { ...config.hostHarnesses ?? {} };
4775
+ delete remaining[entry.id];
4776
+ try {
4777
+ writeHostConfig({ ...config, hostHarnesses: remaining }, env);
4778
+ } catch (err) {
4779
+ io.stderr(`${bin} harness: could not clear the install record: ${messageOf(err)}
4780
+ `);
4781
+ return 1;
4782
+ }
4783
+ io.stdout(`${entry.id}: cleared the host install record
4784
+ `);
4785
+ }
4786
+ return outcome.code;
4787
+ }
4788
+ async function runHarnessUninstall(name, opts, io) {
4789
+ const run = opts.run ?? spawnRunner;
4790
+ const root = resolveProjectRoot(opts.cwd);
4791
+ const entry = findHarness(name);
4792
+ if (!entry) return unknownHarness(name, io, opts.bin);
4793
+ const target = targetFor(root, run, opts.env);
4794
+ if (!isReachable(await resolveTargetStatus(target))) {
4795
+ return await uninstallOnHost(entry, opts, io, run);
4796
+ }
4797
+ return (await removeHarness(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
4798
+ }
4267
4799
  async function runHarnessInstall(name, opts, io) {
4268
4800
  const run = opts.run ?? spawnRunner;
4269
4801
  const env = opts.env ?? process.env;
@@ -4274,23 +4806,9 @@ async function runHarnessInstall(name, opts, io) {
4274
4806
  const entry = findHarness(name);
4275
4807
  if (!entry) return unknownHarness(name, io, opts.bin);
4276
4808
  const target = targetFor(root, run, opts.env);
4277
- let status;
4278
- try {
4279
- status = await target.status();
4280
- } catch (err) {
4281
- if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
4282
- io.stderr("docker is required to install into the running sandbox but was not found on PATH\n");
4283
- return 1;
4284
- }
4285
- throw err;
4286
- }
4809
+ const status = await resolveTargetStatus(target);
4287
4810
  if (!isReachable(status)) {
4288
- io.stderr(
4289
- `${opts.bin} harness: the sandbox is not running (${status}).
4290
- Start it with \`${opts.bin} sandbox\`, then re-run this command.
4291
- `
4292
- );
4293
- return 1;
4811
+ return await installOnHost(entry, opts, io, run, status);
4294
4812
  }
4295
4813
  const hermes = entry.id === "hermes";
4296
4814
  const installEnv = hermes ? {
@@ -4298,10 +4816,10 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4298
4816
  HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
4299
4817
  } : void 0;
4300
4818
  if (hermes) {
4301
- const code = await reconcileHermes(target, io, opts.bin);
4819
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4302
4820
  if (code !== 0) return code;
4303
4821
  }
4304
- const already = await probeInstalled(target, entry, installEnv);
4822
+ const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
4305
4823
  if (already === true) {
4306
4824
  io.stdout(`${entry.id}: already installed (${entry.binary})
4307
4825
  `);
@@ -4314,7 +4832,7 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4314
4832
  io.stdout(`installing ${entry.title} into the sandbox\u2026
4315
4833
  `);
4316
4834
  const r = await target.exec({
4317
- argv: [...entry.installArgv],
4835
+ argv: resolveInstallArgv(entry, SANDBOX_HARNESS_PREFIX),
4318
4836
  user: entry.installUser,
4319
4837
  stdio: "inherit",
4320
4838
  ...installEnv ? { env: installEnv } : {}
@@ -4325,12 +4843,12 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4325
4843
  return r.exitCode;
4326
4844
  }
4327
4845
  if (hermes) {
4328
- if (await probeInstalled(target, entry, installEnv) !== true) {
4846
+ if (await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv) !== true) {
4329
4847
  io.stderr(`${opts.bin} harness: Hermes installation finished but executable verification failed.
4330
4848
  `);
4331
4849
  return 1;
4332
4850
  }
4333
- const code = await reconcileHermes(target, io, opts.bin);
4851
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4334
4852
  if (code !== 0) return code;
4335
4853
  }
4336
4854
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
@@ -4338,8 +4856,14 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4338
4856
  return 0;
4339
4857
  }
4340
4858
 
4859
+ // src/commands/tool.ts
4860
+ import { existsSync as existsSync19 } from "node:fs";
4861
+ import { homedir as homedir3 } from "node:os";
4862
+ import { delimiter as delimiter3, join as join13, resolve as resolve13 } from "node:path";
4863
+
4341
4864
  // src/lib/tools/catalog.ts
4342
4865
  var TOOLS_DOC = "docs/installation.md";
4866
+ var TOOL_HOST_PLATFORM = "linux";
4343
4867
  var TOOL_CATALOG = Object.freeze([
4344
4868
  Object.freeze({
4345
4869
  id: "agent-browser",
@@ -4354,6 +4878,9 @@ var TOOL_CATALOG = Object.freeze([
4354
4878
  ]),
4355
4879
  installUser: "sandbox",
4356
4880
  downloadSize: "~1 GB",
4881
+ hostCapable: false,
4882
+ notHostCapableReason: () => "agent-browser runs `agent-browser install --with-deps`, which installs system packages with the operating system package manager. AGRO installs it into the sandbox only, where that stays contained.",
4883
+ uninstallArgv: Object.freeze(["bash", "-lc", "pnpm remove -g agent-browser"]),
4357
4884
  docsPath: TOOLS_DOC
4358
4885
  }),
4359
4886
  Object.freeze({
@@ -4385,6 +4912,8 @@ var TOOL_CATALOG = Object.freeze([
4385
4912
  ].join("\n")
4386
4913
  ]),
4387
4914
  installUser: "sandbox",
4915
+ hostCapable: true,
4916
+ uninstallArgv: Object.freeze(["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/herdr`]),
4388
4917
  docsPath: TOOLS_DOC
4389
4918
  }),
4390
4919
  Object.freeze({
@@ -4417,6 +4946,8 @@ var TOOL_CATALOG = Object.freeze([
4417
4946
  ].join("\n")
4418
4947
  ]),
4419
4948
  installUser: "sandbox",
4949
+ hostCapable: true,
4950
+ uninstallArgv: Object.freeze(["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/cloudflared`]),
4420
4951
  docsPath: TOOLS_DOC
4421
4952
  }),
4422
4953
  Object.freeze({
@@ -4442,6 +4973,13 @@ var TOOL_CATALOG = Object.freeze([
4442
4973
  ].join("\n")
4443
4974
  ]),
4444
4975
  installUser: "sandbox",
4976
+ hostCapable: true,
4977
+ uninstallArgv: Object.freeze([
4978
+ "rm",
4979
+ "-rf",
4980
+ `${HARNESS_PREFIX_TOKEN}/bin/msb`,
4981
+ `${HARNESS_PREFIX_TOKEN}/microsandbox`
4982
+ ]),
4445
4983
  docsPath: "docs/runtimes/microsandbox.md"
4446
4984
  }),
4447
4985
  Object.freeze({
@@ -4452,6 +4990,8 @@ var TOOL_CATALOG = Object.freeze([
4452
4990
  verifyArgv: Object.freeze(["bash", "-lc", "command -v docker >/dev/null"]),
4453
4991
  versionArgv: Object.freeze(["docker", "--version"]),
4454
4992
  notInstallableReason: (bin) => `The Docker CLI is installed in the base image. Note that the CLI being present says nothing about whether a daemon is reachable \u2014 \`${bin} ps <name>\` answers that.`,
4993
+ hostCapable: false,
4994
+ uninstallArgv: null,
4455
4995
  docsPath: TOOLS_DOC
4456
4996
  }),
4457
4997
  Object.freeze({
@@ -4462,6 +5002,8 @@ var TOOL_CATALOG = Object.freeze([
4462
5002
  verifyArgv: Object.freeze(["bash", "-lc", "command -v gh >/dev/null"]),
4463
5003
  versionArgv: Object.freeze(["gh", "--version"]),
4464
5004
  notInstallableReason: () => "The GitHub CLI is installed in the base image. Run `gh auth login` inside the sandbox to authenticate it.",
5005
+ hostCapable: false,
5006
+ uninstallArgv: null,
4465
5007
  docsPath: TOOLS_DOC
4466
5008
  }),
4467
5009
  Object.freeze({
@@ -4493,6 +5035,13 @@ install -m 0755 "$tmp/tailscale_1.102.3_$arch/tailscaled" "$prefix/bin/tailscale
4493
5035
  install -d -m 0700 "$HOME/.tailscale"`
4494
5036
  ]),
4495
5037
  installUser: "sandbox",
5038
+ hostCapable: true,
5039
+ uninstallArgv: Object.freeze([
5040
+ "rm",
5041
+ "-rf",
5042
+ `${HARNESS_PREFIX_TOKEN}/bin/tailscale`,
5043
+ `${HARNESS_PREFIX_TOKEN}/bin/tailscaled`
5044
+ ]),
4496
5045
  docsPath: TOOLS_DOC
4497
5046
  })
4498
5047
  ]);
@@ -4505,6 +5054,12 @@ function toolIds() {
4505
5054
  function installableToolIds() {
4506
5055
  return TOOL_CATALOG.filter((t) => t.installArgv !== void 0).map((t) => t.id);
4507
5056
  }
5057
+ function hostCapableToolIds() {
5058
+ return TOOL_CATALOG.filter((t) => t.hostCapable).map((t) => t.id);
5059
+ }
5060
+ function resolveToolUninstallArgv(entry, prefix) {
5061
+ return entry.uninstallArgv === null ? null : entry.uninstallArgv.map((part) => part.split(HARNESS_PREFIX_TOKEN).join(prefix));
5062
+ }
4508
5063
 
4509
5064
  // src/commands/tool.ts
4510
5065
  function isReachable2(status) {
@@ -4519,56 +5074,116 @@ function targetFor2(root, run, env) {
4519
5074
  ...env ? { env } : {}
4520
5075
  });
4521
5076
  }
4522
- async function tryExec(target, argv, user) {
5077
+ function hostPrefix2(home) {
5078
+ return join13(home, ".local");
5079
+ }
5080
+ function pathEntries2(env) {
5081
+ return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
5082
+ }
5083
+ function onPath2(prefix, env) {
5084
+ return pathEntries2(env).includes(harnessBinPath(prefix));
5085
+ }
5086
+ function hostTargetFor2(root, prefix, run, env) {
5087
+ const path4 = [harnessBinPath(prefix), ...pathEntries2(env)].join(delimiter3);
5088
+ return new LocalExecutionTarget({
5089
+ projectRoot: root,
5090
+ run,
5091
+ env: { ...env, PATH: path4 }
5092
+ });
5093
+ }
5094
+ function messageOf2(error) {
5095
+ return error instanceof Error ? error.message : String(error);
5096
+ }
5097
+ function homeOf2(opts) {
5098
+ return (opts.homedir ?? homedir3)();
5099
+ }
5100
+ function platformOf(opts) {
5101
+ return opts.platform ?? process.platform;
5102
+ }
5103
+ function isInteractive2(opts) {
5104
+ return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
5105
+ }
5106
+ async function tryExec(target, argv, user, env) {
4523
5107
  try {
4524
- const r = await target.exec({ argv: [...argv], user, stdio: "capture" });
5108
+ const r = await target.exec({
5109
+ argv: [...argv],
5110
+ ...user ? { user } : {},
5111
+ stdio: "capture",
5112
+ ...env ? { env } : {}
5113
+ });
4525
5114
  return { exitCode: r.exitCode, stdout: r.stdout };
4526
5115
  } catch (err) {
4527
5116
  if (err instanceof ExecutionSpawnError) return null;
4528
5117
  throw err;
4529
5118
  }
4530
5119
  }
4531
- async function probeInstalled2(target, entry) {
4532
- const r = await tryExec(target, entry.verifyArgv, "sandbox");
5120
+ async function probeInstalled2(target, entry, user, env) {
5121
+ const r = await tryExec(target, entry.verifyArgv, user, env);
4533
5122
  return r === null ? null : r.exitCode === 0;
4534
5123
  }
4535
- async function probeVersion(target, entry) {
5124
+ async function probeVersion(target, entry, user) {
4536
5125
  if (entry.versionArgv === void 0) return null;
4537
- const r = await tryExec(target, entry.versionArgv, "sandbox");
5126
+ const r = await tryExec(target, entry.versionArgv, user);
4538
5127
  if (r === null || r.exitCode !== 0) return null;
4539
5128
  const first = r.stdout.trim().split("\n")[0] ?? "";
4540
5129
  return first === "" ? null : first;
4541
5130
  }
4542
- async function collectRows(root, run, env, only) {
5131
+ function rowOf(entry, installed, version, location) {
5132
+ return {
5133
+ id: entry.id,
5134
+ title: entry.title,
5135
+ binary: entry.binary,
5136
+ kind: entry.kind,
5137
+ installed,
5138
+ version,
5139
+ installable: entry.installArgv !== void 0,
5140
+ hostCapable: entry.hostCapable,
5141
+ location,
5142
+ docs: sourceDocsUrl(entry.docsPath)
5143
+ };
5144
+ }
5145
+ function probeableHost2(env, home) {
5146
+ let root;
5147
+ try {
5148
+ root = resolveHarnessRoot(void 0, env);
5149
+ } catch {
5150
+ return void 0;
5151
+ }
5152
+ const prefix = hostPrefix2(home);
5153
+ if (!existsSync19(join13(root, ".git")) && !existsSync19(prefix)) return void 0;
5154
+ return { root, prefix };
5155
+ }
5156
+ async function collectRows(root, run, home, env, only) {
4543
5157
  const entries = only ? [...only] : [...TOOL_CATALOG];
4544
5158
  const target = targetFor2(root, run, env);
4545
- let reachable = false;
4546
- try {
4547
- reachable = isReachable2(await target.status());
4548
- } catch (err) {
4549
- if (!(err instanceof ExecutionSpawnError)) throw err;
5159
+ if (isReachable2(await resolveTargetStatus(target))) {
5160
+ const rows2 = [];
5161
+ for (const entry of entries) {
5162
+ const installed = await probeInstalled2(target, entry, "sandbox");
5163
+ const version = installed === true ? await probeVersion(target, entry, "sandbox") : null;
5164
+ rows2.push(rowOf(entry, installed, version, installed === null ? "unknown" : "sandbox"));
5165
+ }
5166
+ return { rows: rows2 };
4550
5167
  }
5168
+ const host = probeableHost2(env ?? process.env, home);
5169
+ if (host === void 0) {
5170
+ return { rows: entries.map((entry) => rowOf(entry, null, null, "unknown")) };
5171
+ }
5172
+ const hostTarget = hostTargetFor2(host.root, host.prefix, run, env ?? process.env);
4551
5173
  const rows = [];
4552
5174
  for (const entry of entries) {
4553
- const installed = reachable ? await probeInstalled2(target, entry) : null;
4554
- rows.push({
4555
- id: entry.id,
4556
- title: entry.title,
4557
- binary: entry.binary,
4558
- kind: entry.kind,
4559
- installed,
4560
- version: reachable && installed === true ? await probeVersion(target, entry) : null,
4561
- installable: entry.installArgv !== void 0,
4562
- docs: sourceDocsUrl(entry.docsPath)
4563
- });
5175
+ const installed = await probeInstalled2(hostTarget, entry, void 0);
5176
+ const version = installed === true ? await probeVersion(hostTarget, entry, void 0) : null;
5177
+ rows.push(rowOf(entry, installed, version, installed === null ? "unknown" : "host"));
4564
5178
  }
4565
- return rows;
5179
+ return { rows, hostPrefix: host.prefix };
4566
5180
  }
4567
5181
  function cell2(value, absent) {
4568
5182
  if (value === null) return absent;
4569
5183
  return value ? "yes" : "no";
4570
5184
  }
4571
- function renderTable2(rows, io, bin) {
5185
+ function renderTable2(collected, io, bin) {
5186
+ const rows = collected.rows;
4572
5187
  const header2 = ["TOOL", "KIND", "INSTALLED"];
4573
5188
  const body = rows.map((r) => [r.id, r.kind, cell2(r.installed, "?")]);
4574
5189
  const widths = header2.map(
@@ -4577,21 +5192,31 @@ function renderTable2(rows, io, bin) {
4577
5192
  const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
4578
5193
  io.stdout(line(header2));
4579
5194
  for (const row of body) io.stdout(line(row));
5195
+ if (rows.some((r) => r.location === "host")) {
5196
+ io.stdout(
5197
+ `
5198
+ INSTALLED reports the host prefix ${collected.hostPrefix} \u2014 the sandbox is not running.
5199
+ `
5200
+ );
5201
+ return;
5202
+ }
4580
5203
  if (rows.some((r) => r.installed === null)) {
4581
5204
  io.stdout(`
4582
5205
  INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
4583
5206
  `);
4584
5207
  }
4585
5208
  }
4586
- function renderDetail(rows, io, bin) {
4587
- renderTable2(rows, io, bin);
4588
- for (const r of rows) {
5209
+ function renderDetail(collected, io, bin) {
5210
+ renderTable2(collected, io, bin);
5211
+ for (const r of collected.rows) {
4589
5212
  io.stdout(`
4590
5213
  ${r.id} \u2014 ${r.title}
4591
5214
  `);
4592
5215
  io.stdout(` version: ${r.version ?? "\u2014"}
4593
5216
  `);
4594
5217
  io.stdout(` installable: ${r.installable ? "yes" : "no"}
5218
+ `);
5219
+ io.stdout(` on the host: ${r.hostCapable ? "yes" : "no"}
4595
5220
  `);
4596
5221
  io.stdout(` see ${r.docs}
4597
5222
  `);
@@ -4600,12 +5225,12 @@ ${r.id} \u2014 ${r.title}
4600
5225
  async function runToolList(opts, io) {
4601
5226
  const run = opts.run ?? spawnRunner;
4602
5227
  const root = resolveProjectRoot(opts.cwd);
4603
- const rows = await collectRows(root, run, opts.env);
5228
+ const collected = await collectRows(root, run, homeOf2(opts), opts.env);
4604
5229
  if (opts.json) {
4605
- io.stdout(`${JSON.stringify(rows, null, 2)}
5230
+ io.stdout(`${JSON.stringify(collected.rows, null, 2)}
4606
5231
  `);
4607
5232
  } else {
4608
- renderTable2(rows, io, opts.bin);
5233
+ renderTable2(collected, io, opts.bin);
4609
5234
  }
4610
5235
  return 0;
4611
5236
  }
@@ -4626,12 +5251,12 @@ async function runToolStatus(name, opts, io) {
4626
5251
  only = findTool(name);
4627
5252
  if (!only) return unknownTool(name, io, opts.bin);
4628
5253
  }
4629
- const rows = await collectRows(root, run, opts.env, only ? [only] : void 0);
5254
+ const collected = await collectRows(root, run, homeOf2(opts), opts.env, only ? [only] : void 0);
4630
5255
  if (opts.json) {
4631
- io.stdout(`${JSON.stringify(only ? rows[0] : rows, null, 2)}
5256
+ io.stdout(`${JSON.stringify(only ? collected.rows[0] : collected.rows, null, 2)}
4632
5257
  `);
4633
5258
  } else {
4634
- renderDetail(rows, io, opts.bin);
5259
+ renderDetail(collected, io, opts.bin);
4635
5260
  }
4636
5261
  return 0;
4637
5262
  }
@@ -4648,6 +5273,151 @@ Re-run with --yes to accept the download.
4648
5273
  );
4649
5274
  return false;
4650
5275
  }
5276
+ function unreachableReason2(status) {
5277
+ return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
5278
+ }
5279
+ function sandboxRefusal2(bin, status) {
5280
+ if (runtimeIsAbsent(status)) {
5281
+ return `${bin} tool: no container runtime is on PATH.
5282
+ `;
5283
+ }
5284
+ return `${bin} tool: the sandbox is not running (${status}).
5285
+ Start it with \`${bin} sandbox\`, then re-run this command.
5286
+ `;
5287
+ }
5288
+ function hostCapableList(bin) {
5289
+ return `Tools that install on the host:
5290
+ ${hostCapableToolIds().map((t) => ` ${bin} tool install ${t} --host`).join("\n")}
5291
+ `;
5292
+ }
5293
+ async function installOnHost2(entry, opts, io, run, status) {
5294
+ const bin = opts.bin;
5295
+ const env = opts.env ?? process.env;
5296
+ const flagged = opts.host === true || opts.path !== void 0;
5297
+ if (!entry.hostCapable) {
5298
+ io.stderr(sandboxRefusal2(bin, status));
5299
+ io.stderr(`
5300
+ ${bin} tool: ${entry.id} cannot be installed on the host.
5301
+ `);
5302
+ io.stderr(`${(entry.notHostCapableReason ?? entry.notInstallableReason)?.(bin) ?? ""}
5303
+
5304
+ `);
5305
+ io.stderr(hostCapableList(bin));
5306
+ return 1;
5307
+ }
5308
+ const platform = platformOf(opts);
5309
+ if (platform !== TOOL_HOST_PLATFORM) {
5310
+ io.stderr(sandboxRefusal2(bin, status));
5311
+ io.stderr(
5312
+ `
5313
+ ${bin} tool: a host install needs ${TOOL_HOST_PLATFORM}; this host is ${platform}.
5314
+ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with your own package manager, or start the sandbox.
5315
+ `
5316
+ );
5317
+ return 1;
5318
+ }
5319
+ if (!flagged && !isInteractive2(opts)) {
5320
+ io.stderr(
5321
+ sandboxRefusal2(bin, status) + `Or install on the host with \`${bin} tool install ${entry.id} --host\`.
5322
+ `
5323
+ );
5324
+ return 1;
5325
+ }
5326
+ let root;
5327
+ let recorded;
5328
+ try {
5329
+ recorded = readHostConfig(env).harnessRoot;
5330
+ root = resolveHarnessRoot(opts.path, env);
5331
+ } catch (err) {
5332
+ io.stderr(`${bin} tool: ${messageOf2(err)}
5333
+ `);
5334
+ return 1;
5335
+ }
5336
+ const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
5337
+ if (!flagged) {
5338
+ const ask2 = io.ask ?? ask;
5339
+ const answer = (await ask2(
5340
+ `${unreachableReason2(status)} Install ${entry.title} on the host? [y/N]`
5341
+ )).trim().toLowerCase();
5342
+ if (!/^y/.test(answer)) {
5343
+ io.stderr(sandboxRefusal2(bin, status));
5344
+ return 1;
5345
+ }
5346
+ if (!sticky) {
5347
+ const chosen = (await ask2(`Harness root [${root}]:`)).trim();
5348
+ if (chosen !== "") root = resolve13(chosen);
5349
+ }
5350
+ }
5351
+ if (sticky) io.stdout(`using the recorded harness root ${root}
5352
+ `);
5353
+ try {
5354
+ if (!existsSync19(join13(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
5355
+ `);
5356
+ const workspace = ensureHostWorkspace(root, run);
5357
+ root = workspace.root;
5358
+ io.stdout(
5359
+ workspace.action === "cloned" ? `host workspace cloned into ${root}
5360
+ ` : `host workspace reused at ${root}
5361
+ `
5362
+ );
5363
+ } catch (err) {
5364
+ io.stderr(`${bin} tool: ${messageOf2(err)}
5365
+ `);
5366
+ return 1;
5367
+ }
5368
+ const prefix = hostPrefix2(homeOf2(opts));
5369
+ const installEnv = { NPM_USER_PREFIX: prefix };
5370
+ const target = hostTargetFor2(root, prefix, run, env);
5371
+ if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
5372
+ io.stdout(`${entry.id}: already installed (${entry.binary})
5373
+ `);
5374
+ return 0;
5375
+ }
5376
+ if (!await confirmDownload(entry, opts, io)) return 1;
5377
+ io.stdout(`installing ${entry.title} on the host\u2026
5378
+ `);
5379
+ const r = await target.exec({
5380
+ argv: [...entry.installArgv],
5381
+ stdio: "inherit",
5382
+ env: installEnv
5383
+ });
5384
+ if (r.exitCode !== 0) {
5385
+ io.stderr(`${bin} tool: installing ${entry.id} failed (exit ${r.exitCode}).
5386
+ `);
5387
+ return r.exitCode;
5388
+ }
5389
+ const receipt = {
5390
+ prefix,
5391
+ binary: entry.binary,
5392
+ binPath: harnessBinPath(prefix),
5393
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
5394
+ workspaceRoot: root
5395
+ };
5396
+ try {
5397
+ const config = readHostConfig(env);
5398
+ writeHostConfig(
5399
+ {
5400
+ ...config,
5401
+ harnessRoot: root,
5402
+ hostTools: { ...config.hostTools ?? {}, [entry.id]: receipt }
5403
+ },
5404
+ env
5405
+ );
5406
+ } catch (err) {
5407
+ io.stderr(`${bin} tool: could not record the install: ${messageOf2(err)}
5408
+ `);
5409
+ return 1;
5410
+ }
5411
+ io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)}
5412
+ `);
5413
+ if (!onPath2(prefix, env)) {
5414
+ io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
5415
+ `);
5416
+ }
5417
+ io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
5418
+ `);
5419
+ return 0;
5420
+ }
4651
5421
  async function runToolInstall(name, opts, io) {
4652
5422
  const run = opts.run ?? spawnRunner;
4653
5423
  const root = resolveProjectRoot(opts.cwd);
@@ -4666,25 +5436,11 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
4666
5436
  return 1;
4667
5437
  }
4668
5438
  const target = targetFor2(root, run, opts.env);
4669
- let status;
4670
- try {
4671
- status = await target.status();
4672
- } catch (err) {
4673
- if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
4674
- io.stderr("docker is required to install into the running sandbox but was not found on PATH\n");
4675
- return 1;
4676
- }
4677
- throw err;
4678
- }
5439
+ const status = await resolveTargetStatus(target);
4679
5440
  if (!isReachable2(status)) {
4680
- io.stderr(
4681
- `${opts.bin} tool: the sandbox is not running (${status}).
4682
- Start it with \`${opts.bin} sandbox\`, then re-run this command.
4683
- `
4684
- );
4685
- return 1;
5441
+ return await installOnHost2(entry, opts, io, run, status);
4686
5442
  }
4687
- const already = await probeInstalled2(target, entry);
5443
+ const already = await probeInstalled2(target, entry, "sandbox");
4688
5444
  if (already === true) {
4689
5445
  io.stdout(`${entry.id}: already installed (${entry.binary})
4690
5446
  `);
@@ -4711,6 +5467,98 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4711
5467
  `);
4712
5468
  return 0;
4713
5469
  }
5470
+ async function removeTool(entry, target, prefix, user, opts, io) {
5471
+ const argv = resolveToolUninstallArgv(entry, prefix);
5472
+ if (await probeInstalled2(target, entry, user) !== true) {
5473
+ io.stdout(`${entry.id}: not installed (${entry.binary})
5474
+ `);
5475
+ return { code: 0, dropReceipt: true };
5476
+ }
5477
+ if (isInteractive2(opts)) {
5478
+ const ask2 = io.ask ?? ask;
5479
+ const answer = (await ask2(`Remove ${entry.title} from ${prefix}? [y/N]`)).trim().toLowerCase();
5480
+ if (!/^y/.test(answer)) {
5481
+ io.stderr(`${opts.bin} tool: removed nothing.
5482
+ `);
5483
+ return { code: 1, dropReceipt: false };
5484
+ }
5485
+ }
5486
+ io.stdout(`removing ${entry.title} from ${prefix}\u2026
5487
+ `);
5488
+ const r = await target.exec({
5489
+ argv,
5490
+ ...user ? { user } : {},
5491
+ stdio: "inherit"
5492
+ });
5493
+ if (r.exitCode !== 0) {
5494
+ io.stderr(`${opts.bin} tool: removing ${entry.id} failed (exit ${r.exitCode}).
5495
+ `);
5496
+ return { code: r.exitCode, dropReceipt: false };
5497
+ }
5498
+ io.stdout(`${entry.id}: removed from ${prefix}
5499
+ `);
5500
+ return { code: 0, dropReceipt: true };
5501
+ }
5502
+ async function uninstallOnHost2(entry, opts, io, run) {
5503
+ const bin = opts.bin;
5504
+ const env = opts.env ?? process.env;
5505
+ const computed = hostPrefix2(homeOf2(opts));
5506
+ let config;
5507
+ let workspace;
5508
+ try {
5509
+ config = readHostConfig(env);
5510
+ workspace = resolveHarnessRoot(void 0, env);
5511
+ } catch (err) {
5512
+ io.stderr(`${bin} tool: ${messageOf2(err)}
5513
+ `);
5514
+ return 1;
5515
+ }
5516
+ const receipt = config.hostTools?.[entry.id];
5517
+ if (receipt === void 0 && opts.force !== true) {
5518
+ io.stderr(
5519
+ `${bin} tool: no record of installing ${entry.id} on this host.
5520
+ Removing it could delete a tool you installed yourself. Re-run with \`--force\` to remove it from ${computed}.
5521
+ `
5522
+ );
5523
+ return 1;
5524
+ }
5525
+ const prefix = receipt?.prefix ?? computed;
5526
+ const target = hostTargetFor2(receipt?.workspaceRoot ?? workspace, prefix, run, env);
5527
+ const outcome = await removeTool(entry, target, prefix, void 0, opts, io);
5528
+ if (outcome.dropReceipt && receipt !== void 0) {
5529
+ const remaining = { ...config.hostTools ?? {} };
5530
+ delete remaining[entry.id];
5531
+ try {
5532
+ writeHostConfig({ ...config, hostTools: remaining }, env);
5533
+ } catch (err) {
5534
+ io.stderr(`${bin} tool: could not clear the install record: ${messageOf2(err)}
5535
+ `);
5536
+ return 1;
5537
+ }
5538
+ io.stdout(`${entry.id}: cleared the host install record
5539
+ `);
5540
+ }
5541
+ return outcome.code;
5542
+ }
5543
+ async function runToolUninstall(name, opts, io) {
5544
+ const run = opts.run ?? spawnRunner;
5545
+ const root = resolveProjectRoot(opts.cwd);
5546
+ const entry = findTool(name);
5547
+ if (!entry) return unknownTool(name, io, opts.bin);
5548
+ if (entry.uninstallArgv === null) {
5549
+ io.stderr(`${opts.bin} tool: ${entry.id} cannot be removed by this command.
5550
+
5551
+ `);
5552
+ io.stderr(`${entry.notInstallableReason?.(opts.bin) ?? ""}
5553
+ `);
5554
+ return 1;
5555
+ }
5556
+ const target = targetFor2(root, run, opts.env);
5557
+ if (!isReachable2(await resolveTargetStatus(target))) {
5558
+ return await uninstallOnHost2(entry, opts, io, run);
5559
+ }
5560
+ return (await removeTool(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
5561
+ }
4714
5562
 
4715
5563
  // src/lib/remote.ts
4716
5564
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -4776,9 +5624,9 @@ function fetchRemoteSource(opts) {
4776
5624
  }
4777
5625
 
4778
5626
  // src/cli.ts
4779
- var VERSION = true ? "0.11.0" : "0.0.0-dev";
4780
- var DEFAULT_SOURCE_OH_DIR = resolve10(
4781
- dirname8(fileURLToPath(import.meta.url)),
5627
+ var VERSION = true ? "0.12.1" : "0.0.0-dev";
5628
+ var DEFAULT_SOURCE_OH_DIR = resolve14(
5629
+ dirname10(fileURLToPath(import.meta.url)),
4782
5630
  "../.."
4783
5631
  );
4784
5632
  var INTEGRATIONS = {};
@@ -5008,16 +5856,26 @@ function printHarnessHelp(bin = LEGACY_PRODUCT.bin) {
5008
5856
 
5009
5857
  Usage:
5010
5858
  ${bin} harness list List known harnesses and their state
5011
- ${bin} harness install <name> Install a harness into the sandbox
5859
+ ${bin} harness install <name> Install a harness into the sandbox or the host
5860
+ ${bin} harness uninstall <name> Remove a harness the same way it was installed
5012
5861
  ${bin} harness status [name] Show installed state
5013
5862
 
5014
- \`install\` is the only door: it probes the running sandbox, installs the harness
5015
- into the persistent home volume, and reports. It reads and writes no \`${stateNames(bin).configFile}\`
5016
- field, and it never rebuilds or restarts the sandbox. It requires a running
5017
- sandbox \u2014 start one with \`${bin} sandbox\` first.
5863
+ \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
5864
+ reachable they act on the host. A host install clones the AGRO workspace into the
5865
+ harness root, then installs the harness into \`~/.local\`. \`--path <dir>\` chooses
5866
+ the harness root; a successful install records it as \`harnessRoot\` in the host
5867
+ \`${stateNames(bin).configFile}\`. The default is \`~/.agro\`, and the install prefix is
5868
+ always \`~/.local\`.
5869
+
5870
+ On the host, \`uninstall\` removes only the harness \`install\` recorded, from the
5871
+ prefix in that record. Without a record it refuses, and \`--force\` overrides. In
5872
+ the sandbox it needs no record. See \`docs/harnesses/overview.md\`.
5018
5873
 
5019
5874
  Flags:
5020
5875
  --json Machine-readable output (list/status)
5876
+ --host Install on the host when the sandbox is not running (install)
5877
+ --path <dir> Harness root for the workspace clone; implies --host (install)
5878
+ --force Remove without a recorded host install (uninstall)
5021
5879
 
5022
5880
  Harnesses:
5023
5881
  ${harnessIds().map((h) => ` ${h}`).join("\n")}
@@ -5055,19 +5913,34 @@ tunnel client, the GitHub CLI, an isolation runtime's own binary.
5055
5913
  Usage:
5056
5914
  ${bin} tool list List known tools and their state
5057
5915
  ${bin} tool status [name] Show installed state and version
5058
- ${bin} tool install <name> Install a tool into the sandbox
5916
+ ${bin} tool install <name> Install a tool into the sandbox or the host
5917
+ ${bin} tool uninstall <name> Remove a tool the same way it was installed
5059
5918
 
5060
5919
  Most tools are baked into the image and are report-only; \`install\` works on:
5061
5920
  ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5062
5921
 
5063
5922
  \`install\` is the only door: it probes the running sandbox, installs the tool
5064
- into the persistent home volume, and reports. It reads and writes no \`${stateNames(bin).configFile}\`
5065
- field, and it never rebuilds or restarts the sandbox. A large download is
5066
- confirmed first, and a non-interactive run without --yes installs nothing.
5923
+ into the persistent home volume, and reports. It never rebuilds or restarts the
5924
+ sandbox. A large download is confirmed first, and a non-interactive run without
5925
+ --yes installs nothing.
5926
+
5927
+ When no sandbox is reachable, \`install\` and \`uninstall\` act on the host. A host
5928
+ install needs Linux, because every installer is Debian-specific, and it works
5929
+ only on these tools:
5930
+ ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
5931
+
5932
+ A host install clones the AGRO workspace into the harness root \u2014 \`--path <dir>\`,
5933
+ then \`harnessRoot\` in the host \`${stateNames(bin).configFile}\`, then \`~/.agro\` \u2014 and installs into
5934
+ \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
5935
+ \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
5936
+ Without a record it refuses, and \`--force\` overrides.
5067
5937
 
5068
5938
  Flags:
5069
5939
  --yes Accept a large download without prompting
5070
5940
  --json Machine-readable output (list/status)
5941
+ --host Install on the host when the sandbox is not running (install)
5942
+ --path <dir> Harness root for the workspace clone; implies --host (install)
5943
+ --force Remove without a recorded host install (uninstall)
5071
5944
 
5072
5945
  Tools:
5073
5946
  ${toolIds().map((t) => ` ${t}`).join("\n")}
@@ -5449,72 +6322,117 @@ function parseShellArgs(rest, bin = LEGACY_PRODUCT.bin) {
5449
6322
  function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
5450
6323
  const args = {
5451
6324
  help: false,
5452
- json: false
6325
+ json: false,
6326
+ host: false,
6327
+ force: false
5453
6328
  };
5454
6329
  if (rest.length === 0 || isHelpFlag(rest[0])) {
5455
6330
  return { ok: true, args: { ...args, help: true } };
5456
6331
  }
5457
6332
  const positionals = [];
6333
+ let expectPath = false;
5458
6334
  for (const token of rest) {
5459
- if (token === "--json") {
6335
+ if (expectPath) {
6336
+ expectPath = false;
6337
+ args.path = token;
6338
+ args.host = true;
6339
+ } else if (token === "--json") {
5460
6340
  args.json = true;
6341
+ } else if (token === "--host") {
6342
+ args.host = true;
6343
+ } else if (token === "--force") {
6344
+ args.force = true;
6345
+ } else if (token === "--path") {
6346
+ expectPath = true;
6347
+ } else if (token.startsWith("--path=")) {
6348
+ args.path = token.slice("--path=".length);
6349
+ args.host = true;
5461
6350
  } else if (token.startsWith("-")) {
5462
6351
  return { ok: false, error: `${bin} harness: unknown flag "${token}"` };
5463
6352
  } else {
5464
6353
  positionals.push(token);
5465
6354
  }
5466
6355
  }
6356
+ if (expectPath || args.path === "") {
6357
+ return { ok: false, error: `${bin} harness: --path requires a directory` };
6358
+ }
5467
6359
  const [sub, name, ...extra] = positionals;
5468
- if (sub !== "list" && sub !== "install" && sub !== "status") {
6360
+ if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
5469
6361
  return {
5470
6362
  ok: false,
5471
- error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, or status`,
6363
+ error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
5472
6364
  showHelp: true
5473
6365
  };
5474
6366
  }
5475
6367
  if (extra.length > 0) {
5476
6368
  return { ok: false, error: `${bin} harness: unexpected argument "${extra[0]}"` };
5477
6369
  }
5478
- if (sub === "install" && name === void 0) {
5479
- return { ok: false, error: `${bin} harness install: a harness name is required`, showHelp: true };
6370
+ if ((sub === "install" || sub === "uninstall") && name === void 0) {
6371
+ return { ok: false, error: `${bin} harness ${sub}: a harness name is required`, showHelp: true };
5480
6372
  }
5481
6373
  if (sub === "list" && name !== void 0) {
5482
6374
  return { ok: false, error: `${bin} harness list: unexpected argument "${name}"` };
5483
6375
  }
6376
+ if (sub !== "install" && args.host) {
6377
+ return { ok: false, error: `${bin} harness ${sub}: --host and --path apply to install only` };
6378
+ }
6379
+ if (sub !== "uninstall" && args.force) {
6380
+ return { ok: false, error: `${bin} harness ${sub}: --force applies to uninstall only` };
6381
+ }
5484
6382
  args.subcommand = sub;
5485
6383
  if (name !== void 0) args.name = name;
5486
6384
  return { ok: true, args };
5487
6385
  }
5488
6386
  function parseToolArgs(rest, bin = LEGACY_PRODUCT.bin) {
5489
- const args = { help: false, yes: false, json: false };
6387
+ const args = { help: false, yes: false, json: false, host: false, force: false };
5490
6388
  if (rest.length === 0 || isHelpFlag(rest[0])) {
5491
6389
  return { ok: true, args: { ...args, help: true } };
5492
6390
  }
5493
6391
  const positionals = [];
6392
+ let expectPath = false;
5494
6393
  for (const token of rest) {
5495
- if (token === "--yes" || token === "-y") args.yes = true;
6394
+ if (expectPath) {
6395
+ expectPath = false;
6396
+ args.path = token;
6397
+ args.host = true;
6398
+ } else if (token === "--yes" || token === "-y") args.yes = true;
5496
6399
  else if (token === "--json") args.json = true;
5497
- else if (token.startsWith("-")) {
6400
+ else if (token === "--host") args.host = true;
6401
+ else if (token === "--force") args.force = true;
6402
+ else if (token === "--path") expectPath = true;
6403
+ else if (token.startsWith("--path=")) {
6404
+ args.path = token.slice("--path=".length);
6405
+ args.host = true;
6406
+ } else if (token.startsWith("-")) {
5498
6407
  return { ok: false, error: `${bin} tool: unknown flag "${token}"` };
5499
6408
  } else positionals.push(token);
5500
6409
  }
6410
+ if (expectPath || args.path === "") {
6411
+ return { ok: false, error: `${bin} tool: --path requires a directory` };
6412
+ }
5501
6413
  const [sub, name, ...extra] = positionals;
5502
- if (sub !== "list" && sub !== "install" && sub !== "status") {
6414
+ if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
5503
6415
  return {
5504
6416
  ok: false,
5505
- error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, or status`,
6417
+ error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
5506
6418
  showHelp: true
5507
6419
  };
5508
6420
  }
5509
6421
  if (extra.length > 0) {
5510
6422
  return { ok: false, error: `${bin} tool: unexpected argument "${extra[0]}"` };
5511
6423
  }
5512
- if (sub === "install" && name === void 0) {
5513
- return { ok: false, error: `${bin} tool install: a tool name is required`, showHelp: true };
6424
+ if ((sub === "install" || sub === "uninstall") && name === void 0) {
6425
+ return { ok: false, error: `${bin} tool ${sub}: a tool name is required`, showHelp: true };
5514
6426
  }
5515
6427
  if (sub === "list" && name !== void 0) {
5516
6428
  return { ok: false, error: `${bin} tool list: unexpected argument "${name}"` };
5517
6429
  }
6430
+ if (sub !== "install" && args.host) {
6431
+ return { ok: false, error: `${bin} tool ${sub}: --host and --path apply to install only` };
6432
+ }
6433
+ if (sub !== "uninstall" && args.force) {
6434
+ return { ok: false, error: `${bin} tool ${sub}: --force applies to uninstall only` };
6435
+ }
5518
6436
  args.subcommand = sub;
5519
6437
  if (name !== void 0) args.name = name;
5520
6438
  return { ok: true, args };
@@ -5525,19 +6443,19 @@ function parseGatewayArgs(rest) {
5525
6443
  }
5526
6444
  return { ok: true, args: { help: false, passthrough: [...rest] } };
5527
6445
  }
5528
- function bundledPayloadExists(bundled, exists = existsSync16) {
5529
- return exists(join10(bundled.sourceOhDir, "manifest.json"));
6446
+ function bundledPayloadExists(bundled, exists = existsSync20) {
6447
+ return exists(join14(bundled.sourceOhDir, "manifest.json"));
5530
6448
  }
5531
6449
  function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
5532
- const exists = bundled.exists ?? existsSync16;
6450
+ const exists = bundled.exists ?? existsSync20;
5533
6451
  if (args.fromDir !== void 0) {
5534
- return { kind: "local", fromDir: resolve10(args.fromDir) };
6452
+ return { kind: "local", fromDir: resolve14(args.fromDir) };
5535
6453
  }
5536
6454
  if (args.fromRemote) {
5537
6455
  return { kind: "remote", ref: args.ref };
5538
6456
  }
5539
6457
  if (bundledPayloadExists(bundled, exists)) {
5540
- return { kind: "local", fromDir: resolve10(bundled.sourceOhDir, "..") };
6458
+ return { kind: "local", fromDir: resolve14(bundled.sourceOhDir, "..") };
5541
6459
  }
5542
6460
  return {
5543
6461
  kind: "remote",
@@ -5549,7 +6467,7 @@ function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
5549
6467
  function readPayloadVersion(checkoutDir) {
5550
6468
  try {
5551
6469
  const parsed = JSON.parse(
5552
- readFileSync11(join10(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
6470
+ readFileSync12(join14(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
5553
6471
  );
5554
6472
  if (parsed && typeof parsed.version === "string") return parsed.version;
5555
6473
  } catch {
@@ -5558,8 +6476,8 @@ function readPayloadVersion(checkoutDir) {
5558
6476
  }
5559
6477
  async function runWithRemoteSource(hooks, fn) {
5560
6478
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
5561
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync3(join10(tmpdir3(), "oh-remote-")));
5562
- const rm = hooks.rm ?? ((dir) => rmSync3(dir, { recursive: true, force: true }));
6479
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync4(join14(tmpdir3(), "oh-remote-")));
6480
+ const rm = hooks.rm ?? ((dir) => rmSync4(dir, { recursive: true, force: true }));
5563
6481
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
5564
6482
  const checkoutDir = mkdtemp();
5565
6483
  try {
@@ -5824,7 +6742,14 @@ async function main(argv) {
5824
6742
  if (a.subcommand === "status") {
5825
6743
  return await runHarnessStatus(a.name, { bin, json: a.json }, io);
5826
6744
  }
5827
- return await runHarnessInstall(a.name, { bin }, io);
6745
+ if (a.subcommand === "uninstall") {
6746
+ return await runHarnessUninstall(a.name, { bin, force: a.force }, io);
6747
+ }
6748
+ return await runHarnessInstall(
6749
+ a.name,
6750
+ { bin, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
6751
+ io
6752
+ );
5828
6753
  }
5829
6754
  if (first === "tool") {
5830
6755
  const parsed = parseToolArgs(argv.slice(1), bin);
@@ -5849,7 +6774,14 @@ async function main(argv) {
5849
6774
  if (a.subcommand === "status") {
5850
6775
  return await runToolStatus(a.name, { bin, json: a.json }, io);
5851
6776
  }
5852
- return await runToolInstall(a.name, { bin, yes: a.yes }, io);
6777
+ if (a.subcommand === "uninstall") {
6778
+ return await runToolUninstall(a.name, { bin, force: a.force }, io);
6779
+ }
6780
+ return await runToolInstall(
6781
+ a.name,
6782
+ { bin, yes: a.yes, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
6783
+ io
6784
+ );
5853
6785
  }
5854
6786
  if (first === "gateway") {
5855
6787
  const parsed = parseGatewayArgs(argv.slice(1));