@mifune/agro 0.15.1 → 0.16.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/README.md +14 -2
  2. package/dist/agro.js +607 -324
  3. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -7,8 +7,8 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/cli.ts
9
9
  import { fileURLToPath } from "node:url";
10
- import { dirname as dirname10, join as join18, resolve as resolve11 } from "node:path";
11
- import { existsSync as existsSync22, mkdtempSync as mkdtempSync5, readFileSync as readFileSync12, rmSync as rmSync6 } from "node:fs";
10
+ import { dirname as dirname10, join as join19, resolve as resolve11 } from "node:path";
11
+ import { existsSync as existsSync23, mkdtempSync as mkdtempSync5, readFileSync as readFileSync13, rmSync as rmSync6 } from "node:fs";
12
12
  import { tmpdir as tmpdir4 } from "node:os";
13
13
 
14
14
  // src/commands/update.ts
@@ -2222,7 +2222,7 @@ function resolveExecutionTarget(opts) {
2222
2222
  }
2223
2223
 
2224
2224
  // src/lib/version.ts
2225
- var AGRO_VERSION = true ? "0.15.1" : "0.0.0-dev";
2225
+ var AGRO_VERSION = true ? "0.16.0" : "0.0.0-dev";
2226
2226
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2227
2227
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2228
2228
  function parseReleaseVersion(value) {
@@ -3129,7 +3129,7 @@ var HARNESS_CATALOG = [
3129
3129
  installArgv: [
3130
3130
  "bash",
3131
3131
  "-lc",
3132
- `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]'`
3132
+ `curl -fsSL https://hermes-agent.nousresearch.com/install.sh | HERMES_INSTALL_DIR="${HARNESS_PREFIX_TOKEN}/lib/hermes-agent" bash -s -- --skip-setup --skip-browser && "${HARNESS_PREFIX_TOKEN}/bin/hermes" pm install --extra slack --extra teams`
3133
3133
  ],
3134
3134
  installUser: "sandbox",
3135
3135
  verifyArgv: ["hermes", "--version"],
@@ -3784,6 +3784,55 @@ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTT
3784
3784
  }
3785
3785
  }
3786
3786
 
3787
+ // src/commands/sandbox-list.ts
3788
+ async function entryStatus(root, name, run) {
3789
+ try {
3790
+ const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
3791
+ return await target.status();
3792
+ } catch {
3793
+ return "unknown";
3794
+ }
3795
+ }
3796
+ async function runSandboxList(opts, io) {
3797
+ const run = opts.run ?? spawnRunner;
3798
+ const rows = [];
3799
+ for (const name of listEntries()) {
3800
+ const root = entryRoot(name);
3801
+ const config = readAgroConfig(agroConfigPath(root));
3802
+ const checkout = configCheckout(config) ?? "-";
3803
+ rows.push({
3804
+ name,
3805
+ runtime: config.runtime ?? "docker",
3806
+ checkout,
3807
+ repo: checkout,
3808
+ status: await entryStatus(root, name, run)
3809
+ });
3810
+ }
3811
+ if (opts.json === true) {
3812
+ io.stdout(`${JSON.stringify(rows, null, 2)}
3813
+ `);
3814
+ return 0;
3815
+ }
3816
+ if (rows.length === 0) {
3817
+ io.stdout(
3818
+ `no sandbox is registered in ${registryRoot()} \u2014 create one with \`${opts.bin} sandbox install docker\`
3819
+ `
3820
+ );
3821
+ return 0;
3822
+ }
3823
+ const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
3824
+ const nameWidth = width((row) => row.name);
3825
+ const runtimeWidth = width((row) => row.runtime);
3826
+ const statusWidth = width((row) => row.status);
3827
+ for (const row of rows) {
3828
+ io.stdout(
3829
+ `${row.name.padEnd(nameWidth)} ${row.runtime.padEnd(runtimeWidth)} ${row.status.padEnd(statusWidth)} ${row.checkout}
3830
+ `
3831
+ );
3832
+ }
3833
+ return 0;
3834
+ }
3835
+
3787
3836
  // src/commands/sandbox.ts
3788
3837
  function hostTimezone() {
3789
3838
  const tz = process.env.TZ;
@@ -4001,58 +4050,384 @@ async function runSandboxInstall(opts, io) {
4001
4050
  }
4002
4051
  return code;
4003
4052
  }
4004
- async function entryStatus(root, name, run) {
4053
+
4054
+ // src/services/sandbox-upgrade.ts
4055
+ import { closeSync as closeSync2, existsSync as existsSync17, openSync as openSync2, readFileSync as readFileSync11, unlinkSync as unlinkSync5, writeSync } from "node:fs";
4056
+ import { join as join13 } from "node:path";
4057
+ function errorMessage(error) {
4058
+ return error instanceof Error ? error.message : String(error);
4059
+ }
4060
+ function conflictingDotenvImage(root, imageRef) {
4061
+ const rootEnv = join13(root, ".env");
4062
+ const fallbackEnv = join13(root, ".devcontainer", ".env");
4063
+ const file = existsSync17(rootEnv) ? rootEnv : fallbackEnv;
4064
+ if (!existsSync17(file)) return void 0;
4065
+ for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
4066
+ const match = /^AGRO_SANDBOX_IMAGE=(.*)$/.exec(line);
4067
+ if (!match) continue;
4068
+ const value = match[1].trim().replace(/^(?:"(.*)"|'(.*)')$/, (_whole, double, single) => double ?? single ?? "");
4069
+ if (value !== imageRef) return file;
4070
+ }
4071
+ return void 0;
4072
+ }
4073
+ async function runSandboxUpgrade(opts, io) {
4074
+ const prefix = `${opts.bin} sandbox upgrade:`;
4075
+ const version = parseReleaseVersion(opts.version);
4076
+ if (version === void 0) {
4077
+ io.stderr(`${prefix} invalid release version "${opts.version}"
4078
+ `);
4079
+ return 1;
4080
+ }
4081
+ if (runningInsideSandbox()) {
4082
+ io.stderr(`${prefix} host-only \u2014 run this command on the host
4083
+ `);
4084
+ return 1;
4085
+ }
4086
+ let root;
4005
4087
  try {
4006
- const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
4007
- return await target.status();
4008
- } catch {
4009
- return "unknown";
4088
+ root = entryRoot(opts.name);
4089
+ } catch (error) {
4090
+ io.stderr(`${prefix} ${errorMessage(error)}
4091
+ `);
4092
+ return 1;
4093
+ }
4094
+ const configFile = agroConfigPath(root);
4095
+ if (!existsSync17(configFile)) {
4096
+ io.stderr(`${prefix} no sandbox entry named "${opts.name}"
4097
+ `);
4098
+ return 1;
4099
+ }
4100
+ const lock = join13(root, ".sandbox-upgrade.lock");
4101
+ let fd;
4102
+ try {
4103
+ fd = openSync2(lock, "wx", 384);
4104
+ } catch (error) {
4105
+ const message = error instanceof Error && "code" in error && error.code === "EEXIST" ? `upgrade already in progress for ${opts.name}; lock: ${lock}. Confirm no upgrade process owns this entry before removing that lock and retrying` : errorMessage(error);
4106
+ io.stderr(`${prefix} ${message}
4107
+ `);
4108
+ return 1;
4109
+ }
4110
+ try {
4111
+ writeSync(fd, `${process.pid}
4112
+ `);
4113
+ const config = readAgroConfig(configFile);
4114
+ if (config.image?.mode !== "image") {
4115
+ io.stderr(`${prefix} ${opts.name} requires image.mode "image"
4116
+ `);
4117
+ return 1;
4118
+ }
4119
+ const imageRef = officialImageRef(version);
4120
+ if (process.env.AGRO_SANDBOX_IMAGE !== void 0 && process.env.AGRO_SANDBOX_IMAGE !== "" && process.env.AGRO_SANDBOX_IMAGE !== imageRef) {
4121
+ io.stderr(`${prefix} conflicting AGRO_SANDBOX_IMAGE in the host environment
4122
+ `);
4123
+ return 1;
4124
+ }
4125
+ const dotenv = conflictingDotenvImage(root, imageRef);
4126
+ if (dotenv !== void 0) {
4127
+ io.stderr(`${prefix} conflicting AGRO_SANDBOX_IMAGE in ${dotenv}
4128
+ `);
4129
+ return 1;
4130
+ }
4131
+ const run = opts.run ?? spawnRunner;
4132
+ let priorRef = config.image.ref;
4133
+ if (!priorRef) {
4134
+ const name = configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
4135
+ const inspected = run("docker", ["inspect", "-f", "{{.Config.Image}}", name], { stdio: "capture" });
4136
+ priorRef = inspected.status === 0 && !inspected.error ? inspected.stdout?.trim() : void 0;
4137
+ if (!priorRef) {
4138
+ io.stderr(`${prefix} cannot determine previous image for ${name}; no changes made
4139
+ `);
4140
+ return 1;
4141
+ }
4142
+ }
4143
+ const apply = (ref) => runSandbox({ bin: opts.bin, cwd: root, imageRef: ref, run }, io);
4144
+ let failure;
4145
+ try {
4146
+ const code = await apply(imageRef);
4147
+ if (code !== 0) failure = `exit ${code}`;
4148
+ } catch (error) {
4149
+ failure = errorMessage(error);
4150
+ }
4151
+ if (failure === void 0) {
4152
+ try {
4153
+ (opts.writeConfig ?? writeAgroConfig)(root, { ...config, image: { ...config.image, ref: imageRef } });
4154
+ return 0;
4155
+ } catch (error) {
4156
+ failure = `config persistence failed (${errorMessage(error)})`;
4157
+ }
4158
+ } else {
4159
+ failure = `provisioning failed (${failure})`;
4160
+ }
4161
+ io.stderr(`${prefix} ${failure}; restoring previous image
4162
+ `);
4163
+ try {
4164
+ const code = await apply(priorRef);
4165
+ if (code !== 0) io.stderr(`${prefix} restoration failed (exit ${code})
4166
+ `);
4167
+ } catch (error) {
4168
+ io.stderr(`${prefix} restoration failed (${errorMessage(error)})
4169
+ `);
4170
+ }
4171
+ return 1;
4172
+ } catch (error) {
4173
+ io.stderr(`${prefix} ${errorMessage(error)}
4174
+ `);
4175
+ return 1;
4176
+ } finally {
4177
+ closeSync2(fd);
4178
+ unlinkSync5(lock);
4010
4179
  }
4011
4180
  }
4012
- async function runSandboxList(opts, io) {
4013
- const run = opts.run ?? spawnRunner;
4014
- const rows = [];
4015
- for (const name of listEntries()) {
4016
- const root = entryRoot(name);
4017
- const config = readAgroConfig(agroConfigPath(root));
4018
- const checkout = configCheckout(config) ?? "-";
4019
- rows.push({
4020
- name,
4021
- runtime: config.runtime ?? "docker",
4022
- checkout,
4023
- repo: checkout,
4024
- status: await entryStatus(root, name, run)
4025
- });
4181
+
4182
+ // src/controllers/sandbox.ts
4183
+ function isHelpFlag(arg) {
4184
+ return arg === "--help" || arg === "-h" || arg === "help";
4185
+ }
4186
+ function runtimeLines() {
4187
+ const width = Math.max(...RUNTIME_CATALOG.map((r) => r.id.length));
4188
+ return RUNTIME_CATALOG.map(
4189
+ (r) => ` ${r.id.padEnd(width)} ${r.provisionable ? "provisionable" : "planned"}`
4190
+ ).join("\n");
4191
+ }
4192
+ function printSandboxHelp(bin = AGRO_PRODUCT.bin) {
4193
+ process.stdout.write(`${bin} sandbox \u2014 Create and list sandboxes
4194
+
4195
+ Usage:
4196
+ ${bin} sandbox install <runtime> [--name <name>] [--checkout <dir>]
4197
+ [--home-mount <dir>] [--yes]
4198
+ [--version <X.Y.Z> | --image[=<ref>]]
4199
+ [--no-build] [--print-argv]
4200
+ ${bin} sandbox list [--json]
4201
+ ${bin} sandbox upgrade <name> --version <X.Y.Z>
4202
+
4203
+ \`install\` writes a sandbox entry under \${${stateNames(bin).envPrefix}HOME:-~/${stateNames(bin).userStateDir}}/sandboxes/<name>/,
4204
+ materialises the compose files and the compose wrapper into it, then starts the
4205
+ container. It needs no project checkout and runs from any directory. Without
4206
+ --checkout the sandbox runs the prebuilt image; with --checkout <dir> that
4207
+ checkout is bound at /home/sandbox/harness; the sandbox builds locally only
4208
+ when that directory holds .devcontainer/Dockerfile.
4209
+
4210
+ On a terminal without --yes it asks for the sandbox name, the timezone, the git
4211
+ identity, SSH (with its host port), the host Docker socket, and the host path
4212
+ for /home/sandbox. With --checkout it seeds those answers from that
4213
+ checkout's ${stateNames(bin).configFile}.
4214
+
4215
+ Flags:
4216
+ --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
4217
+ --checkout <dir> Bind this checkout into the sandbox and seed the defaults
4218
+ from its ${stateNames(bin).configFile}. The sandbox builds
4219
+ locally only when that directory holds
4220
+ .devcontainer/Dockerfile
4221
+ --repo <dir> Deprecated alias for --checkout
4222
+ --home-mount <dir>
4223
+ Bind this host directory at /home/sandbox instead of the
4224
+ Docker-managed volume. Relative paths resolve against the
4225
+ working directory. The directory is created when absent
4226
+ --yes Non-interactive: keep every default and ask nothing
4227
+ --version <X.Y.Z>
4228
+ Run the official release image ghcr.io/mifunedev/agro:<X.Y.Z>
4229
+ (implies --image). A leading v and a -<channel>.<n>
4230
+ pre-release suffix are accepted
4231
+ --image Run the prebuilt image instead of building (implies
4232
+ --no-build). The ref resolves first-match: --version or
4233
+ --image=<ref> > ${stateNames(bin).envPrefix}SANDBOX_IMAGE >
4234
+ ${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
4235
+ --image=<ref> Run a custom image (implies --image); conflicts with --version
4236
+ --no-build Suppress the local build and reuse an existing image
4237
+ --print-argv Print the docker compose argv that would run, then exit
4238
+ without writing an entry
4239
+ --json Machine-readable output (list)
4240
+
4241
+ Runtimes:
4242
+ ${runtimeLines()}
4243
+
4244
+ Next: ${bin} shell <name>
4245
+ `);
4246
+ }
4247
+ var SANDBOX_VALUE_FLAGS = {
4248
+ "--name": "name",
4249
+ "--checkout": "checkout",
4250
+ "--repo": "checkout",
4251
+ "--home-mount": "homeMount"
4252
+ };
4253
+ function parseSandboxArgs(rest, bin = AGRO_PRODUCT.bin) {
4254
+ const args = {
4255
+ help: false,
4256
+ yes: false,
4257
+ image: false,
4258
+ noBuild: false,
4259
+ printArgv: false,
4260
+ json: false
4261
+ };
4262
+ if (rest.length === 0 || isHelpFlag(rest[0])) {
4263
+ return { ok: true, args: { ...args, help: true } };
4026
4264
  }
4027
- if (opts.json === true) {
4028
- io.stdout(`${JSON.stringify(rows, null, 2)}
4265
+ const [head, ...tail] = rest;
4266
+ if (head === "upgrade") {
4267
+ if (tail.length === 1 && isHelpFlag(tail[0])) {
4268
+ return { ok: true, args: { ...args, subcommand: "upgrade", help: true } };
4269
+ }
4270
+ const [name, flag, value, ...extra] = tail;
4271
+ if (name === void 0 || name === "" || name.startsWith("-")) {
4272
+ return { ok: false, error: `${bin} sandbox upgrade: a name is required` };
4273
+ }
4274
+ if (flag === void 0) {
4275
+ return { ok: false, error: `${bin} sandbox upgrade: --version is required` };
4276
+ }
4277
+ if (flag !== "--version") {
4278
+ return { ok: false, error: `${bin} sandbox upgrade: ${flag.startsWith("-") ? "unknown flag" : "unexpected argument"} "${flag}"` };
4279
+ }
4280
+ if (value === void 0 || value === "") {
4281
+ return { ok: false, error: `${bin} sandbox upgrade: --version requires a value` };
4282
+ }
4283
+ if (value.startsWith("-")) {
4284
+ return { ok: false, error: `${bin} sandbox upgrade: unknown flag "${value}"` };
4285
+ }
4286
+ const version2 = parseReleaseVersion(value);
4287
+ if (version2 === void 0) {
4288
+ return { ok: false, error: `${bin} sandbox upgrade: --version "${value}" is not a release version \u2014 expected X.Y.Z` };
4289
+ }
4290
+ if (extra.length > 0) {
4291
+ const token = extra[0];
4292
+ return { ok: false, error: `${bin} sandbox upgrade: ${token.startsWith("-") && token !== "--version" ? "unknown flag" : "unexpected argument"} "${token}"` };
4293
+ }
4294
+ return { ok: true, args: { ...args, subcommand: "upgrade", name, version: version2 } };
4295
+ }
4296
+ if (head !== "install" && head !== "list") {
4297
+ return {
4298
+ ok: false,
4299
+ error: `${bin} sandbox: unknown subcommand "${head}" \u2014 expected install or list`,
4300
+ showHelp: true
4301
+ };
4302
+ }
4303
+ args.subcommand = head;
4304
+ if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
4305
+ const positionals = [];
4306
+ const checkoutSpellings = /* @__PURE__ */ new Set();
4307
+ let version;
4308
+ for (let i = 0; i < tail.length; i++) {
4309
+ const token = tail[i];
4310
+ const valueFlag = SANDBOX_VALUE_FLAGS[token];
4311
+ if (valueFlag !== void 0) {
4312
+ const value = tail[i + 1];
4313
+ if (value === void 0) {
4314
+ return { ok: false, error: `${bin} sandbox ${head}: ${token} requires a value` };
4315
+ }
4316
+ if (valueFlag === "checkout") checkoutSpellings.add(token);
4317
+ args[valueFlag] = value;
4318
+ i++;
4319
+ } else if (token === "--yes") {
4320
+ args.yes = true;
4321
+ } else if (token === "--no-build") {
4322
+ args.noBuild = true;
4323
+ } else if (token === "--print-argv") {
4324
+ args.printArgv = true;
4325
+ } else if (token === "--image") {
4326
+ args.image = true;
4327
+ } else if (token.startsWith("--image=")) {
4328
+ const ref = token.slice("--image=".length);
4329
+ if (ref === "") {
4330
+ return { ok: false, error: `${bin} sandbox: --image=<ref> requires a non-empty image ref` };
4331
+ }
4332
+ args.image = true;
4333
+ args.imageRef = ref;
4334
+ } else if (token === "--version" || token.startsWith("--version=")) {
4335
+ const value = token === "--version" ? tail[++i] : token.slice("--version=".length);
4336
+ if (value === void 0 || value === "") {
4337
+ return { ok: false, error: `${bin} sandbox ${head}: --version requires a value` };
4338
+ }
4339
+ version = parseReleaseVersion(value);
4340
+ if (version === void 0) {
4341
+ return {
4342
+ ok: false,
4343
+ error: `${bin} sandbox ${head}: --version "${value}" is not a release version \u2014 expected X.Y.Z`
4344
+ };
4345
+ }
4346
+ } else if (token === "--json") {
4347
+ args.json = true;
4348
+ } else if (token.startsWith("-")) {
4349
+ return { ok: false, error: `${bin} sandbox ${head}: unknown flag "${token}"` };
4350
+ } else {
4351
+ positionals.push(token);
4352
+ }
4353
+ }
4354
+ if (checkoutSpellings.has("--checkout") && checkoutSpellings.has("--repo")) {
4355
+ return {
4356
+ ok: false,
4357
+ error: `${bin} sandbox ${head}: --checkout conflicts with --repo \u2014 pass exactly one, and prefer --checkout`
4358
+ };
4359
+ }
4360
+ if (version !== void 0) {
4361
+ if (args.imageRef !== void 0) {
4362
+ return {
4363
+ ok: false,
4364
+ error: `${bin} sandbox ${head}: --version conflicts with --image=<ref> \u2014 pass --version for an official release or --image=<ref> for a custom image`
4365
+ };
4366
+ }
4367
+ args.image = true;
4368
+ args.imageRef = officialImageRef(version);
4369
+ }
4370
+ if (head === "list") {
4371
+ if (positionals.length > 0) {
4372
+ return { ok: false, error: `${bin} sandbox list: unexpected argument "${positionals[0]}"` };
4373
+ }
4374
+ return { ok: true, args };
4375
+ }
4376
+ if (positionals.length === 0) {
4377
+ return {
4378
+ ok: false,
4379
+ error: `${bin} sandbox install: a runtime is required, e.g. \`${bin} sandbox install docker\``,
4380
+ showHelp: true
4381
+ };
4382
+ }
4383
+ if (positionals.length > 1) {
4384
+ return { ok: false, error: `${bin} sandbox install: unexpected argument "${positionals[1]}"` };
4385
+ }
4386
+ args.runtime = positionals[0];
4387
+ return { ok: true, args };
4388
+ }
4389
+ async function runSandboxCommand(rest, bin) {
4390
+ const parsed = parseSandboxArgs(rest, bin);
4391
+ if (!parsed.ok) {
4392
+ process.stderr.write(`${parsed.error}
4029
4393
  `);
4030
- return 0;
4394
+ if (parsed.showHelp) printSandboxHelp(bin);
4395
+ return 1;
4031
4396
  }
4032
- if (rows.length === 0) {
4033
- io.stdout(
4034
- `no sandbox is registered in ${registryRoot()} \u2014 create one with \`${opts.bin} sandbox install docker\`
4035
- `
4036
- );
4037
- return 0;
4397
+ const a = parsed.args;
4398
+ if (a.help) {
4399
+ printSandboxHelp(bin);
4400
+ return a.subcommand === void 0 ? 1 : 0;
4038
4401
  }
4039
- const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
4040
- const nameWidth = width((row) => row.name);
4041
- const runtimeWidth = width((row) => row.runtime);
4042
- const statusWidth = width((row) => row.status);
4043
- for (const row of rows) {
4044
- io.stdout(
4045
- `${row.name.padEnd(nameWidth)} ${row.runtime.padEnd(runtimeWidth)} ${row.status.padEnd(statusWidth)} ${row.checkout}
4046
- `
4047
- );
4402
+ const io = {
4403
+ stdout: (s) => process.stdout.write(s),
4404
+ stderr: (s) => process.stderr.write(s)
4405
+ };
4406
+ if (a.subcommand === "list") return await runSandboxList({ bin, json: a.json }, io);
4407
+ if (a.subcommand === "upgrade") {
4408
+ return await runSandboxUpgrade({ bin, name: a.name, version: a.version }, io);
4048
4409
  }
4049
- return 0;
4410
+ return await runSandboxInstall(
4411
+ {
4412
+ bin,
4413
+ runtime: a.runtime,
4414
+ ...a.name !== void 0 ? { name: a.name } : {},
4415
+ ...a.checkout !== void 0 ? { checkout: a.checkout } : {},
4416
+ ...a.homeMount !== void 0 ? { homeMount: a.homeMount } : {},
4417
+ yes: a.yes,
4418
+ image: a.image,
4419
+ ...a.imageRef !== void 0 ? { imageRef: a.imageRef } : {},
4420
+ noBuild: a.noBuild,
4421
+ printArgv: a.printArgv
4422
+ },
4423
+ io
4424
+ );
4050
4425
  }
4051
4426
 
4052
4427
  // src/commands/harness.ts
4053
- import { existsSync as existsSync19 } from "node:fs";
4428
+ import { existsSync as existsSync20 } from "node:fs";
4054
4429
  import { homedir as homedir3 } from "node:os";
4055
- import { delimiter as delimiter2, join as join15 } from "node:path";
4430
+ import { delimiter as delimiter2, join as join16 } from "node:path";
4056
4431
 
4057
4432
  // src/lib/docs.ts
4058
4433
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4065,17 +4440,20 @@ function sourceDocsUrl(docsPath) {
4065
4440
  }
4066
4441
 
4067
4442
  // src/lib/host-config.ts
4068
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readFileSync as readFileSync11, renameSync as renameSync4, unlinkSync as unlinkSync5, writeFileSync as writeFileSync9 } from "node:fs";
4069
- import { basename as basename5, dirname as dirname8, isAbsolute, join as join13, resolve as resolve8 } from "node:path";
4443
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
4444
+ import { basename as basename5, dirname as dirname8, isAbsolute, join as join14, resolve as resolve8 } from "node:path";
4070
4445
  var HOST_CONFIG_MODE = 420;
4071
4446
  var HOST_CONFIG_FILE = "config.json";
4072
4447
  var WORKSPACES_SUBDIR = "workspaces";
4073
4448
  var DEFAULT_WORKSPACE_NAME = "harness";
4449
+ function isRootToolReceipt(receipt) {
4450
+ return receipt.scope === "root";
4451
+ }
4074
4452
  function hostStateHome(env, home) {
4075
4453
  return resolveUserStateHome(env, home);
4076
4454
  }
4077
4455
  function hostConfigPath(env, home) {
4078
- return join13(hostStateHome(env, home), HOST_CONFIG_FILE);
4456
+ return join14(hostStateHome(env, home), HOST_CONFIG_FILE);
4079
4457
  }
4080
4458
  function assertWorkspaceName(name) {
4081
4459
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -4085,14 +4463,14 @@ function assertWorkspaceName(name) {
4085
4463
  }
4086
4464
  }
4087
4465
  function workspacesRoot(env, home) {
4088
- return join13(hostStateHome(env, home), WORKSPACES_SUBDIR);
4466
+ return join14(hostStateHome(env, home), WORKSPACES_SUBDIR);
4089
4467
  }
4090
4468
  function workspaceRoot(name, env, home) {
4091
4469
  assertWorkspaceName(name);
4092
- return join13(workspacesRoot(env, home), name);
4470
+ return join14(workspacesRoot(env, home), name);
4093
4471
  }
4094
4472
  function defaultHarnessRoot(env, home) {
4095
- return join13(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4473
+ return join14(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4096
4474
  }
4097
4475
  function validateHostConfig(value) {
4098
4476
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4114,7 +4492,7 @@ function validateHostConfig(value) {
4114
4492
  const hostHarnesses = record.hostHarnesses;
4115
4493
  if (hostHarnesses !== void 0) validateReceipts(hostHarnesses, "hostHarnesses", "harness");
4116
4494
  const hostTools = record.hostTools;
4117
- if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool");
4495
+ if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool", true);
4118
4496
  return { ...record, version: 1 };
4119
4497
  }
4120
4498
  function absoluteField(value, path4) {
@@ -4124,7 +4502,7 @@ function absoluteField(value, path4) {
4124
4502
  if (!isAbsolute(value)) throw fieldError2(path4, "must be an absolute path");
4125
4503
  return value;
4126
4504
  }
4127
- function validateReceipts(value, field, noun) {
4505
+ function validateReceipts(value, field, noun, allowRootScope = false) {
4128
4506
  if (!value || typeof value !== "object" || Array.isArray(value)) {
4129
4507
  throw fieldError2(field, `must be a JSON object keyed by ${noun} id`);
4130
4508
  }
@@ -4134,9 +4512,16 @@ function validateReceipts(value, field, noun) {
4134
4512
  if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
4135
4513
  throw fieldError2(at, "must be a JSON object");
4136
4514
  }
4137
- const entry = receipt;
4138
- absoluteField(entry.prefix, `${at}.prefix`);
4139
- absoluteField(entry.binPath, `${at}.binPath`);
4515
+ const entry = receipt;
4516
+ if (allowRootScope && entry.scope !== void 0) {
4517
+ if (entry.scope !== "root") throw fieldError2(`${at}.scope`, 'must be "root" when present');
4518
+ for (const key of ["prefix", "binPath"]) {
4519
+ if (entry[key] !== void 0) throw fieldError2(`${at}.${key}`, 'must be absent when scope is "root"');
4520
+ }
4521
+ } else {
4522
+ absoluteField(entry.prefix, `${at}.prefix`);
4523
+ absoluteField(entry.binPath, `${at}.binPath`);
4524
+ }
4140
4525
  if (typeof entry.binary !== "string" || entry.binary === "") {
4141
4526
  throw fieldError2(`${at}.binary`, "must be a non-empty string");
4142
4527
  }
@@ -4150,10 +4535,10 @@ function validateReceipts(value, field, noun) {
4150
4535
  }
4151
4536
  function readHostConfig(env, home) {
4152
4537
  const path4 = hostConfigPath(env, home);
4153
- if (!existsSync17(path4)) return { version: 1 };
4538
+ if (!existsSync18(path4)) return { version: 1 };
4154
4539
  let raw;
4155
4540
  try {
4156
- raw = readFileSync11(path4, "utf8");
4541
+ raw = readFileSync12(path4, "utf8");
4157
4542
  } catch (error) {
4158
4543
  const detail = error instanceof Error ? error.message : String(error);
4159
4544
  throw new Error(`could not read ${path4}: ${detail}`);
@@ -4178,7 +4563,7 @@ function writeHostConfig(config, env, home) {
4178
4563
  renameSync4(tmp, path4);
4179
4564
  } catch (error) {
4180
4565
  try {
4181
- unlinkSync5(tmp);
4566
+ unlinkSync6(tmp);
4182
4567
  } catch {
4183
4568
  }
4184
4569
  throw error;
@@ -4201,8 +4586,8 @@ function fieldError2(path4, requirement) {
4201
4586
  }
4202
4587
 
4203
4588
  // src/lib/host-workspace.ts
4204
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
4205
- import { dirname as dirname9, join as join14, resolve as resolve9 } from "node:path";
4589
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
4590
+ import { dirname as dirname9, join as join15, resolve as resolve9 } from "node:path";
4206
4591
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4207
4592
  var IGNORABLE_ENTRIES = [
4208
4593
  REGISTRY_SUBDIR,
@@ -4214,7 +4599,7 @@ function isDirectory3(path4) {
4214
4599
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4215
4600
  }
4216
4601
  function stateHomes(home) {
4217
- return STATE_HOME_DIRS.map((name) => join14(home, name));
4602
+ return STATE_HOME_DIRS.map((name) => join15(home, name));
4218
4603
  }
4219
4604
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4220
4605
  const target = resolve9(root);
@@ -4222,7 +4607,7 @@ function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4222
4607
  if (enclosing === void 0) return void 0;
4223
4608
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
4224
4609
  A workspace there collides with AGRO state and blocks every command run from ${home}.
4225
- Move it out \u2014 for example \`mv ${enclosing} ${join14(home, "agro")}\` \u2014 then re-run this command.
4610
+ Move it out \u2014 for example \`mv ${enclosing} ${join15(home, "agro")}\` \u2014 then re-run this command.
4226
4611
  `;
4227
4612
  }
4228
4613
  function isIgnorableEntry(name) {
@@ -4239,16 +4624,16 @@ function cloneInto(target, run, root, ref) {
4239
4624
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4240
4625
  }
4241
4626
  function cloneThroughStaging(root, run, ref) {
4242
- const staging = mkdtempSync4(join14(dirname9(root), "agro-clone-"));
4627
+ const staging = mkdtempSync4(join15(dirname9(root), "agro-clone-"));
4243
4628
  try {
4244
- const checkout = join14(staging, "agro");
4629
+ const checkout = join15(staging, "agro");
4245
4630
  cloneInto(checkout, run, root, ref);
4246
4631
  for (const name of readdirSync3(checkout)) {
4247
- const destination = join14(root, name);
4248
- if (existsSync18(destination)) {
4632
+ const destination = join15(root, name);
4633
+ if (existsSync19(destination)) {
4249
4634
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4250
4635
  }
4251
- renameSync5(join14(checkout, name), destination);
4636
+ renameSync5(join15(checkout, name), destination);
4252
4637
  }
4253
4638
  } finally {
4254
4639
  rmSync4(staging, { recursive: true, force: true });
@@ -4256,7 +4641,7 @@ function cloneThroughStaging(root, run, ref) {
4256
4641
  }
4257
4642
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4258
4643
  const root = resolve9(path4);
4259
- if (existsSync18(join14(root, ".git"))) return { root, action: "reused" };
4644
+ if (existsSync19(join15(root, ".git"))) return { root, action: "reused" };
4260
4645
  const stats = statSync5(root, { throwIfNoEntry: false });
4261
4646
  if (stats === void 0) {
4262
4647
  mkdirSync10(dirname9(root), { recursive: true });
@@ -4278,11 +4663,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4278
4663
  function listHostWorkspaces(env, home) {
4279
4664
  const root = workspacesRoot(env, home);
4280
4665
  if (!isDirectory3(root)) return [];
4281
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync18(join14(root, name, ".git"))).sort();
4666
+ return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync19(join15(root, name, ".git"))).sort();
4282
4667
  }
4283
4668
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
4284
4669
  const root = resolve9(path4);
4285
- if (existsSync18(join14(root, ".git"))) return { ok: true, root };
4670
+ if (existsSync19(join15(root, ".git"))) return { ok: true, root };
4286
4671
  const existing = listHostWorkspaces(env, home);
4287
4672
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
4288
4673
  `;
@@ -4312,7 +4697,7 @@ function targetFor(root, run, env) {
4312
4697
  });
4313
4698
  }
4314
4699
  function hostPrefix(home) {
4315
- return join15(home, ".local");
4700
+ return join16(home, ".local");
4316
4701
  }
4317
4702
  function pathEntries(env) {
4318
4703
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -4346,6 +4731,21 @@ async function probeInstalled(target, entry, prefix, user, env) {
4346
4731
  throw err;
4347
4732
  }
4348
4733
  }
4734
+ function sandboxMarkerPath(entry) {
4735
+ return `${SANDBOX_HARNESS_PREFIX}/share/agro/harnesses/${entry.id}.installed`;
4736
+ }
4737
+ async function sandboxMarkerExists(target, entry) {
4738
+ const r = await target.exec({
4739
+ argv: ["test", "-f", sandboxMarkerPath(entry)],
4740
+ user: "sandbox",
4741
+ stdio: "capture",
4742
+ timeoutMs: PROBE_TIMEOUT_MS
4743
+ });
4744
+ return r.exitCode === 0;
4745
+ }
4746
+ function hostBinaryUnderPrefix(entry, prefix) {
4747
+ return existsSync20(join16(harnessBinPath(prefix), entry.binary));
4748
+ }
4349
4749
  function stateOf(entry, installed, location) {
4350
4750
  return {
4351
4751
  id: entry.id,
@@ -4365,7 +4765,7 @@ function probeableHost(env, home) {
4365
4765
  return void 0;
4366
4766
  }
4367
4767
  const prefix = hostPrefix(home);
4368
- if (!existsSync19(join15(root, ".git")) && !existsSync19(prefix)) return void 0;
4768
+ if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
4369
4769
  return { root, prefix };
4370
4770
  }
4371
4771
  async function collectStates(root, run, home, env, only) {
@@ -4571,7 +4971,15 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4571
4971
  const code = await reconcileHermes(target, io, bin, void 0, root);
4572
4972
  if (code !== 0) return code;
4573
4973
  }
4574
- if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true) {
4974
+ let hasReceipt;
4975
+ try {
4976
+ hasReceipt = readHostConfig(env, home).hostHarnesses?.[entry.id] !== void 0;
4977
+ } catch (err) {
4978
+ io.stderr(`${bin} harness: ${messageOf(err)}
4979
+ `);
4980
+ return 1;
4981
+ }
4982
+ if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true && (hasReceipt || !hostBinaryUnderPrefix(entry, prefix))) {
4575
4983
  io.stdout(`${entry.id}: already installed (${entry.binary})
4576
4984
  `);
4577
4985
  try {
@@ -4735,7 +5143,20 @@ async function runHarnessUninstall(name, opts, io) {
4735
5143
  if (!isReachable(await resolveTargetStatus(target))) {
4736
5144
  return await uninstallOnHost(entry, opts, io, run);
4737
5145
  }
4738
- return (await removeHarness(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
5146
+ const outcome = await removeHarness(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io);
5147
+ if (outcome.dropReceipt) {
5148
+ const cleared = await target.exec({
5149
+ argv: ["rm", "-f", sandboxMarkerPath(entry)],
5150
+ user: "sandbox",
5151
+ stdio: "capture"
5152
+ });
5153
+ if (cleared.exitCode !== 0) {
5154
+ io.stderr(`${opts.bin} harness: could not clear the install record (exit ${cleared.exitCode}).
5155
+ `);
5156
+ return 1;
5157
+ }
5158
+ }
5159
+ return outcome.code;
4739
5160
  }
4740
5161
  async function runHarnessInstall(name, opts, io) {
4741
5162
  const run = opts.run ?? spawnRunner;
@@ -4761,7 +5182,7 @@ async function runHarnessInstall(name, opts, io) {
4761
5182
  if (code !== 0) return code;
4762
5183
  }
4763
5184
  const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
4764
- if (already === true) {
5185
+ if (already === true && await sandboxMarkerExists(target, entry)) {
4765
5186
  io.stdout(`${entry.id}: already installed (${entry.binary})
4766
5187
  `);
4767
5188
  return 0;
@@ -4792,15 +5213,26 @@ async function runHarnessInstall(name, opts, io) {
4792
5213
  const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4793
5214
  if (code !== 0) return code;
4794
5215
  }
5216
+ const marker = sandboxMarkerPath(entry);
5217
+ const marked = await target.exec({
5218
+ argv: ["sh", "-c", `mkdir -p "$(dirname "$1")" && : > "$1"`, "sh", marker],
5219
+ user: "sandbox",
5220
+ stdio: "capture"
5221
+ });
5222
+ if (marked.exitCode !== 0) {
5223
+ io.stderr(`${opts.bin} harness: could not record the install at ${marker} (exit ${marked.exitCode}).
5224
+ `);
5225
+ return 1;
5226
+ }
4795
5227
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
4796
5228
  `);
4797
5229
  return 0;
4798
5230
  }
4799
5231
 
4800
5232
  // src/commands/workspace.ts
4801
- import { existsSync as existsSync20, rmSync as rmSync5 } from "node:fs";
5233
+ import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
4802
5234
  import { homedir as homedir4 } from "node:os";
4803
- import { join as join16, resolve as resolve10 } from "node:path";
5235
+ import { join as join17, resolve as resolve10 } from "node:path";
4804
5236
  function homeOf2(opts) {
4805
5237
  return (opts.homedir ?? homedir4)();
4806
5238
  }
@@ -4846,9 +5278,9 @@ async function runWorkspaceCreate(name, opts, io) {
4846
5278
  return 1;
4847
5279
  }
4848
5280
  let action;
4849
- const preexisting = existsSync20(root);
5281
+ const preexisting = existsSync21(root);
4850
5282
  try {
4851
- if (!opts.json && !existsSync20(join16(root, ".git"))) {
5283
+ if (!opts.json && !existsSync21(join17(root, ".git"))) {
4852
5284
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
4853
5285
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
4854
5286
  `);
@@ -4893,7 +5325,7 @@ async function runWorkspaceList(opts, io) {
4893
5325
  const recorded = recordedRoot(env, home);
4894
5326
  const registry = workspacesRoot(env, home);
4895
5327
  rows = listHostWorkspaces(env, home).map((name) => {
4896
- const root = join16(registry, name);
5328
+ const root = join17(registry, name);
4897
5329
  return { name, root, default: recorded === root };
4898
5330
  });
4899
5331
  } catch (err) {
@@ -4918,9 +5350,9 @@ async function runWorkspaceList(opts, io) {
4918
5350
  }
4919
5351
 
4920
5352
  // src/commands/tool.ts
4921
- import { existsSync as existsSync21 } from "node:fs";
5353
+ import { existsSync as existsSync22 } from "node:fs";
4922
5354
  import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
4923
- import { delimiter as delimiter3, join as join17 } from "node:path";
5355
+ import { delimiter as delimiter3, join as join18 } from "node:path";
4924
5356
 
4925
5357
  // src/lib/tools/catalog.ts
4926
5358
  var TOOLS_DOC = "docs/installation.md";
@@ -5337,6 +5769,7 @@ function resolveToolInstallArgv(entry, host) {
5337
5769
  }
5338
5770
 
5339
5771
  // src/commands/tool.ts
5772
+ var ROOT_TOOL_REMOVAL_DOCS = `${sourceDocsUrl("docs/installation.md")}#remove-a-root-level-tool`;
5340
5773
  function isReachable2(status) {
5341
5774
  return status === "ready" || status === "starting";
5342
5775
  }
@@ -5350,7 +5783,21 @@ function targetFor2(root, run, env) {
5350
5783
  });
5351
5784
  }
5352
5785
  function hostPrefix2(home) {
5353
- return join17(home, ".local");
5786
+ return join18(home, ".local");
5787
+ }
5788
+ function sandboxMarkerPath2(entry) {
5789
+ return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
5790
+ }
5791
+ async function sandboxMarkerExists2(target, entry) {
5792
+ const r = await target.exec({
5793
+ argv: ["test", "-f", sandboxMarkerPath2(entry)],
5794
+ user: "sandbox",
5795
+ stdio: "capture"
5796
+ });
5797
+ return r.exitCode === 0;
5798
+ }
5799
+ function hostBinaryUnderPrefix2(entry, prefix) {
5800
+ return existsSync22(join18(harnessBinPath(prefix), entry.binary));
5354
5801
  }
5355
5802
  function pathEntries2(env) {
5356
5803
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -5430,7 +5877,7 @@ function probeableHost2(env, home) {
5430
5877
  return void 0;
5431
5878
  }
5432
5879
  const prefix = hostPrefix2(home);
5433
- if (!existsSync21(join17(root, ".git")) && !existsSync21(prefix)) return void 0;
5880
+ if (!existsSync22(join18(root, ".git")) && !existsSync22(prefix)) return void 0;
5434
5881
  return { root, prefix };
5435
5882
  }
5436
5883
  async function collectRows(root, run, home, env, only) {
@@ -5646,7 +6093,15 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5646
6093
  const prefix = hostPrefix2(home);
5647
6094
  const installEnv = { NPM_USER_PREFIX: prefix };
5648
6095
  const target = hostTargetFor2(root, prefix, run, env);
5649
- if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
6096
+ let hasReceipt;
6097
+ try {
6098
+ hasReceipt = readHostConfig(env, home).hostTools?.[entry.id] !== void 0;
6099
+ } catch (err) {
6100
+ io.stderr(`${bin} tool: ${messageOf3(err)}
6101
+ `);
6102
+ return 1;
6103
+ }
6104
+ if (await probeInstalled2(target, entry, void 0, installEnv) === true && (hasReceipt || entry.hostInstallUser === "root" || !hostBinaryUnderPrefix2(entry, prefix))) {
5650
6105
  io.stdout(`${entry.id}: already installed (${entry.binary})
5651
6106
  `);
5652
6107
  try {
@@ -5681,13 +6136,8 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5681
6136
  `);
5682
6137
  return r.exitCode;
5683
6138
  }
5684
- const receipt = {
5685
- prefix,
5686
- binary: entry.binary,
5687
- binPath: harnessBinPath(prefix),
5688
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
5689
- workspaceRoot: root
5690
- };
6139
+ const installedAt = (/* @__PURE__ */ new Date()).toISOString();
6140
+ const receipt = entry.hostInstallUser === "root" ? { scope: "root", binary: entry.binary, installedAt, workspaceRoot: root } : { prefix, binary: entry.binary, binPath: harnessBinPath(prefix), installedAt, workspaceRoot: root };
5691
6141
  try {
5692
6142
  const config = readHostConfig(env, home);
5693
6143
  writeHostConfig(
@@ -5747,7 +6197,7 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5747
6197
  }
5748
6198
  if (entry.installArgv === void 0) return refuseContainer();
5749
6199
  const already = await probeInstalled2(target, entry, "sandbox");
5750
- if (already === true) {
6200
+ if (already === true && await sandboxMarkerExists2(target, entry)) {
5751
6201
  io.stdout(`${entry.id}: already installed (${entry.binary})
5752
6202
  `);
5753
6203
  return 0;
@@ -5769,6 +6219,17 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5769
6219
  `);
5770
6220
  return r.exitCode;
5771
6221
  }
6222
+ const marker = sandboxMarkerPath2(entry);
6223
+ const marked = await target.exec({
6224
+ argv: ["sh", "-c", `mkdir -p "$(dirname "$1")" && : > "$1"`, "sh", marker],
6225
+ user: "sandbox",
6226
+ stdio: "capture"
6227
+ });
6228
+ if (marked.exitCode !== 0) {
6229
+ io.stderr(`${opts.bin} tool: could not record the install at ${marker} (exit ${marked.exitCode}).
6230
+ `);
6231
+ return 1;
6232
+ }
5772
6233
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)}
5773
6234
  `);
5774
6235
  return 0;
@@ -5829,7 +6290,7 @@ Removing it could delete a tool you installed yourself. Re-run with \`--force\`
5829
6290
  );
5830
6291
  return 1;
5831
6292
  }
5832
- const prefix = receipt?.prefix ?? computed;
6293
+ const prefix = receipt === void 0 || isRootToolReceipt(receipt) ? computed : receipt.prefix;
5833
6294
  const target = hostTargetFor2(receipt?.workspaceRoot ?? workspace, prefix, run, env);
5834
6295
  const outcome = await removeTool(entry, target, prefix, void 0, opts, io, true);
5835
6296
  if (outcome.dropReceipt && receipt !== void 0) {
@@ -5852,6 +6313,15 @@ async function runToolUninstall(name, opts, io) {
5852
6313
  const root = resolveProjectRoot(opts.cwd);
5853
6314
  const entry = findTool(name);
5854
6315
  if (!entry) return unknownTool(name, io, opts.bin);
6316
+ if (entry.hostInstallUser === "root") {
6317
+ io.stderr(
6318
+ `${opts.bin} tool: ${entry.id} cannot be removed by this command.
6319
+ ${opts.bin} does not remove system packages that it installed as root.
6320
+ Remove ${entry.id} by hand: ${ROOT_TOOL_REMOVAL_DOCS}
6321
+ `
6322
+ );
6323
+ return 1;
6324
+ }
5855
6325
  if (entry.uninstallArgv === null) {
5856
6326
  io.stderr(`${opts.bin} tool: ${entry.id} cannot be removed by this command.
5857
6327
 
@@ -5864,7 +6334,20 @@ async function runToolUninstall(name, opts, io) {
5864
6334
  if (!isReachable2(await resolveTargetStatus(target))) {
5865
6335
  return await uninstallOnHost2(entry, opts, io, run);
5866
6336
  }
5867
- return (await removeTool(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
6337
+ const outcome = await removeTool(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io);
6338
+ if (outcome.dropReceipt) {
6339
+ const cleared = await target.exec({
6340
+ argv: ["rm", "-f", sandboxMarkerPath2(entry)],
6341
+ user: "sandbox",
6342
+ stdio: "capture"
6343
+ });
6344
+ if (cleared.exitCode !== 0) {
6345
+ io.stderr(`${opts.bin} tool: could not clear the install record (exit ${cleared.exitCode}).
6346
+ `);
6347
+ return 1;
6348
+ }
6349
+ }
6350
+ return outcome.code;
5868
6351
  }
5869
6352
 
5870
6353
  // src/lib/remote.ts
@@ -5944,7 +6427,7 @@ var INTEGRATIONS = {
5944
6427
  )
5945
6428
  }
5946
6429
  };
5947
- function isHelpFlag(arg) {
6430
+ function isHelpFlag2(arg) {
5948
6431
  return arg === "--help" || arg === "-h" || arg === "help";
5949
6432
  }
5950
6433
  function isVersionFlag(arg) {
@@ -5961,7 +6444,7 @@ function printAgroHelp(product = AGRO_PRODUCT) {
5961
6444
  process.stdout.write(`${bin} \u2014 ${title} (v${AGRO_VERSION})
5962
6445
 
5963
6446
  Usage:
5964
- ${bin} sandbox <args...> Create and list sandboxes (install|list)
6447
+ ${bin} sandbox <args...> Create, list, and upgrade sandboxes (install|list|upgrade)
5965
6448
  ${bin} shell [name] Open a zsh shell in the running sandbox container
5966
6449
  ${bin} config <args...> Read and write ${stateNames(bin).configFile} (show|set), or run a wizard
5967
6450
  ${bin} secret <args...> Read and write the gitignored root .env (set|list)
@@ -6111,66 +6594,6 @@ Flags:
6111
6594
  --force Override the up-to-date / downgrade gate.
6112
6595
  `);
6113
6596
  }
6114
- function runtimeLines() {
6115
- const width = Math.max(...RUNTIME_CATALOG.map((r) => r.id.length));
6116
- return RUNTIME_CATALOG.map(
6117
- (r) => ` ${r.id.padEnd(width)} ${r.provisionable ? "provisionable" : "planned"}`
6118
- ).join("\n");
6119
- }
6120
- function printSandboxHelp(bin = AGRO_PRODUCT.bin) {
6121
- process.stdout.write(`${bin} sandbox \u2014 Create and list sandboxes
6122
-
6123
- Usage:
6124
- ${bin} sandbox install <runtime> [--name <name>] [--checkout <dir>]
6125
- [--home-mount <dir>] [--yes]
6126
- [--version <X.Y.Z> | --image[=<ref>]]
6127
- [--no-build] [--print-argv]
6128
- ${bin} sandbox list [--json]
6129
-
6130
- \`install\` writes a sandbox entry under \${${stateNames(bin).envPrefix}HOME:-~/${stateNames(bin).userStateDir}}/sandboxes/<name>/,
6131
- materialises the compose files and the compose wrapper into it, then starts the
6132
- container. It needs no project checkout and runs from any directory. Without
6133
- --checkout the sandbox runs the prebuilt image; with --checkout <dir> that
6134
- checkout is bound at /home/sandbox/harness; the sandbox builds locally only
6135
- when that directory holds .devcontainer/Dockerfile.
6136
-
6137
- On a terminal without --yes it asks for the sandbox name, the timezone, the git
6138
- identity, SSH (with its host port), the host Docker socket, and the host path
6139
- for /home/sandbox. With --checkout it seeds those answers from that
6140
- checkout's ${stateNames(bin).configFile}.
6141
-
6142
- Flags:
6143
- --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
6144
- --checkout <dir> Bind this checkout into the sandbox and seed the defaults
6145
- from its ${stateNames(bin).configFile}. The sandbox builds
6146
- locally only when that directory holds
6147
- .devcontainer/Dockerfile
6148
- --repo <dir> Deprecated alias for --checkout
6149
- --home-mount <dir>
6150
- Bind this host directory at /home/sandbox instead of the
6151
- Docker-managed volume. Relative paths resolve against the
6152
- working directory. The directory is created when absent
6153
- --yes Non-interactive: keep every default and ask nothing
6154
- --version <X.Y.Z>
6155
- Run the official release image ghcr.io/mifunedev/agro:<X.Y.Z>
6156
- (implies --image). A leading v and a -<channel>.<n>
6157
- pre-release suffix are accepted
6158
- --image Run the prebuilt image instead of building (implies
6159
- --no-build). The ref resolves first-match: --version or
6160
- --image=<ref> > ${stateNames(bin).envPrefix}SANDBOX_IMAGE >
6161
- ${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
6162
- --image=<ref> Run a custom image (implies --image); conflicts with --version
6163
- --no-build Suppress the local build and reuse an existing image
6164
- --print-argv Print the docker compose argv that would run, then exit
6165
- without writing an entry
6166
- --json Machine-readable output (list)
6167
-
6168
- Runtimes:
6169
- ${runtimeLines()}
6170
-
6171
- Next: ${bin} shell <name>
6172
- `);
6173
- }
6174
6597
  function printShellHelp(bin = AGRO_PRODUCT.bin) {
6175
6598
  process.stdout.write(`${bin} shell \u2014 Open a shell in the running sandbox container
6176
6599
 
@@ -6295,6 +6718,8 @@ no workspace: run \`${bin} workspace create\` first. It installs into
6295
6718
  \`~/.local\` and records the install as \`hostTools\` in that same file. On the host,
6296
6719
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
6297
6720
  Without a record it refuses, and \`--force\` overrides.
6721
+ \`uninstall\` refuses each root-level tool, marked \`(root)\` in \`${bin} tool list\`,
6722
+ with and without \`--force\`. The refusal links the manual removal steps.
6298
6723
 
6299
6724
  Flags:
6300
6725
  --yes Accept a large download without prompting
@@ -6384,7 +6809,7 @@ function extractSandboxFlag(command, rest) {
6384
6809
  }
6385
6810
  var LANGFUSE_VERBS = ["apply", "status", "disable"];
6386
6811
  function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
6387
- if (rest.length === 0 || isHelpFlag(rest[0])) return { ok: true, args: { help: true } };
6812
+ if (rest.length === 0 || isHelpFlag2(rest[0])) return { ok: true, args: { help: true } };
6388
6813
  const [head, ...tail] = rest;
6389
6814
  if (!LANGFUSE_VERBS.includes(head)) {
6390
6815
  return {
@@ -6393,7 +6818,7 @@ function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
6393
6818
  showHelp: true
6394
6819
  };
6395
6820
  }
6396
- if (isHelpFlag(tail[0])) return { ok: true, args: { help: true } };
6821
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { help: true } };
6397
6822
  if (tail.length > 0) {
6398
6823
  return { ok: false, error: `${bin} langfuse ${head}: unexpected argument "${tail[0]}"` };
6399
6824
  }
@@ -6411,12 +6836,12 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6411
6836
  ...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox },
6412
6837
  ...forced ? { force: true } : {}
6413
6838
  };
6414
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6839
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6415
6840
  return { ok: true, args: { ...args, help: true } };
6416
6841
  }
6417
6842
  const [head, ...tail] = rest;
6418
6843
  if (!CONFIG_VERBS.includes(head)) {
6419
- if (tail.length > 0 && isHelpFlag(tail[0])) {
6844
+ if (tail.length > 0 && isHelpFlag2(tail[0])) {
6420
6845
  return { ok: true, args: { ...args, integration: head, integrationHelp: true } };
6421
6846
  }
6422
6847
  if (tail.length > 0) {
@@ -6428,7 +6853,7 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6428
6853
  return { ok: true, args: { ...args, integration: head } };
6429
6854
  }
6430
6855
  const verb = head;
6431
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6856
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6432
6857
  if (verb === "show" || verb === "repo") {
6433
6858
  if (tail.length > 0) {
6434
6859
  return { ok: false, error: `${bin} config ${verb}: unexpected argument "${tail[0]}"` };
@@ -6460,7 +6885,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6460
6885
  help: false,
6461
6886
  ...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox }
6462
6887
  };
6463
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6888
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6464
6889
  return { ok: true, args: { ...args, help: true } };
6465
6890
  }
6466
6891
  const [head, ...tail] = rest;
@@ -6472,7 +6897,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6472
6897
  };
6473
6898
  }
6474
6899
  const verb = head;
6475
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6900
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6476
6901
  if (verb === "list") {
6477
6902
  if (tail.length > 0) {
6478
6903
  return { ok: false, error: `${bin} secret list: unexpected argument "${tail[0]}"` };
@@ -6493,7 +6918,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6493
6918
  }
6494
6919
  function parseDestroyArgs(rest, bin = AGRO_PRODUCT.bin) {
6495
6920
  const args = { help: false, yes: false };
6496
- if (isHelpFlag(rest[0])) return { ok: true, args: { ...args, help: true } };
6921
+ if (isHelpFlag2(rest[0])) return { ok: true, args: { ...args, help: true } };
6497
6922
  for (const token of rest) {
6498
6923
  if (token === "--yes") {
6499
6924
  args.yes = true;
@@ -6512,7 +6937,7 @@ function parseDestroyArgs(rest, bin = AGRO_PRODUCT.bin) {
6512
6937
  }
6513
6938
  function parseComposeArgs(rest, bin = AGRO_PRODUCT.bin) {
6514
6939
  const args = { help: false, passthrough: [] };
6515
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6940
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6516
6941
  return { ok: true, args: { ...args, help: true } };
6517
6942
  }
6518
6943
  if (rest[0] !== "config") {
@@ -6524,7 +6949,7 @@ function parseComposeArgs(rest, bin = AGRO_PRODUCT.bin) {
6524
6949
  }
6525
6950
  args.subcommand = "config";
6526
6951
  const tail = rest.slice(1);
6527
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6952
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6528
6953
  const sep4 = tail.indexOf("--");
6529
6954
  if (sep4 === -1 && tail.length > 0) {
6530
6955
  return {
@@ -6550,7 +6975,7 @@ function parseSelfUpgradeArgs(rest, bin = AGRO_PRODUCT.bin) {
6550
6975
  args.dryRun = true;
6551
6976
  continue;
6552
6977
  }
6553
- if (isHelpFlag(arg)) {
6978
+ if (isHelpFlag2(arg)) {
6554
6979
  args.help = true;
6555
6980
  return { ok: true, args };
6556
6981
  }
@@ -6592,7 +7017,7 @@ function parseVendorArgs(rest, bin = AGRO_PRODUCT.bin) {
6592
7017
  args.force = true;
6593
7018
  continue;
6594
7019
  }
6595
- if (isHelpFlag(arg)) {
7020
+ if (isHelpFlag2(arg)) {
6596
7021
  args.help = true;
6597
7022
  return { ok: true, args };
6598
7023
  }
@@ -6609,121 +7034,9 @@ function parseVendorArgs(rest, bin = AGRO_PRODUCT.bin) {
6609
7034
  }
6610
7035
  return { ok: true, args };
6611
7036
  }
6612
- var SANDBOX_VALUE_FLAGS = {
6613
- "--name": "name",
6614
- "--checkout": "checkout",
6615
- "--repo": "checkout",
6616
- "--home-mount": "homeMount"
6617
- };
6618
- function parseSandboxArgs(rest, bin = AGRO_PRODUCT.bin) {
6619
- const args = {
6620
- help: false,
6621
- yes: false,
6622
- image: false,
6623
- noBuild: false,
6624
- printArgv: false,
6625
- json: false
6626
- };
6627
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6628
- return { ok: true, args: { ...args, help: true } };
6629
- }
6630
- const [head, ...tail] = rest;
6631
- if (head !== "install" && head !== "list") {
6632
- return {
6633
- ok: false,
6634
- error: `${bin} sandbox: unknown subcommand "${head}" \u2014 expected install or list`,
6635
- showHelp: true
6636
- };
6637
- }
6638
- args.subcommand = head;
6639
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6640
- const positionals = [];
6641
- const checkoutSpellings = /* @__PURE__ */ new Set();
6642
- let version;
6643
- for (let i = 0; i < tail.length; i++) {
6644
- const token = tail[i];
6645
- const valueFlag = SANDBOX_VALUE_FLAGS[token];
6646
- if (valueFlag !== void 0) {
6647
- const value = tail[i + 1];
6648
- if (value === void 0) {
6649
- return { ok: false, error: `${bin} sandbox ${head}: ${token} requires a value` };
6650
- }
6651
- if (valueFlag === "checkout") checkoutSpellings.add(token);
6652
- args[valueFlag] = value;
6653
- i++;
6654
- } else if (token === "--yes") {
6655
- args.yes = true;
6656
- } else if (token === "--no-build") {
6657
- args.noBuild = true;
6658
- } else if (token === "--print-argv") {
6659
- args.printArgv = true;
6660
- } else if (token === "--image") {
6661
- args.image = true;
6662
- } else if (token.startsWith("--image=")) {
6663
- const ref = token.slice("--image=".length);
6664
- if (ref === "") {
6665
- return { ok: false, error: `${bin} sandbox: --image=<ref> requires a non-empty image ref` };
6666
- }
6667
- args.image = true;
6668
- args.imageRef = ref;
6669
- } else if (token === "--version" || token.startsWith("--version=")) {
6670
- const value = token === "--version" ? tail[++i] : token.slice("--version=".length);
6671
- if (value === void 0 || value === "") {
6672
- return { ok: false, error: `${bin} sandbox ${head}: --version requires a value` };
6673
- }
6674
- version = parseReleaseVersion(value);
6675
- if (version === void 0) {
6676
- return {
6677
- ok: false,
6678
- error: `${bin} sandbox ${head}: --version "${value}" is not a release version \u2014 expected X.Y.Z`
6679
- };
6680
- }
6681
- } else if (token === "--json") {
6682
- args.json = true;
6683
- } else if (token.startsWith("-")) {
6684
- return { ok: false, error: `${bin} sandbox ${head}: unknown flag "${token}"` };
6685
- } else {
6686
- positionals.push(token);
6687
- }
6688
- }
6689
- if (checkoutSpellings.has("--checkout") && checkoutSpellings.has("--repo")) {
6690
- return {
6691
- ok: false,
6692
- error: `${bin} sandbox ${head}: --checkout conflicts with --repo \u2014 pass exactly one, and prefer --checkout`
6693
- };
6694
- }
6695
- if (version !== void 0) {
6696
- if (args.imageRef !== void 0) {
6697
- return {
6698
- ok: false,
6699
- error: `${bin} sandbox ${head}: --version conflicts with --image=<ref> \u2014 pass --version for an official release or --image=<ref> for a custom image`
6700
- };
6701
- }
6702
- args.image = true;
6703
- args.imageRef = officialImageRef(version);
6704
- }
6705
- if (head === "list") {
6706
- if (positionals.length > 0) {
6707
- return { ok: false, error: `${bin} sandbox list: unexpected argument "${positionals[0]}"` };
6708
- }
6709
- return { ok: true, args };
6710
- }
6711
- if (positionals.length === 0) {
6712
- return {
6713
- ok: false,
6714
- error: `${bin} sandbox install: a runtime is required, e.g. \`${bin} sandbox install docker\``,
6715
- showHelp: true
6716
- };
6717
- }
6718
- if (positionals.length > 1) {
6719
- return { ok: false, error: `${bin} sandbox install: unexpected argument "${positionals[1]}"` };
6720
- }
6721
- args.runtime = positionals[0];
6722
- return { ok: true, args };
6723
- }
6724
7037
  function parseShellArgs(rest, bin = AGRO_PRODUCT.bin) {
6725
7038
  const args = { help: false };
6726
- if (isHelpFlag(rest[0])) return { ok: true, args: { help: true } };
7039
+ if (isHelpFlag2(rest[0])) return { ok: true, args: { help: true } };
6727
7040
  for (const token of rest) {
6728
7041
  if (token.startsWith("-")) {
6729
7042
  return { ok: false, error: `${bin} shell: unknown flag "${token}"` };
@@ -6737,7 +7050,7 @@ function parseShellArgs(rest, bin = AGRO_PRODUCT.bin) {
6737
7050
  }
6738
7051
  function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6739
7052
  const args = { help: false, json: false };
6740
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7053
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6741
7054
  return { ok: true, args: { ...args, help: true } };
6742
7055
  }
6743
7056
  const positionals = [];
@@ -6809,7 +7122,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
6809
7122
  host: false,
6810
7123
  force: false
6811
7124
  };
6812
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7125
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6813
7126
  return { ok: true, args: { ...args, help: true } };
6814
7127
  }
6815
7128
  const positionals = [];
@@ -6890,7 +7203,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
6890
7203
  }
6891
7204
  function parseToolArgs(rest, bin = AGRO_PRODUCT.bin) {
6892
7205
  const args = { help: false, yes: false, json: false, host: false, force: false };
6893
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7206
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6894
7207
  return { ok: true, args: { ...args, help: true } };
6895
7208
  }
6896
7209
  const positionals = [];
@@ -6948,11 +7261,11 @@ function parseGatewayArgs(rest) {
6948
7261
  }
6949
7262
  return { ok: true, args: { help: false, passthrough: [...rest] } };
6950
7263
  }
6951
- function bundledPayloadExists(bundled, exists = existsSync22) {
6952
- return exists(join18(bundled.sourceControlDir, "manifest.json"));
7264
+ function bundledPayloadExists(bundled, exists = existsSync23) {
7265
+ return exists(join19(bundled.sourceControlDir, "manifest.json"));
6953
7266
  }
6954
7267
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6955
- const exists = bundled.exists ?? existsSync22;
7268
+ const exists = bundled.exists ?? existsSync23;
6956
7269
  if (args.fromDir !== void 0) {
6957
7270
  return { kind: "local", fromDir: resolve11(args.fromDir) };
6958
7271
  }
@@ -6972,7 +7285,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6972
7285
  function readPayloadVersion(checkoutDir) {
6973
7286
  try {
6974
7287
  const parsed = JSON.parse(
6975
- readFileSync12(join18(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7288
+ readFileSync13(join19(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
6976
7289
  );
6977
7290
  if (parsed && typeof parsed.version === "string") return parsed.version;
6978
7291
  } catch {
@@ -6981,7 +7294,7 @@ function readPayloadVersion(checkoutDir) {
6981
7294
  }
6982
7295
  async function runWithRemoteSource(hooks, fn) {
6983
7296
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
6984
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join18(tmpdir4(), "oh-remote-")));
7297
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join19(tmpdir4(), "oh-remote-")));
6985
7298
  const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
6986
7299
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
6987
7300
  const checkoutDir = mkdtemp();
@@ -6998,7 +7311,7 @@ async function main(argv) {
6998
7311
  const product = resolveProduct(process.argv[1]);
6999
7312
  const bin = product.bin;
7000
7313
  const [first, second] = argv;
7001
- if (!first || isHelpFlag(first)) {
7314
+ if (!first || isHelpFlag2(first)) {
7002
7315
  printAgroHelp(product);
7003
7316
  return 0;
7004
7317
  }
@@ -7141,37 +7454,7 @@ async function main(argv) {
7141
7454
  (checkoutDir) => runUpdate({ bin, targetDir, fromDir: checkoutDir, force, dryRun }, io)
7142
7455
  );
7143
7456
  }
7144
- if (first === "sandbox") {
7145
- const parsed = parseSandboxArgs(argv.slice(1), bin);
7146
- if (!parsed.ok) {
7147
- process.stderr.write(`${parsed.error}
7148
- `);
7149
- if (parsed.showHelp) printSandboxHelp(bin);
7150
- return 1;
7151
- }
7152
- const a = parsed.args;
7153
- if (a.help) {
7154
- printSandboxHelp(bin);
7155
- return a.subcommand === void 0 ? 1 : 0;
7156
- }
7157
- const io = lifecycleIo();
7158
- if (a.subcommand === "list") return await runSandboxList({ bin, json: a.json }, io);
7159
- return await runSandboxInstall(
7160
- {
7161
- bin,
7162
- runtime: a.runtime,
7163
- ...a.name !== void 0 ? { name: a.name } : {},
7164
- ...a.checkout !== void 0 ? { checkout: a.checkout } : {},
7165
- ...a.homeMount !== void 0 ? { homeMount: a.homeMount } : {},
7166
- yes: a.yes,
7167
- image: a.image,
7168
- ...a.imageRef !== void 0 ? { imageRef: a.imageRef } : {},
7169
- noBuild: a.noBuild,
7170
- printArgv: a.printArgv
7171
- },
7172
- io
7173
- );
7174
- }
7457
+ if (first === "sandbox") return runSandboxCommand(argv.slice(1), bin);
7175
7458
  if (first === "shell") {
7176
7459
  const parsed = parseShellArgs(argv.slice(1), bin);
7177
7460
  if (!parsed.ok) {
@@ -7256,7 +7539,7 @@ async function main(argv) {
7256
7539
  if (composeVerbs().includes(first)) {
7257
7540
  const verb = first;
7258
7541
  const rest = argv.slice(1);
7259
- if (isHelpFlag(rest[0])) {
7542
+ if (isHelpFlag2(rest[0])) {
7260
7543
  printComposeVerbHelp(verb, bin);
7261
7544
  return 0;
7262
7545
  }
@@ -7386,7 +7669,7 @@ export {
7386
7669
  SECRET_VERBS,
7387
7670
  bundledPayloadExists,
7388
7671
  extractSandboxFlag,
7389
- isHelpFlag,
7672
+ isHelpFlag2 as isHelpFlag,
7390
7673
  isVersionFlag,
7391
7674
  parseComposeArgs,
7392
7675
  parseConfigArgs,