@mifune/agro 0.11.0 → 0.12.0

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 +1061 -151
  2. package/dist/oh.js +1061 -151
  3. package/package.json +1 -1
package/dist/agro.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
@@ -1959,8 +1959,8 @@ function redact(token) {
1959
1959
  async function ask(question) {
1960
1960
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1961
1961
  try {
1962
- return await new Promise((resolve11) => {
1963
- rl.question(` ${question} `, (answer) => resolve11(answer.trim()));
1962
+ return await new Promise((resolve15) => {
1963
+ rl.question(` ${question} `, (answer) => resolve15(answer.trim()));
1964
1964
  });
1965
1965
  } finally {
1966
1966
  rl.close();
@@ -1972,7 +1972,7 @@ async function askSecret(question) {
1972
1972
  process.stdin.setRawMode(true);
1973
1973
  process.stdin.resume();
1974
1974
  const bytes = [];
1975
- return await new Promise((resolve11) => {
1975
+ return await new Promise((resolve15) => {
1976
1976
  const cleanup = () => {
1977
1977
  try {
1978
1978
  process.stdin.setRawMode(false);
@@ -1990,7 +1990,7 @@ async function askSecret(question) {
1990
1990
  } else if (byte === 13 || byte === 10) {
1991
1991
  cleanup();
1992
1992
  process.stdout.write("\n");
1993
- resolve11(Buffer.from(bytes).toString("utf8").trim());
1993
+ resolve15(Buffer.from(bytes).toString("utf8").trim());
1994
1994
  return;
1995
1995
  } else if (byte === 127 || byte === 8) {
1996
1996
  if (bytes.length > 0) {
@@ -2040,10 +2040,10 @@ async function askSecretPiped(question) {
2040
2040
  };
2041
2041
  process.on("SIGINT", onSigint);
2042
2042
  try {
2043
- return await new Promise((resolve11) => {
2043
+ return await new Promise((resolve15) => {
2044
2044
  rl.question(` ${question} `, (answer) => {
2045
2045
  process.stdout.write("\n");
2046
- resolve11(answer.trim());
2046
+ resolve15(answer.trim());
2047
2047
  });
2048
2048
  });
2049
2049
  } finally {
@@ -2976,6 +2976,20 @@ function defaultIdentity() {
2976
2976
  return { name: info.username, uid: info.uid };
2977
2977
  }
2978
2978
 
2979
+ // src/lib/execution/status.ts
2980
+ var RUNTIME_ABSENT_STATUS = "no container runtime";
2981
+ async function resolveTargetStatus(target) {
2982
+ try {
2983
+ return await target.status();
2984
+ } catch (err) {
2985
+ if (err instanceof ExecutionSpawnError) return RUNTIME_ABSENT_STATUS;
2986
+ throw err;
2987
+ }
2988
+ }
2989
+ function runtimeIsAbsent(status) {
2990
+ return status === RUNTIME_ABSENT_STATUS;
2991
+ }
2992
+
2979
2993
  // src/lib/execution/index.ts
2980
2994
  function resolveExecutionTarget(opts) {
2981
2995
  if (runningInsideSandbox(opts.env ?? process.env)) {
@@ -3984,6 +3998,11 @@ async function runSandboxList(opts, io) {
3984
3998
  return 0;
3985
3999
  }
3986
4000
 
4001
+ // src/commands/harness.ts
4002
+ import { existsSync as existsSync18 } from "node:fs";
4003
+ import { homedir as homedir2 } from "node:os";
4004
+ import { delimiter as delimiter2, join as join12, resolve as resolve12 } from "node:path";
4005
+
3987
4006
  // src/lib/docs.ts
3988
4007
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
3989
4008
  function sourceDocsUrl(docsPath) {
@@ -3994,7 +4013,183 @@ function sourceDocsUrl(docsPath) {
3994
4013
  return new URL(segments.map(encodeURIComponent).join("/"), SOURCE_DOCS_BASE).toString();
3995
4014
  }
3996
4015
 
4016
+ // src/lib/host-config.ts
4017
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4018
+ import { basename as basename6, dirname as dirname8, isAbsolute as isAbsolute2, join as join10, resolve as resolve10 } from "node:path";
4019
+ var HOST_CONFIG_MODE = 420;
4020
+ function hostConfigFileName(stateHome) {
4021
+ return basename6(stateHome) === GENERATIONS.legacy.userStateDir ? GENERATIONS.legacy.configFile : GENERATIONS.agro.configFile;
4022
+ }
4023
+ function hostConfigPath(env = process.env) {
4024
+ const stateHome = resolveUserStateHome(env);
4025
+ return join10(stateHome, hostConfigFileName(stateHome));
4026
+ }
4027
+ function defaultHarnessRoot(env = process.env) {
4028
+ return resolveUserStateHome(env);
4029
+ }
4030
+ function validateHostConfig(value) {
4031
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4032
+ throw new Error(`${GENERATIONS.agro.configFile}: must contain a JSON object`);
4033
+ }
4034
+ const record = value;
4035
+ if (record.version !== void 0 && record.version !== 1) {
4036
+ throw fieldError2("version", "must be 1");
4037
+ }
4038
+ const harnessRoot = record.harnessRoot;
4039
+ if (harnessRoot !== void 0) {
4040
+ if (typeof harnessRoot !== "string" || harnessRoot === "") {
4041
+ throw fieldError2("harnessRoot", "must be a non-empty string");
4042
+ }
4043
+ if (!isAbsolute2(harnessRoot)) {
4044
+ throw fieldError2("harnessRoot", "must be an absolute path");
4045
+ }
4046
+ }
4047
+ const hostHarnesses = record.hostHarnesses;
4048
+ if (hostHarnesses !== void 0) validateReceipts(hostHarnesses, "hostHarnesses", "harness");
4049
+ const hostTools = record.hostTools;
4050
+ if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool");
4051
+ return { ...record, version: 1 };
4052
+ }
4053
+ function absoluteField(value, path4) {
4054
+ if (typeof value !== "string" || value === "") {
4055
+ throw fieldError2(path4, "must be a non-empty string");
4056
+ }
4057
+ if (!isAbsolute2(value)) throw fieldError2(path4, "must be an absolute path");
4058
+ return value;
4059
+ }
4060
+ function validateReceipts(value, field, noun) {
4061
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4062
+ throw fieldError2(field, `must be a JSON object keyed by ${noun} id`);
4063
+ }
4064
+ for (const [id, receipt] of Object.entries(value)) {
4065
+ if (id === "") throw fieldError2(field, `must use a non-empty ${noun} id as each key`);
4066
+ const at = `${field}.${id}`;
4067
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
4068
+ throw fieldError2(at, "must be a JSON object");
4069
+ }
4070
+ const entry = receipt;
4071
+ absoluteField(entry.prefix, `${at}.prefix`);
4072
+ absoluteField(entry.binPath, `${at}.binPath`);
4073
+ if (typeof entry.binary !== "string" || entry.binary === "") {
4074
+ throw fieldError2(`${at}.binary`, "must be a non-empty string");
4075
+ }
4076
+ if (typeof entry.installedAt !== "string" || entry.installedAt === "") {
4077
+ throw fieldError2(`${at}.installedAt`, "must be a non-empty string");
4078
+ }
4079
+ if (entry.workspaceRoot !== void 0) {
4080
+ absoluteField(entry.workspaceRoot, `${at}.workspaceRoot`);
4081
+ }
4082
+ }
4083
+ }
4084
+ function readHostConfig(env = process.env) {
4085
+ const path4 = hostConfigPath(env);
4086
+ if (!existsSync16(path4)) return { version: 1 };
4087
+ let raw;
4088
+ try {
4089
+ raw = readFileSync11(path4, "utf8");
4090
+ } catch (error) {
4091
+ const detail = error instanceof Error ? error.message : String(error);
4092
+ throw new Error(`could not read ${path4}: ${detail}`);
4093
+ }
4094
+ let parsed;
4095
+ try {
4096
+ parsed = JSON.parse(raw);
4097
+ } catch {
4098
+ throw new Error(`${basename6(path4)} is not valid JSON: ${path4}`);
4099
+ }
4100
+ return validateHostConfig(parsed);
4101
+ }
4102
+ function writeHostConfig(config, env = process.env) {
4103
+ const path4 = hostConfigPath(env);
4104
+ const validated = validateHostConfig({ ...config, version: 1 });
4105
+ const body = `${JSON.stringify(validated, null, 2)}
4106
+ `;
4107
+ const tmp = `${path4}.tmp.${process.pid}`;
4108
+ mkdirSync7(dirname8(path4), { recursive: true });
4109
+ try {
4110
+ writeFileSync8(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4111
+ renameSync5(tmp, path4);
4112
+ } catch (error) {
4113
+ try {
4114
+ unlinkSync5(tmp);
4115
+ } catch {
4116
+ }
4117
+ throw error;
4118
+ }
4119
+ }
4120
+ function resolveHarnessRoot(explicit, env = process.env) {
4121
+ if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4122
+ const configured = readHostConfig(env).harnessRoot;
4123
+ if (typeof configured === "string" && configured !== "") return resolve10(configured);
4124
+ return defaultHarnessRoot(env);
4125
+ }
4126
+ function fieldError2(path4, requirement) {
4127
+ return new Error(`${GENERATIONS.agro.configFile}: ${path4} ${requirement}`);
4128
+ }
4129
+
4130
+ // src/lib/host-workspace.ts
4131
+ 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";
4132
+ import { dirname as dirname9, join as join11, resolve as resolve11 } from "node:path";
4133
+ var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4134
+ var IGNORABLE_ENTRIES = [
4135
+ REGISTRY_SUBDIR,
4136
+ "escalate",
4137
+ GENERATIONS.agro.configFile,
4138
+ GENERATIONS.legacy.configFile
4139
+ ];
4140
+ function isIgnorableEntry(name) {
4141
+ return name.startsWith(".") || IGNORABLE_ENTRIES.includes(name);
4142
+ }
4143
+ function assertCloned(result, root) {
4144
+ const status = result.error ? result.error.code ?? "spawn failed" : result.status;
4145
+ if (result.error || result.status !== 0) {
4146
+ throw new Error(`could not clone ${AGRO_REPO_URL} into ${root} (exit status ${String(status)})`);
4147
+ }
4148
+ }
4149
+ function cloneInto(target, run, root) {
4150
+ assertCloned(run("git", ["clone", AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4151
+ }
4152
+ function cloneThroughStaging(root, run) {
4153
+ const staging = mkdtempSync3(join11(dirname9(root), "agro-clone-"));
4154
+ try {
4155
+ const checkout = join11(staging, "agro");
4156
+ cloneInto(checkout, run, root);
4157
+ for (const name of readdirSync4(checkout)) {
4158
+ const destination = join11(root, name);
4159
+ if (existsSync17(destination)) {
4160
+ throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4161
+ }
4162
+ renameSync6(join11(checkout, name), destination);
4163
+ }
4164
+ } finally {
4165
+ rmSync3(staging, { recursive: true, force: true });
4166
+ }
4167
+ }
4168
+ function ensureHostWorkspace(path4, run = spawnRunner) {
4169
+ const root = resolve11(path4);
4170
+ if (existsSync17(join11(root, ".git"))) return { root, action: "reused" };
4171
+ const stats = statSync5(root, { throwIfNoEntry: false });
4172
+ if (stats === void 0) {
4173
+ mkdirSync8(dirname9(root), { recursive: true });
4174
+ cloneInto(root, run, root);
4175
+ return { root, action: "cloned" };
4176
+ }
4177
+ if (!stats.isDirectory()) {
4178
+ throw new Error(`${root} exists and is not a directory \u2014 choose another harness root`);
4179
+ }
4180
+ const blocking = readdirSync4(root).filter((name) => !isIgnorableEntry(name));
4181
+ if (blocking.length > 0) {
4182
+ throw new Error(
4183
+ `${root} holds files but no git checkout (${blocking.sort().join(", ")}) \u2014 choose an empty directory or an existing AGRO checkout`
4184
+ );
4185
+ }
4186
+ cloneThroughStaging(root, run);
4187
+ return { root, action: "cloned" };
4188
+ }
4189
+
3997
4190
  // src/lib/harnesses/catalog.ts
4191
+ var SANDBOX_HARNESS_PREFIX = "/home/sandbox/.local";
4192
+ var HARNESS_PREFIX_TOKEN = "{{prefix}}";
3998
4193
  var HARNESS_CATALOG = [
3999
4194
  {
4000
4195
  id: "claude-code",
@@ -4003,13 +4198,14 @@ var HARNESS_CATALOG = [
4003
4198
  installArgv: [
4004
4199
  "npm",
4005
4200
  "--prefix",
4006
- "/home/sandbox/.local",
4201
+ HARNESS_PREFIX_TOKEN,
4007
4202
  "install",
4008
4203
  "-g",
4009
4204
  "@anthropic-ai/claude-code"
4010
4205
  ],
4011
4206
  installUser: "sandbox",
4012
4207
  verifyArgv: ["claude", "--version"],
4208
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@anthropic-ai/claude-code"],
4013
4209
  docsPath: "docs/harnesses/claude-code.md",
4014
4210
  kind: "installable"
4015
4211
  },
@@ -4020,13 +4216,14 @@ var HARNESS_CATALOG = [
4020
4216
  installArgv: [
4021
4217
  "npm",
4022
4218
  "--prefix",
4023
- "/home/sandbox/.local",
4219
+ HARNESS_PREFIX_TOKEN,
4024
4220
  "install",
4025
4221
  "-g",
4026
4222
  "@openai/codex"
4027
4223
  ],
4028
4224
  installUser: "sandbox",
4029
4225
  verifyArgv: ["codex", "--version"],
4226
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@openai/codex"],
4030
4227
  docsPath: "docs/harnesses/codex.md",
4031
4228
  kind: "installable"
4032
4229
  },
@@ -4037,7 +4234,7 @@ var HARNESS_CATALOG = [
4037
4234
  installArgv: [
4038
4235
  "npm",
4039
4236
  "--prefix",
4040
- "/home/sandbox/.local",
4237
+ HARNESS_PREFIX_TOKEN,
4041
4238
  "install",
4042
4239
  "-g",
4043
4240
  "--ignore-scripts",
@@ -4045,6 +4242,7 @@ var HARNESS_CATALOG = [
4045
4242
  ],
4046
4243
  installUser: "sandbox",
4047
4244
  verifyArgv: ["pi", "--version"],
4245
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "@earendil-works/pi-coding-agent"],
4048
4246
  docsPath: "docs/harnesses/pi.md",
4049
4247
  kind: "installable"
4050
4248
  },
@@ -4055,13 +4253,14 @@ var HARNESS_CATALOG = [
4055
4253
  installArgv: [
4056
4254
  "npm",
4057
4255
  "--prefix",
4058
- "/home/sandbox/.local",
4256
+ HARNESS_PREFIX_TOKEN,
4059
4257
  "install",
4060
4258
  "-g",
4061
4259
  "opencode-ai"
4062
4260
  ],
4063
4261
  installUser: "sandbox",
4064
4262
  verifyArgv: ["opencode", "--version"],
4263
+ uninstallArgv: ["npm", "--prefix", HARNESS_PREFIX_TOKEN, "uninstall", "-g", "opencode-ai"],
4065
4264
  docsPath: "docs/harnesses/opencode.md",
4066
4265
  kind: "installable"
4067
4266
  },
@@ -4072,10 +4271,11 @@ var HARNESS_CATALOG = [
4072
4271
  installArgv: [
4073
4272
  "bash",
4074
4273
  "-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"'
4274
+ `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
4275
  ],
4077
4276
  installUser: "sandbox",
4078
4277
  verifyArgv: ["grok", "--version"],
4278
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/grok`],
4079
4279
  docsPath: "docs/harnesses/grok-build.md",
4080
4280
  kind: "installable"
4081
4281
  },
@@ -4086,10 +4286,11 @@ var HARNESS_CATALOG = [
4086
4286
  installArgv: [
4087
4287
  "bash",
4088
4288
  "-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]'`
4289
+ `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
4290
  ],
4091
4291
  installUser: "sandbox",
4092
4292
  verifyArgv: ["hermes", "--version"],
4293
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/hermes`, `${HARNESS_PREFIX_TOKEN}/lib/hermes-agent`],
4093
4294
  docsPath: "docs/harnesses/hermes.md",
4094
4295
  kind: "installable"
4095
4296
  },
@@ -4100,10 +4301,11 @@ var HARNESS_CATALOG = [
4100
4301
  installArgv: [
4101
4302
  "bash",
4102
4303
  "-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'
4304
+ `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
4305
  ],
4105
4306
  installUser: "sandbox",
4106
4307
  verifyArgv: ["muse", "--version"],
4308
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/muse`],
4107
4309
  docsPath: "docs/harnesses/muse-code.md",
4108
4310
  kind: "installable"
4109
4311
  },
@@ -4114,10 +4316,11 @@ var HARNESS_CATALOG = [
4114
4316
  installArgv: [
4115
4317
  "bash",
4116
4318
  "-lc",
4117
- 'set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir "$HOME/.local/bin"'
4319
+ `set -o pipefail; curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir "${HARNESS_PREFIX_TOKEN}/bin"`
4118
4320
  ],
4119
4321
  installUser: "sandbox",
4120
4322
  verifyArgv: ["agy", "--version"],
4323
+ uninstallArgv: ["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/agy`],
4121
4324
  docsPath: "docs/harnesses/antigravity-cli.md",
4122
4325
  kind: "installable"
4123
4326
  },
@@ -4128,6 +4331,7 @@ var HARNESS_CATALOG = [
4128
4331
  installArgv: ["npx", "--yes", "t3", "--version"],
4129
4332
  installUser: "sandbox",
4130
4333
  verifyArgv: ["npx", "--no-install", "t3", "--version"],
4334
+ uninstallArgv: null,
4131
4335
  docsPath: "docs/harnesses/t3code.md",
4132
4336
  kind: "on-demand"
4133
4337
  }
@@ -4138,6 +4342,21 @@ function findHarness(id) {
4138
4342
  function harnessIds() {
4139
4343
  return HARNESS_CATALOG.map((h) => h.id);
4140
4344
  }
4345
+ function substitute(argv, prefix) {
4346
+ return argv.map((part) => part.split(HARNESS_PREFIX_TOKEN).join(prefix));
4347
+ }
4348
+ function resolveInstallArgv(entry, prefix) {
4349
+ return substitute(entry.installArgv, prefix);
4350
+ }
4351
+ function resolveVerifyArgv(entry, prefix) {
4352
+ return substitute(entry.verifyArgv, prefix);
4353
+ }
4354
+ function resolveUninstallArgv(entry, prefix) {
4355
+ return entry.uninstallArgv === null ? null : substitute(entry.uninstallArgv, prefix);
4356
+ }
4357
+ function harnessBinPath(prefix) {
4358
+ return `${prefix}/bin`;
4359
+ }
4141
4360
 
4142
4361
  // src/commands/harness.ts
4143
4362
  var PROBE_TIMEOUT_MS = 15e3;
@@ -4153,11 +4372,31 @@ function targetFor(root, run, env) {
4153
4372
  ...env ? { env } : {}
4154
4373
  });
4155
4374
  }
4156
- async function probeInstalled(target, entry, env) {
4375
+ function hostPrefix(home) {
4376
+ return join12(home, ".local");
4377
+ }
4378
+ function pathEntries(env) {
4379
+ return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
4380
+ }
4381
+ function onPath(prefix, env) {
4382
+ return pathEntries(env).includes(harnessBinPath(prefix));
4383
+ }
4384
+ function hostTargetFor(root, prefix, run, env) {
4385
+ const path4 = [harnessBinPath(prefix), ...pathEntries(env)].join(delimiter2);
4386
+ return new LocalExecutionTarget({
4387
+ projectRoot: root,
4388
+ run,
4389
+ env: { ...env, PATH: path4 }
4390
+ });
4391
+ }
4392
+ function messageOf(error) {
4393
+ return error instanceof Error ? error.message : String(error);
4394
+ }
4395
+ async function probeInstalled(target, entry, prefix, user, env) {
4157
4396
  try {
4158
4397
  const r = await target.exec({
4159
- argv: [...entry.verifyArgv],
4160
- user: "sandbox",
4398
+ argv: resolveVerifyArgv(entry, prefix),
4399
+ ...user ? { user } : {},
4161
4400
  stdio: "capture",
4162
4401
  timeoutMs: PROBE_TIMEOUT_MS,
4163
4402
  ...env ? { env } : {}
@@ -4168,33 +4407,58 @@ async function probeInstalled(target, entry, env) {
4168
4407
  throw err;
4169
4408
  }
4170
4409
  }
4171
- async function collectStates(root, run, env, only) {
4410
+ function stateOf(entry, installed, location) {
4411
+ return {
4412
+ id: entry.id,
4413
+ title: entry.title,
4414
+ binary: entry.binary,
4415
+ kind: entry.kind,
4416
+ installed,
4417
+ location,
4418
+ docs: sourceDocsUrl(entry.docsPath)
4419
+ };
4420
+ }
4421
+ function probeableHost(env, home) {
4422
+ let root;
4423
+ try {
4424
+ root = resolveHarnessRoot(void 0, env);
4425
+ } catch {
4426
+ return void 0;
4427
+ }
4428
+ const prefix = hostPrefix(home);
4429
+ if (!existsSync18(join12(root, ".git")) && !existsSync18(prefix)) return void 0;
4430
+ return { root, prefix };
4431
+ }
4432
+ async function collectStates(root, run, home, env, only) {
4172
4433
  const entries = only ? [...only] : [...HARNESS_CATALOG];
4173
4434
  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;
4435
+ const reachable = isReachable(await resolveTargetStatus(target));
4436
+ if (reachable) {
4437
+ const states2 = [];
4438
+ for (const entry of entries) {
4439
+ const installed = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox");
4440
+ states2.push(stateOf(entry, installed, installed === null ? "unknown" : "sandbox"));
4441
+ }
4442
+ return { states: states2 };
4179
4443
  }
4444
+ const host = probeableHost(env ?? process.env, home);
4445
+ if (host === void 0) {
4446
+ return { states: entries.map((entry) => stateOf(entry, null, "unknown")) };
4447
+ }
4448
+ const hostTarget = hostTargetFor(host.root, host.prefix, run, env ?? process.env);
4180
4449
  const states = [];
4181
4450
  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
- });
4451
+ const installed = await probeInstalled(hostTarget, entry, host.prefix, void 0);
4452
+ states.push(stateOf(entry, installed, installed === null ? "unknown" : "host"));
4190
4453
  }
4191
- return states;
4454
+ return { states, hostPrefix: host.prefix };
4192
4455
  }
4193
4456
  function cell(value, absent) {
4194
4457
  if (value === null) return absent;
4195
4458
  return value ? "yes" : "no";
4196
4459
  }
4197
- function renderTable(states, io, bin) {
4460
+ function renderTable(collected, io, bin) {
4461
+ const states = collected.states;
4198
4462
  const header2 = ["HARNESS", "KIND", "INSTALLED"];
4199
4463
  const rows = states.map((s) => [s.id, s.kind, cell(s.installed, "?")]);
4200
4464
  const widths = header2.map(
@@ -4203,6 +4467,14 @@ function renderTable(states, io, bin) {
4203
4467
  const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
4204
4468
  io.stdout(line(header2));
4205
4469
  for (const row of rows) io.stdout(line(row));
4470
+ if (states.some((s) => s.location === "host")) {
4471
+ io.stdout(
4472
+ `
4473
+ INSTALLED reports the host prefix ${collected.hostPrefix} \u2014 the sandbox is not running.
4474
+ `
4475
+ );
4476
+ return;
4477
+ }
4206
4478
  if (states.some((s) => s.installed === null)) {
4207
4479
  io.stdout(`
4208
4480
  INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
@@ -4212,12 +4484,12 @@ INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} san
4212
4484
  async function runHarnessList(opts, io) {
4213
4485
  const run = opts.run ?? spawnRunner;
4214
4486
  const root = resolveProjectRoot(opts.cwd);
4215
- const states = await collectStates(root, run, opts.env);
4487
+ const collected = await collectStates(root, run, homeOf(opts), opts.env);
4216
4488
  if (opts.json) {
4217
- io.stdout(`${JSON.stringify(states, null, 2)}
4489
+ io.stdout(`${JSON.stringify(collected.states, null, 2)}
4218
4490
  `);
4219
4491
  } else {
4220
- renderTable(states, io, opts.bin);
4492
+ renderTable(collected, io, opts.bin);
4221
4493
  }
4222
4494
  return 0;
4223
4495
  }
@@ -4238,24 +4510,23 @@ async function runHarnessStatus(name, opts, io) {
4238
4510
  only = findHarness(name);
4239
4511
  if (!only) return unknownHarness(name, io, opts.bin);
4240
4512
  }
4241
- const states = await collectStates(root, run, opts.env, only ? [only] : void 0);
4513
+ const collected = await collectStates(root, run, homeOf(opts), opts.env, only ? [only] : void 0);
4242
4514
  if (opts.json) {
4243
- io.stdout(`${JSON.stringify(only ? states[0] : states, null, 2)}
4515
+ io.stdout(`${JSON.stringify(only ? collected.states[0] : collected.states, null, 2)}
4244
4516
  `);
4245
4517
  } else {
4246
- renderTable(states, io, opts.bin);
4518
+ renderTable(collected, io, opts.bin);
4247
4519
  }
4248
4520
  return 0;
4249
4521
  }
4250
4522
  function hermesTargetRoot(target) {
4251
4523
  return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
4252
4524
  }
4253
- async function reconcileHermes(target, io, bin) {
4254
- const root = hermesTargetRoot(target);
4525
+ async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
4255
4526
  const result = await target.exec({
4256
4527
  argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
4257
4528
  env: aliasedEnvPair("PROJECT_ROOT", root),
4258
- user: "sandbox",
4529
+ ...user ? { user } : {},
4259
4530
  stdio: "inherit"
4260
4531
  });
4261
4532
  if (result.exitCode !== 0) {
@@ -4264,6 +4535,245 @@ async function reconcileHermes(target, io, bin) {
4264
4535
  }
4265
4536
  return result.exitCode;
4266
4537
  }
4538
+ function unreachableReason(status) {
4539
+ return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
4540
+ }
4541
+ function sandboxRefusal(bin, status) {
4542
+ if (runtimeIsAbsent(status)) {
4543
+ return `${bin} harness: no container runtime is on PATH.
4544
+ `;
4545
+ }
4546
+ return `${bin} harness: the sandbox is not running (${status}).
4547
+ Start it with \`${bin} sandbox\`, then re-run this command.
4548
+ `;
4549
+ }
4550
+ function homeOf(opts) {
4551
+ return (opts.homedir ?? homedir2)();
4552
+ }
4553
+ function isInteractive(opts) {
4554
+ return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
4555
+ }
4556
+ async function installOnHost(entry, opts, io, run, status) {
4557
+ const bin = opts.bin;
4558
+ const env = opts.env ?? process.env;
4559
+ const flagged = opts.host === true || opts.path !== void 0;
4560
+ if (!flagged && !isInteractive(opts)) {
4561
+ io.stderr(
4562
+ sandboxRefusal(bin, status) + `Or install on the host with \`${bin} harness install ${entry.id} --host\`.
4563
+ `
4564
+ );
4565
+ return 1;
4566
+ }
4567
+ if (entry.kind === "on-demand") {
4568
+ io.stdout(`${entry.id}: no installation is needed \u2014 npx fetches it at each run
4569
+ `);
4570
+ return 0;
4571
+ }
4572
+ let root;
4573
+ let recorded;
4574
+ try {
4575
+ recorded = readHostConfig(env).harnessRoot;
4576
+ root = resolveHarnessRoot(opts.path, env);
4577
+ } catch (err) {
4578
+ io.stderr(`${bin} harness: ${messageOf(err)}
4579
+ `);
4580
+ return 1;
4581
+ }
4582
+ const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
4583
+ if (!flagged) {
4584
+ const ask2 = io.ask ?? ask;
4585
+ const answer = (await ask2(
4586
+ `${unreachableReason(status)} Install ${entry.title} on the host? [y/N]`
4587
+ )).trim().toLowerCase();
4588
+ if (!/^y/.test(answer)) {
4589
+ io.stderr(sandboxRefusal(bin, status));
4590
+ return 1;
4591
+ }
4592
+ if (!sticky) {
4593
+ const chosen = (await ask2(`Harness root [${root}]:`)).trim();
4594
+ if (chosen !== "") root = resolve12(chosen);
4595
+ }
4596
+ }
4597
+ if (sticky) io.stdout(`using the recorded harness root ${root}
4598
+ `);
4599
+ try {
4600
+ if (!existsSync18(join12(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4601
+ `);
4602
+ const workspace = ensureHostWorkspace(root, run);
4603
+ root = workspace.root;
4604
+ io.stdout(
4605
+ workspace.action === "cloned" ? `host workspace cloned into ${root}
4606
+ ` : `host workspace reused at ${root}
4607
+ `
4608
+ );
4609
+ } catch (err) {
4610
+ io.stderr(`${bin} harness: ${messageOf(err)}
4611
+ `);
4612
+ return 1;
4613
+ }
4614
+ const prefix = hostPrefix(homeOf(opts));
4615
+ const target = hostTargetFor(root, prefix, run, env);
4616
+ const hermes = entry.id === "hermes";
4617
+ const installEnv = hermes ? {
4618
+ ...aliasedEnvPair("PROJECT_ROOT", root),
4619
+ HERMES_HOME: `${root}/.hermes`
4620
+ } : void 0;
4621
+ if (hermes) {
4622
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4623
+ if (code !== 0) return code;
4624
+ }
4625
+ if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true) {
4626
+ io.stdout(`${entry.id}: already installed (${entry.binary})
4627
+ `);
4628
+ return 0;
4629
+ }
4630
+ io.stdout(`installing ${entry.title} on the host\u2026
4631
+ `);
4632
+ const r = await target.exec({
4633
+ argv: resolveInstallArgv(entry, prefix),
4634
+ stdio: "inherit",
4635
+ ...installEnv ? { env: installEnv } : {}
4636
+ });
4637
+ if (r.exitCode !== 0) {
4638
+ io.stderr(`${bin} harness: installing ${entry.id} failed (exit ${r.exitCode}).
4639
+ `);
4640
+ return r.exitCode;
4641
+ }
4642
+ if (hermes) {
4643
+ if (await probeInstalled(target, entry, prefix, void 0, installEnv) !== true) {
4644
+ io.stderr(`${bin} harness: Hermes installation finished but executable verification failed.
4645
+ `);
4646
+ return 1;
4647
+ }
4648
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4649
+ if (code !== 0) return code;
4650
+ }
4651
+ const receipt = {
4652
+ prefix,
4653
+ binary: entry.binary,
4654
+ binPath: harnessBinPath(prefix),
4655
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4656
+ workspaceRoot: root
4657
+ };
4658
+ try {
4659
+ const config = readHostConfig(env);
4660
+ writeHostConfig(
4661
+ {
4662
+ ...config,
4663
+ harnessRoot: root,
4664
+ hostHarnesses: { ...config.hostHarnesses ?? {}, [entry.id]: receipt }
4665
+ },
4666
+ env
4667
+ );
4668
+ } catch (err) {
4669
+ io.stderr(`${bin} harness: could not record the install: ${messageOf(err)}
4670
+ `);
4671
+ return 1;
4672
+ }
4673
+ io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
4674
+ `);
4675
+ if (!onPath(prefix, env)) {
4676
+ io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
4677
+ `);
4678
+ }
4679
+ io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
4680
+ `);
4681
+ return 0;
4682
+ }
4683
+ async function removeHarness(entry, target, prefix, user, opts, io) {
4684
+ const argv = resolveUninstallArgv(entry, prefix);
4685
+ if (argv === null) {
4686
+ io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4687
+ `);
4688
+ return { code: 0, dropReceipt: false };
4689
+ }
4690
+ if (await probeInstalled(target, entry, prefix, user) !== true) {
4691
+ io.stdout(`${entry.id}: not installed (${entry.binary})
4692
+ `);
4693
+ return { code: 0, dropReceipt: true };
4694
+ }
4695
+ if (isInteractive(opts)) {
4696
+ const ask2 = io.ask ?? ask;
4697
+ const answer = (await ask2(`Remove ${entry.title} from ${prefix}? [y/N]`)).trim().toLowerCase();
4698
+ if (!/^y/.test(answer)) {
4699
+ io.stderr(`${opts.bin} harness: removed nothing.
4700
+ `);
4701
+ return { code: 1, dropReceipt: false };
4702
+ }
4703
+ }
4704
+ io.stdout(`removing ${entry.title} from ${prefix}\u2026
4705
+ `);
4706
+ const r = await target.exec({
4707
+ argv,
4708
+ ...user ? { user } : {},
4709
+ stdio: "inherit"
4710
+ });
4711
+ if (r.exitCode !== 0) {
4712
+ io.stderr(`${opts.bin} harness: removing ${entry.id} failed (exit ${r.exitCode}).
4713
+ `);
4714
+ return { code: r.exitCode, dropReceipt: false };
4715
+ }
4716
+ io.stdout(`${entry.id}: removed from ${prefix}
4717
+ `);
4718
+ return { code: 0, dropReceipt: true };
4719
+ }
4720
+ async function uninstallOnHost(entry, opts, io, run) {
4721
+ const bin = opts.bin;
4722
+ const env = opts.env ?? process.env;
4723
+ const computed = hostPrefix(homeOf(opts));
4724
+ if (resolveUninstallArgv(entry, computed) === null) {
4725
+ io.stdout(`${entry.id}: nothing to remove \u2014 npx fetches it at each run
4726
+ `);
4727
+ return 0;
4728
+ }
4729
+ let config;
4730
+ let workspace;
4731
+ try {
4732
+ config = readHostConfig(env);
4733
+ workspace = resolveHarnessRoot(void 0, env);
4734
+ } catch (err) {
4735
+ io.stderr(`${bin} harness: ${messageOf(err)}
4736
+ `);
4737
+ return 1;
4738
+ }
4739
+ const receipt = config.hostHarnesses?.[entry.id];
4740
+ if (receipt === void 0 && opts.force !== true) {
4741
+ io.stderr(
4742
+ `${bin} harness: no record of installing ${entry.id} on this host.
4743
+ Removing it could delete a harness you installed yourself. Re-run with \`--force\` to remove it from ${computed}.
4744
+ `
4745
+ );
4746
+ return 1;
4747
+ }
4748
+ const prefix = receipt?.prefix ?? computed;
4749
+ const target = hostTargetFor(receipt?.workspaceRoot ?? workspace, prefix, run, env);
4750
+ const outcome = await removeHarness(entry, target, prefix, void 0, opts, io);
4751
+ if (outcome.dropReceipt && receipt !== void 0) {
4752
+ const remaining = { ...config.hostHarnesses ?? {} };
4753
+ delete remaining[entry.id];
4754
+ try {
4755
+ writeHostConfig({ ...config, hostHarnesses: remaining }, env);
4756
+ } catch (err) {
4757
+ io.stderr(`${bin} harness: could not clear the install record: ${messageOf(err)}
4758
+ `);
4759
+ return 1;
4760
+ }
4761
+ io.stdout(`${entry.id}: cleared the host install record
4762
+ `);
4763
+ }
4764
+ return outcome.code;
4765
+ }
4766
+ async function runHarnessUninstall(name, opts, io) {
4767
+ const run = opts.run ?? spawnRunner;
4768
+ const root = resolveProjectRoot(opts.cwd);
4769
+ const entry = findHarness(name);
4770
+ if (!entry) return unknownHarness(name, io, opts.bin);
4771
+ const target = targetFor(root, run, opts.env);
4772
+ if (!isReachable(await resolveTargetStatus(target))) {
4773
+ return await uninstallOnHost(entry, opts, io, run);
4774
+ }
4775
+ return (await removeHarness(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
4776
+ }
4267
4777
  async function runHarnessInstall(name, opts, io) {
4268
4778
  const run = opts.run ?? spawnRunner;
4269
4779
  const env = opts.env ?? process.env;
@@ -4274,23 +4784,9 @@ async function runHarnessInstall(name, opts, io) {
4274
4784
  const entry = findHarness(name);
4275
4785
  if (!entry) return unknownHarness(name, io, opts.bin);
4276
4786
  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
- }
4787
+ const status = await resolveTargetStatus(target);
4287
4788
  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;
4789
+ return await installOnHost(entry, opts, io, run, status);
4294
4790
  }
4295
4791
  const hermes = entry.id === "hermes";
4296
4792
  const installEnv = hermes ? {
@@ -4298,10 +4794,10 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4298
4794
  HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
4299
4795
  } : void 0;
4300
4796
  if (hermes) {
4301
- const code = await reconcileHermes(target, io, opts.bin);
4797
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4302
4798
  if (code !== 0) return code;
4303
4799
  }
4304
- const already = await probeInstalled(target, entry, installEnv);
4800
+ const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
4305
4801
  if (already === true) {
4306
4802
  io.stdout(`${entry.id}: already installed (${entry.binary})
4307
4803
  `);
@@ -4314,7 +4810,7 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4314
4810
  io.stdout(`installing ${entry.title} into the sandbox\u2026
4315
4811
  `);
4316
4812
  const r = await target.exec({
4317
- argv: [...entry.installArgv],
4813
+ argv: resolveInstallArgv(entry, SANDBOX_HARNESS_PREFIX),
4318
4814
  user: entry.installUser,
4319
4815
  stdio: "inherit",
4320
4816
  ...installEnv ? { env: installEnv } : {}
@@ -4325,12 +4821,12 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4325
4821
  return r.exitCode;
4326
4822
  }
4327
4823
  if (hermes) {
4328
- if (await probeInstalled(target, entry, installEnv) !== true) {
4824
+ if (await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv) !== true) {
4329
4825
  io.stderr(`${opts.bin} harness: Hermes installation finished but executable verification failed.
4330
4826
  `);
4331
4827
  return 1;
4332
4828
  }
4333
- const code = await reconcileHermes(target, io, opts.bin);
4829
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4334
4830
  if (code !== 0) return code;
4335
4831
  }
4336
4832
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
@@ -4338,8 +4834,14 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4338
4834
  return 0;
4339
4835
  }
4340
4836
 
4837
+ // src/commands/tool.ts
4838
+ import { existsSync as existsSync19 } from "node:fs";
4839
+ import { homedir as homedir3 } from "node:os";
4840
+ import { delimiter as delimiter3, join as join13, resolve as resolve13 } from "node:path";
4841
+
4341
4842
  // src/lib/tools/catalog.ts
4342
4843
  var TOOLS_DOC = "docs/installation.md";
4844
+ var TOOL_HOST_PLATFORM = "linux";
4343
4845
  var TOOL_CATALOG = Object.freeze([
4344
4846
  Object.freeze({
4345
4847
  id: "agent-browser",
@@ -4354,6 +4856,9 @@ var TOOL_CATALOG = Object.freeze([
4354
4856
  ]),
4355
4857
  installUser: "sandbox",
4356
4858
  downloadSize: "~1 GB",
4859
+ hostCapable: false,
4860
+ 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.",
4861
+ uninstallArgv: Object.freeze(["bash", "-lc", "pnpm remove -g agent-browser"]),
4357
4862
  docsPath: TOOLS_DOC
4358
4863
  }),
4359
4864
  Object.freeze({
@@ -4385,6 +4890,8 @@ var TOOL_CATALOG = Object.freeze([
4385
4890
  ].join("\n")
4386
4891
  ]),
4387
4892
  installUser: "sandbox",
4893
+ hostCapable: true,
4894
+ uninstallArgv: Object.freeze(["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/herdr`]),
4388
4895
  docsPath: TOOLS_DOC
4389
4896
  }),
4390
4897
  Object.freeze({
@@ -4417,6 +4924,8 @@ var TOOL_CATALOG = Object.freeze([
4417
4924
  ].join("\n")
4418
4925
  ]),
4419
4926
  installUser: "sandbox",
4927
+ hostCapable: true,
4928
+ uninstallArgv: Object.freeze(["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/cloudflared`]),
4420
4929
  docsPath: TOOLS_DOC
4421
4930
  }),
4422
4931
  Object.freeze({
@@ -4442,6 +4951,13 @@ var TOOL_CATALOG = Object.freeze([
4442
4951
  ].join("\n")
4443
4952
  ]),
4444
4953
  installUser: "sandbox",
4954
+ hostCapable: true,
4955
+ uninstallArgv: Object.freeze([
4956
+ "rm",
4957
+ "-rf",
4958
+ `${HARNESS_PREFIX_TOKEN}/bin/msb`,
4959
+ `${HARNESS_PREFIX_TOKEN}/microsandbox`
4960
+ ]),
4445
4961
  docsPath: "docs/runtimes/microsandbox.md"
4446
4962
  }),
4447
4963
  Object.freeze({
@@ -4452,6 +4968,8 @@ var TOOL_CATALOG = Object.freeze([
4452
4968
  verifyArgv: Object.freeze(["bash", "-lc", "command -v docker >/dev/null"]),
4453
4969
  versionArgv: Object.freeze(["docker", "--version"]),
4454
4970
  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.`,
4971
+ hostCapable: false,
4972
+ uninstallArgv: null,
4455
4973
  docsPath: TOOLS_DOC
4456
4974
  }),
4457
4975
  Object.freeze({
@@ -4462,6 +4980,8 @@ var TOOL_CATALOG = Object.freeze([
4462
4980
  verifyArgv: Object.freeze(["bash", "-lc", "command -v gh >/dev/null"]),
4463
4981
  versionArgv: Object.freeze(["gh", "--version"]),
4464
4982
  notInstallableReason: () => "The GitHub CLI is installed in the base image. Run `gh auth login` inside the sandbox to authenticate it.",
4983
+ hostCapable: false,
4984
+ uninstallArgv: null,
4465
4985
  docsPath: TOOLS_DOC
4466
4986
  }),
4467
4987
  Object.freeze({
@@ -4493,6 +5013,13 @@ install -m 0755 "$tmp/tailscale_1.102.3_$arch/tailscaled" "$prefix/bin/tailscale
4493
5013
  install -d -m 0700 "$HOME/.tailscale"`
4494
5014
  ]),
4495
5015
  installUser: "sandbox",
5016
+ hostCapable: true,
5017
+ uninstallArgv: Object.freeze([
5018
+ "rm",
5019
+ "-rf",
5020
+ `${HARNESS_PREFIX_TOKEN}/bin/tailscale`,
5021
+ `${HARNESS_PREFIX_TOKEN}/bin/tailscaled`
5022
+ ]),
4496
5023
  docsPath: TOOLS_DOC
4497
5024
  })
4498
5025
  ]);
@@ -4505,6 +5032,12 @@ function toolIds() {
4505
5032
  function installableToolIds() {
4506
5033
  return TOOL_CATALOG.filter((t) => t.installArgv !== void 0).map((t) => t.id);
4507
5034
  }
5035
+ function hostCapableToolIds() {
5036
+ return TOOL_CATALOG.filter((t) => t.hostCapable).map((t) => t.id);
5037
+ }
5038
+ function resolveToolUninstallArgv(entry, prefix) {
5039
+ return entry.uninstallArgv === null ? null : entry.uninstallArgv.map((part) => part.split(HARNESS_PREFIX_TOKEN).join(prefix));
5040
+ }
4508
5041
 
4509
5042
  // src/commands/tool.ts
4510
5043
  function isReachable2(status) {
@@ -4519,56 +5052,116 @@ function targetFor2(root, run, env) {
4519
5052
  ...env ? { env } : {}
4520
5053
  });
4521
5054
  }
4522
- async function tryExec(target, argv, user) {
5055
+ function hostPrefix2(home) {
5056
+ return join13(home, ".local");
5057
+ }
5058
+ function pathEntries2(env) {
5059
+ return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
5060
+ }
5061
+ function onPath2(prefix, env) {
5062
+ return pathEntries2(env).includes(harnessBinPath(prefix));
5063
+ }
5064
+ function hostTargetFor2(root, prefix, run, env) {
5065
+ const path4 = [harnessBinPath(prefix), ...pathEntries2(env)].join(delimiter3);
5066
+ return new LocalExecutionTarget({
5067
+ projectRoot: root,
5068
+ run,
5069
+ env: { ...env, PATH: path4 }
5070
+ });
5071
+ }
5072
+ function messageOf2(error) {
5073
+ return error instanceof Error ? error.message : String(error);
5074
+ }
5075
+ function homeOf2(opts) {
5076
+ return (opts.homedir ?? homedir3)();
5077
+ }
5078
+ function platformOf(opts) {
5079
+ return opts.platform ?? process.platform;
5080
+ }
5081
+ function isInteractive2(opts) {
5082
+ return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
5083
+ }
5084
+ async function tryExec(target, argv, user, env) {
4523
5085
  try {
4524
- const r = await target.exec({ argv: [...argv], user, stdio: "capture" });
5086
+ const r = await target.exec({
5087
+ argv: [...argv],
5088
+ ...user ? { user } : {},
5089
+ stdio: "capture",
5090
+ ...env ? { env } : {}
5091
+ });
4525
5092
  return { exitCode: r.exitCode, stdout: r.stdout };
4526
5093
  } catch (err) {
4527
5094
  if (err instanceof ExecutionSpawnError) return null;
4528
5095
  throw err;
4529
5096
  }
4530
5097
  }
4531
- async function probeInstalled2(target, entry) {
4532
- const r = await tryExec(target, entry.verifyArgv, "sandbox");
5098
+ async function probeInstalled2(target, entry, user, env) {
5099
+ const r = await tryExec(target, entry.verifyArgv, user, env);
4533
5100
  return r === null ? null : r.exitCode === 0;
4534
5101
  }
4535
- async function probeVersion(target, entry) {
5102
+ async function probeVersion(target, entry, user) {
4536
5103
  if (entry.versionArgv === void 0) return null;
4537
- const r = await tryExec(target, entry.versionArgv, "sandbox");
5104
+ const r = await tryExec(target, entry.versionArgv, user);
4538
5105
  if (r === null || r.exitCode !== 0) return null;
4539
5106
  const first = r.stdout.trim().split("\n")[0] ?? "";
4540
5107
  return first === "" ? null : first;
4541
5108
  }
4542
- async function collectRows(root, run, env, only) {
5109
+ function rowOf(entry, installed, version, location) {
5110
+ return {
5111
+ id: entry.id,
5112
+ title: entry.title,
5113
+ binary: entry.binary,
5114
+ kind: entry.kind,
5115
+ installed,
5116
+ version,
5117
+ installable: entry.installArgv !== void 0,
5118
+ hostCapable: entry.hostCapable,
5119
+ location,
5120
+ docs: sourceDocsUrl(entry.docsPath)
5121
+ };
5122
+ }
5123
+ function probeableHost2(env, home) {
5124
+ let root;
5125
+ try {
5126
+ root = resolveHarnessRoot(void 0, env);
5127
+ } catch {
5128
+ return void 0;
5129
+ }
5130
+ const prefix = hostPrefix2(home);
5131
+ if (!existsSync19(join13(root, ".git")) && !existsSync19(prefix)) return void 0;
5132
+ return { root, prefix };
5133
+ }
5134
+ async function collectRows(root, run, home, env, only) {
4543
5135
  const entries = only ? [...only] : [...TOOL_CATALOG];
4544
5136
  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;
5137
+ if (isReachable2(await resolveTargetStatus(target))) {
5138
+ const rows2 = [];
5139
+ for (const entry of entries) {
5140
+ const installed = await probeInstalled2(target, entry, "sandbox");
5141
+ const version = installed === true ? await probeVersion(target, entry, "sandbox") : null;
5142
+ rows2.push(rowOf(entry, installed, version, installed === null ? "unknown" : "sandbox"));
5143
+ }
5144
+ return { rows: rows2 };
4550
5145
  }
5146
+ const host = probeableHost2(env ?? process.env, home);
5147
+ if (host === void 0) {
5148
+ return { rows: entries.map((entry) => rowOf(entry, null, null, "unknown")) };
5149
+ }
5150
+ const hostTarget = hostTargetFor2(host.root, host.prefix, run, env ?? process.env);
4551
5151
  const rows = [];
4552
5152
  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
- });
5153
+ const installed = await probeInstalled2(hostTarget, entry, void 0);
5154
+ const version = installed === true ? await probeVersion(hostTarget, entry, void 0) : null;
5155
+ rows.push(rowOf(entry, installed, version, installed === null ? "unknown" : "host"));
4564
5156
  }
4565
- return rows;
5157
+ return { rows, hostPrefix: host.prefix };
4566
5158
  }
4567
5159
  function cell2(value, absent) {
4568
5160
  if (value === null) return absent;
4569
5161
  return value ? "yes" : "no";
4570
5162
  }
4571
- function renderTable2(rows, io, bin) {
5163
+ function renderTable2(collected, io, bin) {
5164
+ const rows = collected.rows;
4572
5165
  const header2 = ["TOOL", "KIND", "INSTALLED"];
4573
5166
  const body = rows.map((r) => [r.id, r.kind, cell2(r.installed, "?")]);
4574
5167
  const widths = header2.map(
@@ -4577,21 +5170,31 @@ function renderTable2(rows, io, bin) {
4577
5170
  const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
4578
5171
  io.stdout(line(header2));
4579
5172
  for (const row of body) io.stdout(line(row));
5173
+ if (rows.some((r) => r.location === "host")) {
5174
+ io.stdout(
5175
+ `
5176
+ INSTALLED reports the host prefix ${collected.hostPrefix} \u2014 the sandbox is not running.
5177
+ `
5178
+ );
5179
+ return;
5180
+ }
4580
5181
  if (rows.some((r) => r.installed === null)) {
4581
5182
  io.stdout(`
4582
5183
  INSTALLED is \`?\` \u2014 the sandbox is not running. Start it with \`${bin} sandbox\`.
4583
5184
  `);
4584
5185
  }
4585
5186
  }
4586
- function renderDetail(rows, io, bin) {
4587
- renderTable2(rows, io, bin);
4588
- for (const r of rows) {
5187
+ function renderDetail(collected, io, bin) {
5188
+ renderTable2(collected, io, bin);
5189
+ for (const r of collected.rows) {
4589
5190
  io.stdout(`
4590
5191
  ${r.id} \u2014 ${r.title}
4591
5192
  `);
4592
5193
  io.stdout(` version: ${r.version ?? "\u2014"}
4593
5194
  `);
4594
5195
  io.stdout(` installable: ${r.installable ? "yes" : "no"}
5196
+ `);
5197
+ io.stdout(` on the host: ${r.hostCapable ? "yes" : "no"}
4595
5198
  `);
4596
5199
  io.stdout(` see ${r.docs}
4597
5200
  `);
@@ -4600,12 +5203,12 @@ ${r.id} \u2014 ${r.title}
4600
5203
  async function runToolList(opts, io) {
4601
5204
  const run = opts.run ?? spawnRunner;
4602
5205
  const root = resolveProjectRoot(opts.cwd);
4603
- const rows = await collectRows(root, run, opts.env);
5206
+ const collected = await collectRows(root, run, homeOf2(opts), opts.env);
4604
5207
  if (opts.json) {
4605
- io.stdout(`${JSON.stringify(rows, null, 2)}
5208
+ io.stdout(`${JSON.stringify(collected.rows, null, 2)}
4606
5209
  `);
4607
5210
  } else {
4608
- renderTable2(rows, io, opts.bin);
5211
+ renderTable2(collected, io, opts.bin);
4609
5212
  }
4610
5213
  return 0;
4611
5214
  }
@@ -4626,12 +5229,12 @@ async function runToolStatus(name, opts, io) {
4626
5229
  only = findTool(name);
4627
5230
  if (!only) return unknownTool(name, io, opts.bin);
4628
5231
  }
4629
- const rows = await collectRows(root, run, opts.env, only ? [only] : void 0);
5232
+ const collected = await collectRows(root, run, homeOf2(opts), opts.env, only ? [only] : void 0);
4630
5233
  if (opts.json) {
4631
- io.stdout(`${JSON.stringify(only ? rows[0] : rows, null, 2)}
5234
+ io.stdout(`${JSON.stringify(only ? collected.rows[0] : collected.rows, null, 2)}
4632
5235
  `);
4633
5236
  } else {
4634
- renderDetail(rows, io, opts.bin);
5237
+ renderDetail(collected, io, opts.bin);
4635
5238
  }
4636
5239
  return 0;
4637
5240
  }
@@ -4648,6 +5251,151 @@ Re-run with --yes to accept the download.
4648
5251
  );
4649
5252
  return false;
4650
5253
  }
5254
+ function unreachableReason2(status) {
5255
+ return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
5256
+ }
5257
+ function sandboxRefusal2(bin, status) {
5258
+ if (runtimeIsAbsent(status)) {
5259
+ return `${bin} tool: no container runtime is on PATH.
5260
+ `;
5261
+ }
5262
+ return `${bin} tool: the sandbox is not running (${status}).
5263
+ Start it with \`${bin} sandbox\`, then re-run this command.
5264
+ `;
5265
+ }
5266
+ function hostCapableList(bin) {
5267
+ return `Tools that install on the host:
5268
+ ${hostCapableToolIds().map((t) => ` ${bin} tool install ${t} --host`).join("\n")}
5269
+ `;
5270
+ }
5271
+ async function installOnHost2(entry, opts, io, run, status) {
5272
+ const bin = opts.bin;
5273
+ const env = opts.env ?? process.env;
5274
+ const flagged = opts.host === true || opts.path !== void 0;
5275
+ if (!entry.hostCapable) {
5276
+ io.stderr(sandboxRefusal2(bin, status));
5277
+ io.stderr(`
5278
+ ${bin} tool: ${entry.id} cannot be installed on the host.
5279
+ `);
5280
+ io.stderr(`${(entry.notHostCapableReason ?? entry.notInstallableReason)?.(bin) ?? ""}
5281
+
5282
+ `);
5283
+ io.stderr(hostCapableList(bin));
5284
+ return 1;
5285
+ }
5286
+ const platform = platformOf(opts);
5287
+ if (platform !== TOOL_HOST_PLATFORM) {
5288
+ io.stderr(sandboxRefusal2(bin, status));
5289
+ io.stderr(
5290
+ `
5291
+ ${bin} tool: a host install needs ${TOOL_HOST_PLATFORM}; this host is ${platform}.
5292
+ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with your own package manager, or start the sandbox.
5293
+ `
5294
+ );
5295
+ return 1;
5296
+ }
5297
+ if (!flagged && !isInteractive2(opts)) {
5298
+ io.stderr(
5299
+ sandboxRefusal2(bin, status) + `Or install on the host with \`${bin} tool install ${entry.id} --host\`.
5300
+ `
5301
+ );
5302
+ return 1;
5303
+ }
5304
+ let root;
5305
+ let recorded;
5306
+ try {
5307
+ recorded = readHostConfig(env).harnessRoot;
5308
+ root = resolveHarnessRoot(opts.path, env);
5309
+ } catch (err) {
5310
+ io.stderr(`${bin} tool: ${messageOf2(err)}
5311
+ `);
5312
+ return 1;
5313
+ }
5314
+ const sticky = opts.path === void 0 && recorded !== void 0 && recorded !== "";
5315
+ if (!flagged) {
5316
+ const ask2 = io.ask ?? ask;
5317
+ const answer = (await ask2(
5318
+ `${unreachableReason2(status)} Install ${entry.title} on the host? [y/N]`
5319
+ )).trim().toLowerCase();
5320
+ if (!/^y/.test(answer)) {
5321
+ io.stderr(sandboxRefusal2(bin, status));
5322
+ return 1;
5323
+ }
5324
+ if (!sticky) {
5325
+ const chosen = (await ask2(`Harness root [${root}]:`)).trim();
5326
+ if (chosen !== "") root = resolve13(chosen);
5327
+ }
5328
+ }
5329
+ if (sticky) io.stdout(`using the recorded harness root ${root}
5330
+ `);
5331
+ try {
5332
+ if (!existsSync19(join13(root, ".git"))) io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
5333
+ `);
5334
+ const workspace = ensureHostWorkspace(root, run);
5335
+ root = workspace.root;
5336
+ io.stdout(
5337
+ workspace.action === "cloned" ? `host workspace cloned into ${root}
5338
+ ` : `host workspace reused at ${root}
5339
+ `
5340
+ );
5341
+ } catch (err) {
5342
+ io.stderr(`${bin} tool: ${messageOf2(err)}
5343
+ `);
5344
+ return 1;
5345
+ }
5346
+ const prefix = hostPrefix2(homeOf2(opts));
5347
+ const installEnv = { NPM_USER_PREFIX: prefix };
5348
+ const target = hostTargetFor2(root, prefix, run, env);
5349
+ if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
5350
+ io.stdout(`${entry.id}: already installed (${entry.binary})
5351
+ `);
5352
+ return 0;
5353
+ }
5354
+ if (!await confirmDownload(entry, opts, io)) return 1;
5355
+ io.stdout(`installing ${entry.title} on the host\u2026
5356
+ `);
5357
+ const r = await target.exec({
5358
+ argv: [...entry.installArgv],
5359
+ stdio: "inherit",
5360
+ env: installEnv
5361
+ });
5362
+ if (r.exitCode !== 0) {
5363
+ io.stderr(`${bin} tool: installing ${entry.id} failed (exit ${r.exitCode}).
5364
+ `);
5365
+ return r.exitCode;
5366
+ }
5367
+ const receipt = {
5368
+ prefix,
5369
+ binary: entry.binary,
5370
+ binPath: harnessBinPath(prefix),
5371
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
5372
+ workspaceRoot: root
5373
+ };
5374
+ try {
5375
+ const config = readHostConfig(env);
5376
+ writeHostConfig(
5377
+ {
5378
+ ...config,
5379
+ harnessRoot: root,
5380
+ hostTools: { ...config.hostTools ?? {}, [entry.id]: receipt }
5381
+ },
5382
+ env
5383
+ );
5384
+ } catch (err) {
5385
+ io.stderr(`${bin} tool: could not record the install: ${messageOf2(err)}
5386
+ `);
5387
+ return 1;
5388
+ }
5389
+ io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)}
5390
+ `);
5391
+ if (!onPath2(prefix, env)) {
5392
+ io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
5393
+ `);
5394
+ }
5395
+ io.stdout(`Run ${entry.binary} from the AGRO workspace: cd ${root}
5396
+ `);
5397
+ return 0;
5398
+ }
4651
5399
  async function runToolInstall(name, opts, io) {
4652
5400
  const run = opts.run ?? spawnRunner;
4653
5401
  const root = resolveProjectRoot(opts.cwd);
@@ -4666,25 +5414,11 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
4666
5414
  return 1;
4667
5415
  }
4668
5416
  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
- }
5417
+ const status = await resolveTargetStatus(target);
4679
5418
  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;
5419
+ return await installOnHost2(entry, opts, io, run, status);
4686
5420
  }
4687
- const already = await probeInstalled2(target, entry);
5421
+ const already = await probeInstalled2(target, entry, "sandbox");
4688
5422
  if (already === true) {
4689
5423
  io.stdout(`${entry.id}: already installed (${entry.binary})
4690
5424
  `);
@@ -4711,6 +5445,98 @@ Start it with \`${opts.bin} sandbox\`, then re-run this command.
4711
5445
  `);
4712
5446
  return 0;
4713
5447
  }
5448
+ async function removeTool(entry, target, prefix, user, opts, io) {
5449
+ const argv = resolveToolUninstallArgv(entry, prefix);
5450
+ if (await probeInstalled2(target, entry, user) !== true) {
5451
+ io.stdout(`${entry.id}: not installed (${entry.binary})
5452
+ `);
5453
+ return { code: 0, dropReceipt: true };
5454
+ }
5455
+ if (isInteractive2(opts)) {
5456
+ const ask2 = io.ask ?? ask;
5457
+ const answer = (await ask2(`Remove ${entry.title} from ${prefix}? [y/N]`)).trim().toLowerCase();
5458
+ if (!/^y/.test(answer)) {
5459
+ io.stderr(`${opts.bin} tool: removed nothing.
5460
+ `);
5461
+ return { code: 1, dropReceipt: false };
5462
+ }
5463
+ }
5464
+ io.stdout(`removing ${entry.title} from ${prefix}\u2026
5465
+ `);
5466
+ const r = await target.exec({
5467
+ argv,
5468
+ ...user ? { user } : {},
5469
+ stdio: "inherit"
5470
+ });
5471
+ if (r.exitCode !== 0) {
5472
+ io.stderr(`${opts.bin} tool: removing ${entry.id} failed (exit ${r.exitCode}).
5473
+ `);
5474
+ return { code: r.exitCode, dropReceipt: false };
5475
+ }
5476
+ io.stdout(`${entry.id}: removed from ${prefix}
5477
+ `);
5478
+ return { code: 0, dropReceipt: true };
5479
+ }
5480
+ async function uninstallOnHost2(entry, opts, io, run) {
5481
+ const bin = opts.bin;
5482
+ const env = opts.env ?? process.env;
5483
+ const computed = hostPrefix2(homeOf2(opts));
5484
+ let config;
5485
+ let workspace;
5486
+ try {
5487
+ config = readHostConfig(env);
5488
+ workspace = resolveHarnessRoot(void 0, env);
5489
+ } catch (err) {
5490
+ io.stderr(`${bin} tool: ${messageOf2(err)}
5491
+ `);
5492
+ return 1;
5493
+ }
5494
+ const receipt = config.hostTools?.[entry.id];
5495
+ if (receipt === void 0 && opts.force !== true) {
5496
+ io.stderr(
5497
+ `${bin} tool: no record of installing ${entry.id} on this host.
5498
+ Removing it could delete a tool you installed yourself. Re-run with \`--force\` to remove it from ${computed}.
5499
+ `
5500
+ );
5501
+ return 1;
5502
+ }
5503
+ const prefix = receipt?.prefix ?? computed;
5504
+ const target = hostTargetFor2(receipt?.workspaceRoot ?? workspace, prefix, run, env);
5505
+ const outcome = await removeTool(entry, target, prefix, void 0, opts, io);
5506
+ if (outcome.dropReceipt && receipt !== void 0) {
5507
+ const remaining = { ...config.hostTools ?? {} };
5508
+ delete remaining[entry.id];
5509
+ try {
5510
+ writeHostConfig({ ...config, hostTools: remaining }, env);
5511
+ } catch (err) {
5512
+ io.stderr(`${bin} tool: could not clear the install record: ${messageOf2(err)}
5513
+ `);
5514
+ return 1;
5515
+ }
5516
+ io.stdout(`${entry.id}: cleared the host install record
5517
+ `);
5518
+ }
5519
+ return outcome.code;
5520
+ }
5521
+ async function runToolUninstall(name, opts, io) {
5522
+ const run = opts.run ?? spawnRunner;
5523
+ const root = resolveProjectRoot(opts.cwd);
5524
+ const entry = findTool(name);
5525
+ if (!entry) return unknownTool(name, io, opts.bin);
5526
+ if (entry.uninstallArgv === null) {
5527
+ io.stderr(`${opts.bin} tool: ${entry.id} cannot be removed by this command.
5528
+
5529
+ `);
5530
+ io.stderr(`${entry.notInstallableReason?.(opts.bin) ?? ""}
5531
+ `);
5532
+ return 1;
5533
+ }
5534
+ const target = targetFor2(root, run, opts.env);
5535
+ if (!isReachable2(await resolveTargetStatus(target))) {
5536
+ return await uninstallOnHost2(entry, opts, io, run);
5537
+ }
5538
+ return (await removeTool(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
5539
+ }
4714
5540
 
4715
5541
  // src/lib/remote.ts
4716
5542
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -4776,9 +5602,9 @@ function fetchRemoteSource(opts) {
4776
5602
  }
4777
5603
 
4778
5604
  // 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)),
5605
+ var VERSION = true ? "0.12.0" : "0.0.0-dev";
5606
+ var DEFAULT_SOURCE_OH_DIR = resolve14(
5607
+ dirname10(fileURLToPath(import.meta.url)),
4782
5608
  "../.."
4783
5609
  );
4784
5610
  var INTEGRATIONS = {};
@@ -5008,16 +5834,26 @@ function printHarnessHelp(bin = LEGACY_PRODUCT.bin) {
5008
5834
 
5009
5835
  Usage:
5010
5836
  ${bin} harness list List known harnesses and their state
5011
- ${bin} harness install <name> Install a harness into the sandbox
5837
+ ${bin} harness install <name> Install a harness into the sandbox or the host
5838
+ ${bin} harness uninstall <name> Remove a harness the same way it was installed
5012
5839
  ${bin} harness status [name] Show installed state
5013
5840
 
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.
5841
+ \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
5842
+ reachable they act on the host. A host install clones the AGRO workspace into the
5843
+ harness root, then installs the harness into \`~/.local\`. \`--path <dir>\` chooses
5844
+ the harness root; a successful install records it as \`harnessRoot\` in the host
5845
+ \`${stateNames(bin).configFile}\`. The default is \`~/.agro\`, and the install prefix is
5846
+ always \`~/.local\`.
5847
+
5848
+ On the host, \`uninstall\` removes only the harness \`install\` recorded, from the
5849
+ prefix in that record. Without a record it refuses, and \`--force\` overrides. In
5850
+ the sandbox it needs no record. See \`docs/harnesses/overview.md\`.
5018
5851
 
5019
5852
  Flags:
5020
5853
  --json Machine-readable output (list/status)
5854
+ --host Install on the host when the sandbox is not running (install)
5855
+ --path <dir> Harness root for the workspace clone; implies --host (install)
5856
+ --force Remove without a recorded host install (uninstall)
5021
5857
 
5022
5858
  Harnesses:
5023
5859
  ${harnessIds().map((h) => ` ${h}`).join("\n")}
@@ -5055,19 +5891,34 @@ tunnel client, the GitHub CLI, an isolation runtime's own binary.
5055
5891
  Usage:
5056
5892
  ${bin} tool list List known tools and their state
5057
5893
  ${bin} tool status [name] Show installed state and version
5058
- ${bin} tool install <name> Install a tool into the sandbox
5894
+ ${bin} tool install <name> Install a tool into the sandbox or the host
5895
+ ${bin} tool uninstall <name> Remove a tool the same way it was installed
5059
5896
 
5060
5897
  Most tools are baked into the image and are report-only; \`install\` works on:
5061
5898
  ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5062
5899
 
5063
5900
  \`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.
5901
+ into the persistent home volume, and reports. It never rebuilds or restarts the
5902
+ sandbox. A large download is confirmed first, and a non-interactive run without
5903
+ --yes installs nothing.
5904
+
5905
+ When no sandbox is reachable, \`install\` and \`uninstall\` act on the host. A host
5906
+ install needs Linux, because every installer is Debian-specific, and it works
5907
+ only on these tools:
5908
+ ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
5909
+
5910
+ A host install clones the AGRO workspace into the harness root \u2014 \`--path <dir>\`,
5911
+ then \`harnessRoot\` in the host \`${stateNames(bin).configFile}\`, then \`~/.agro\` \u2014 and installs into
5912
+ \`~/.local\`. It records the install as \`hostTools\` in that same file. On the host,
5913
+ \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
5914
+ Without a record it refuses, and \`--force\` overrides.
5067
5915
 
5068
5916
  Flags:
5069
5917
  --yes Accept a large download without prompting
5070
5918
  --json Machine-readable output (list/status)
5919
+ --host Install on the host when the sandbox is not running (install)
5920
+ --path <dir> Harness root for the workspace clone; implies --host (install)
5921
+ --force Remove without a recorded host install (uninstall)
5071
5922
 
5072
5923
  Tools:
5073
5924
  ${toolIds().map((t) => ` ${t}`).join("\n")}
@@ -5449,72 +6300,117 @@ function parseShellArgs(rest, bin = LEGACY_PRODUCT.bin) {
5449
6300
  function parseHarnessArgs(rest, bin = LEGACY_PRODUCT.bin) {
5450
6301
  const args = {
5451
6302
  help: false,
5452
- json: false
6303
+ json: false,
6304
+ host: false,
6305
+ force: false
5453
6306
  };
5454
6307
  if (rest.length === 0 || isHelpFlag(rest[0])) {
5455
6308
  return { ok: true, args: { ...args, help: true } };
5456
6309
  }
5457
6310
  const positionals = [];
6311
+ let expectPath = false;
5458
6312
  for (const token of rest) {
5459
- if (token === "--json") {
6313
+ if (expectPath) {
6314
+ expectPath = false;
6315
+ args.path = token;
6316
+ args.host = true;
6317
+ } else if (token === "--json") {
5460
6318
  args.json = true;
6319
+ } else if (token === "--host") {
6320
+ args.host = true;
6321
+ } else if (token === "--force") {
6322
+ args.force = true;
6323
+ } else if (token === "--path") {
6324
+ expectPath = true;
6325
+ } else if (token.startsWith("--path=")) {
6326
+ args.path = token.slice("--path=".length);
6327
+ args.host = true;
5461
6328
  } else if (token.startsWith("-")) {
5462
6329
  return { ok: false, error: `${bin} harness: unknown flag "${token}"` };
5463
6330
  } else {
5464
6331
  positionals.push(token);
5465
6332
  }
5466
6333
  }
6334
+ if (expectPath || args.path === "") {
6335
+ return { ok: false, error: `${bin} harness: --path requires a directory` };
6336
+ }
5467
6337
  const [sub, name, ...extra] = positionals;
5468
- if (sub !== "list" && sub !== "install" && sub !== "status") {
6338
+ if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
5469
6339
  return {
5470
6340
  ok: false,
5471
- error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, or status`,
6341
+ error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
5472
6342
  showHelp: true
5473
6343
  };
5474
6344
  }
5475
6345
  if (extra.length > 0) {
5476
6346
  return { ok: false, error: `${bin} harness: unexpected argument "${extra[0]}"` };
5477
6347
  }
5478
- if (sub === "install" && name === void 0) {
5479
- return { ok: false, error: `${bin} harness install: a harness name is required`, showHelp: true };
6348
+ if ((sub === "install" || sub === "uninstall") && name === void 0) {
6349
+ return { ok: false, error: `${bin} harness ${sub}: a harness name is required`, showHelp: true };
5480
6350
  }
5481
6351
  if (sub === "list" && name !== void 0) {
5482
6352
  return { ok: false, error: `${bin} harness list: unexpected argument "${name}"` };
5483
6353
  }
6354
+ if (sub !== "install" && args.host) {
6355
+ return { ok: false, error: `${bin} harness ${sub}: --host and --path apply to install only` };
6356
+ }
6357
+ if (sub !== "uninstall" && args.force) {
6358
+ return { ok: false, error: `${bin} harness ${sub}: --force applies to uninstall only` };
6359
+ }
5484
6360
  args.subcommand = sub;
5485
6361
  if (name !== void 0) args.name = name;
5486
6362
  return { ok: true, args };
5487
6363
  }
5488
6364
  function parseToolArgs(rest, bin = LEGACY_PRODUCT.bin) {
5489
- const args = { help: false, yes: false, json: false };
6365
+ const args = { help: false, yes: false, json: false, host: false, force: false };
5490
6366
  if (rest.length === 0 || isHelpFlag(rest[0])) {
5491
6367
  return { ok: true, args: { ...args, help: true } };
5492
6368
  }
5493
6369
  const positionals = [];
6370
+ let expectPath = false;
5494
6371
  for (const token of rest) {
5495
- if (token === "--yes" || token === "-y") args.yes = true;
6372
+ if (expectPath) {
6373
+ expectPath = false;
6374
+ args.path = token;
6375
+ args.host = true;
6376
+ } else if (token === "--yes" || token === "-y") args.yes = true;
5496
6377
  else if (token === "--json") args.json = true;
5497
- else if (token.startsWith("-")) {
6378
+ else if (token === "--host") args.host = true;
6379
+ else if (token === "--force") args.force = true;
6380
+ else if (token === "--path") expectPath = true;
6381
+ else if (token.startsWith("--path=")) {
6382
+ args.path = token.slice("--path=".length);
6383
+ args.host = true;
6384
+ } else if (token.startsWith("-")) {
5498
6385
  return { ok: false, error: `${bin} tool: unknown flag "${token}"` };
5499
6386
  } else positionals.push(token);
5500
6387
  }
6388
+ if (expectPath || args.path === "") {
6389
+ return { ok: false, error: `${bin} tool: --path requires a directory` };
6390
+ }
5501
6391
  const [sub, name, ...extra] = positionals;
5502
- if (sub !== "list" && sub !== "install" && sub !== "status") {
6392
+ if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
5503
6393
  return {
5504
6394
  ok: false,
5505
- error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, or status`,
6395
+ error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
5506
6396
  showHelp: true
5507
6397
  };
5508
6398
  }
5509
6399
  if (extra.length > 0) {
5510
6400
  return { ok: false, error: `${bin} tool: unexpected argument "${extra[0]}"` };
5511
6401
  }
5512
- if (sub === "install" && name === void 0) {
5513
- return { ok: false, error: `${bin} tool install: a tool name is required`, showHelp: true };
6402
+ if ((sub === "install" || sub === "uninstall") && name === void 0) {
6403
+ return { ok: false, error: `${bin} tool ${sub}: a tool name is required`, showHelp: true };
5514
6404
  }
5515
6405
  if (sub === "list" && name !== void 0) {
5516
6406
  return { ok: false, error: `${bin} tool list: unexpected argument "${name}"` };
5517
6407
  }
6408
+ if (sub !== "install" && args.host) {
6409
+ return { ok: false, error: `${bin} tool ${sub}: --host and --path apply to install only` };
6410
+ }
6411
+ if (sub !== "uninstall" && args.force) {
6412
+ return { ok: false, error: `${bin} tool ${sub}: --force applies to uninstall only` };
6413
+ }
5518
6414
  args.subcommand = sub;
5519
6415
  if (name !== void 0) args.name = name;
5520
6416
  return { ok: true, args };
@@ -5525,19 +6421,19 @@ function parseGatewayArgs(rest) {
5525
6421
  }
5526
6422
  return { ok: true, args: { help: false, passthrough: [...rest] } };
5527
6423
  }
5528
- function bundledPayloadExists(bundled, exists = existsSync16) {
5529
- return exists(join10(bundled.sourceOhDir, "manifest.json"));
6424
+ function bundledPayloadExists(bundled, exists = existsSync20) {
6425
+ return exists(join14(bundled.sourceOhDir, "manifest.json"));
5530
6426
  }
5531
6427
  function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
5532
- const exists = bundled.exists ?? existsSync16;
6428
+ const exists = bundled.exists ?? existsSync20;
5533
6429
  if (args.fromDir !== void 0) {
5534
- return { kind: "local", fromDir: resolve10(args.fromDir) };
6430
+ return { kind: "local", fromDir: resolve14(args.fromDir) };
5535
6431
  }
5536
6432
  if (args.fromRemote) {
5537
6433
  return { kind: "remote", ref: args.ref };
5538
6434
  }
5539
6435
  if (bundledPayloadExists(bundled, exists)) {
5540
- return { kind: "local", fromDir: resolve10(bundled.sourceOhDir, "..") };
6436
+ return { kind: "local", fromDir: resolve14(bundled.sourceOhDir, "..") };
5541
6437
  }
5542
6438
  return {
5543
6439
  kind: "remote",
@@ -5549,7 +6445,7 @@ function resolveUpdateSource(args, bundled, bin = LEGACY_PRODUCT.bin) {
5549
6445
  function readPayloadVersion(checkoutDir) {
5550
6446
  try {
5551
6447
  const parsed = JSON.parse(
5552
- readFileSync11(join10(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
6448
+ readFileSync12(join14(resolveControlDir(checkoutDir).path, "cli", "package.json"), "utf8")
5553
6449
  );
5554
6450
  if (parsed && typeof parsed.version === "string") return parsed.version;
5555
6451
  } catch {
@@ -5558,8 +6454,8 @@ function readPayloadVersion(checkoutDir) {
5558
6454
  }
5559
6455
  async function runWithRemoteSource(hooks, fn) {
5560
6456
  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 }));
6457
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync4(join14(tmpdir3(), "oh-remote-")));
6458
+ const rm = hooks.rm ?? ((dir) => rmSync4(dir, { recursive: true, force: true }));
5563
6459
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
5564
6460
  const checkoutDir = mkdtemp();
5565
6461
  try {
@@ -5824,7 +6720,14 @@ async function main(argv) {
5824
6720
  if (a.subcommand === "status") {
5825
6721
  return await runHarnessStatus(a.name, { bin, json: a.json }, io);
5826
6722
  }
5827
- return await runHarnessInstall(a.name, { bin }, io);
6723
+ if (a.subcommand === "uninstall") {
6724
+ return await runHarnessUninstall(a.name, { bin, force: a.force }, io);
6725
+ }
6726
+ return await runHarnessInstall(
6727
+ a.name,
6728
+ { bin, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
6729
+ io
6730
+ );
5828
6731
  }
5829
6732
  if (first === "tool") {
5830
6733
  const parsed = parseToolArgs(argv.slice(1), bin);
@@ -5849,7 +6752,14 @@ async function main(argv) {
5849
6752
  if (a.subcommand === "status") {
5850
6753
  return await runToolStatus(a.name, { bin, json: a.json }, io);
5851
6754
  }
5852
- return await runToolInstall(a.name, { bin, yes: a.yes }, io);
6755
+ if (a.subcommand === "uninstall") {
6756
+ return await runToolUninstall(a.name, { bin, force: a.force }, io);
6757
+ }
6758
+ return await runToolInstall(
6759
+ a.name,
6760
+ { bin, yes: a.yes, host: a.host, ...a.path !== void 0 ? { path: a.path } : {} },
6761
+ io
6762
+ );
5853
6763
  }
5854
6764
  if (first === "gateway") {
5855
6765
  const parsed = parseGatewayArgs(argv.slice(1));