@mifune/agro 0.15.0 → 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 +652 -345
  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 join17, resolve as resolve11 } from "node:path";
11
- import { existsSync as existsSync21, 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.0" : "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"],
@@ -3732,9 +3732,9 @@ async function runLangfuseSetup(opts, io) {
3732
3732
  }
3733
3733
 
3734
3734
  // src/commands/sandbox.ts
3735
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
3735
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
3736
3736
  import { tmpdir as tmpdir3 } from "node:os";
3737
- import { join as join11, resolve as resolve7 } from "node:path";
3737
+ import { join as join12, resolve as resolve7 } from "node:path";
3738
3738
 
3739
3739
  // src/lib/runtimes/catalog.ts
3740
3740
  var RUNTIME_CATALOG = Object.freeze([
@@ -3763,6 +3763,76 @@ function runtimeIds() {
3763
3763
  return RUNTIME_CATALOG.map((r) => r.id);
3764
3764
  }
3765
3765
 
3766
+ // src/lib/star-prompt.ts
3767
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
3768
+ import { join as join11 } from "node:path";
3769
+ var STAR_PROMPT_LINE = "\u2B50 If AGRO helps, star https://github.com/mifunedev/agro";
3770
+ var MARKER = "star-prompt-shown";
3771
+ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTTY === true, stateHome = resolveUserStateHome(env)) {
3772
+ if (env.AGRO_NO_STAR_PROMPT === "1") return;
3773
+ if (env.CI !== void 0 && env.CI !== "") return;
3774
+ if (!isTTY) return;
3775
+ const marker = join11(stateHome, MARKER);
3776
+ if (existsSync15(marker)) return;
3777
+ io.stdout(`${STAR_PROMPT_LINE}
3778
+ `);
3779
+ try {
3780
+ mkdirSync7(stateHome, { recursive: true });
3781
+ writeFileSync8(marker, "");
3782
+ } catch {
3783
+ return;
3784
+ }
3785
+ }
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
+
3766
3836
  // src/commands/sandbox.ts
3767
3837
  function hostTimezone() {
3768
3838
  const tz = process.env.TZ;
@@ -3779,27 +3849,27 @@ function gitIdentity(run, key) {
3779
3849
  return (result.stdout ?? "").trim();
3780
3850
  }
3781
3851
  function isBuildCapable(repo) {
3782
- return repo !== void 0 && existsSync15(join11(repo, ".devcontainer", "Dockerfile"));
3852
+ return repo !== void 0 && existsSync16(join12(repo, ".devcontainer", "Dockerfile"));
3783
3853
  }
3784
3854
  function prepareHomeMount(value) {
3785
3855
  const path4 = resolve7(value);
3786
- if (existsSync15(path4)) {
3856
+ if (existsSync16(path4)) {
3787
3857
  if (!statSync4(path4).isDirectory()) {
3788
3858
  throw new Error(`--home-mount path exists and is not a directory: ${path4}`);
3789
3859
  }
3790
3860
  return path4;
3791
3861
  }
3792
- mkdirSync7(path4, { recursive: true });
3862
+ mkdirSync8(path4, { recursive: true });
3793
3863
  return path4;
3794
3864
  }
3795
3865
  function readSeedConfig(repo) {
3796
3866
  if (repo === void 0) return void 0;
3797
3867
  const file = agroConfigPath(repo);
3798
- return existsSync15(file) ? readAgroConfig(file) : void 0;
3868
+ return existsSync16(file) ? readAgroConfig(file) : void 0;
3799
3869
  }
3800
3870
  function readEntryConfig(name) {
3801
3871
  const file = agroConfigPath(entryRoot(name));
3802
- return existsSync15(file) ? readAgroConfig(file) : void 0;
3872
+ return existsSync16(file) ? readAgroConfig(file) : void 0;
3803
3873
  }
3804
3874
  function assignSetting(target, path4, value) {
3805
3875
  const segments = path4.split(".");
@@ -3896,7 +3966,7 @@ async function runSandboxInstall(opts, io) {
3896
3966
  return 1;
3897
3967
  }
3898
3968
  const checkout = opts.checkout === void 0 ? void 0 : resolve7(opts.checkout);
3899
- if (checkout !== void 0 && !existsSync15(checkout)) {
3969
+ if (checkout !== void 0 && !existsSync16(checkout)) {
3900
3970
  io.stderr(`${opts.bin} sandbox install: --checkout directory does not exist: ${checkout}
3901
3971
  `);
3902
3972
  return 1;
@@ -3915,7 +3985,7 @@ async function runSandboxInstall(opts, io) {
3915
3985
  if (buildRequested && !isBuildCapable(configCheckout(config))) {
3916
3986
  const target = configCheckout(config) ?? resolve7(opts.checkout ?? ".");
3917
3987
  io.stderr(
3918
- `${opts.bin} sandbox install: image.mode is "build" but ${join11(target, ".devcontainer", "Dockerfile")} does not exist \u2014 point --checkout <dir> at a harness checkout that has .devcontainer/Dockerfile, or set image.mode to "image" to run the prebuilt image
3988
+ `${opts.bin} sandbox install: image.mode is "build" but ${join12(target, ".devcontainer", "Dockerfile")} does not exist \u2014 point --checkout <dir> at a harness checkout that has .devcontainer/Dockerfile, or set image.mode to "image" to run the prebuilt image
3919
3989
  `
3920
3990
  );
3921
3991
  return 1;
@@ -3959,7 +4029,7 @@ async function runSandboxInstall(opts, io) {
3959
4029
  ...useNoBuild ? { noBuild: true } : {}
3960
4030
  };
3961
4031
  if (opts.printArgv === true) {
3962
- const preview = mkdtempSync3(join11(tmpdir3(), "oh-sandbox-preview-"));
4032
+ const preview = mkdtempSync3(join12(tmpdir3(), "oh-sandbox-preview-"));
3963
4033
  try {
3964
4034
  writeAgroConfig(preview, config);
3965
4035
  materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
@@ -3969,66 +4039,395 @@ async function runSandboxInstall(opts, io) {
3969
4039
  }
3970
4040
  }
3971
4041
  const root = entryRoot(config.name);
3972
- mkdirSync7(root, { recursive: true });
4042
+ mkdirSync8(root, { recursive: true });
3973
4043
  writeAgroConfig(root, config);
3974
4044
  materialize(root, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
3975
4045
  const code = await runSandbox({ ...sandboxOpts, cwd: root }, io);
3976
- if (code === 0) io.stdout(`next: ${opts.bin} shell ${config.name}
4046
+ if (code === 0) {
4047
+ io.stdout(`next: ${opts.bin} shell ${config.name}
3977
4048
  `);
4049
+ maybePrintStarPrompt(io);
4050
+ }
3978
4051
  return code;
3979
4052
  }
3980
- 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;
3981
4087
  try {
3982
- const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
3983
- return await target.status();
3984
- } catch {
3985
- 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);
3986
4179
  }
3987
4180
  }
3988
- async function runSandboxList(opts, io) {
3989
- const run = opts.run ?? spawnRunner;
3990
- const rows = [];
3991
- for (const name of listEntries()) {
3992
- const root = entryRoot(name);
3993
- const config = readAgroConfig(agroConfigPath(root));
3994
- const checkout = configCheckout(config) ?? "-";
3995
- rows.push({
3996
- name,
3997
- runtime: config.runtime ?? "docker",
3998
- checkout,
3999
- repo: checkout,
4000
- status: await entryStatus(root, name, run)
4001
- });
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 } };
4002
4264
  }
4003
- if (opts.json === true) {
4004
- 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}
4005
4393
  `);
4006
- return 0;
4394
+ if (parsed.showHelp) printSandboxHelp(bin);
4395
+ return 1;
4007
4396
  }
4008
- if (rows.length === 0) {
4009
- io.stdout(
4010
- `no sandbox is registered in ${registryRoot()} \u2014 create one with \`${opts.bin} sandbox install docker\`
4011
- `
4012
- );
4013
- return 0;
4397
+ const a = parsed.args;
4398
+ if (a.help) {
4399
+ printSandboxHelp(bin);
4400
+ return a.subcommand === void 0 ? 1 : 0;
4014
4401
  }
4015
- const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
4016
- const nameWidth = width((row) => row.name);
4017
- const runtimeWidth = width((row) => row.runtime);
4018
- const statusWidth = width((row) => row.status);
4019
- for (const row of rows) {
4020
- io.stdout(
4021
- `${row.name.padEnd(nameWidth)} ${row.runtime.padEnd(runtimeWidth)} ${row.status.padEnd(statusWidth)} ${row.checkout}
4022
- `
4023
- );
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);
4024
4409
  }
4025
- 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
+ );
4026
4425
  }
4027
4426
 
4028
4427
  // src/commands/harness.ts
4029
- import { existsSync as existsSync18 } from "node:fs";
4428
+ import { existsSync as existsSync20 } from "node:fs";
4030
4429
  import { homedir as homedir3 } from "node:os";
4031
- import { delimiter as delimiter2, join as join14 } from "node:path";
4430
+ import { delimiter as delimiter2, join as join16 } from "node:path";
4032
4431
 
4033
4432
  // src/lib/docs.ts
4034
4433
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4041,17 +4440,20 @@ function sourceDocsUrl(docsPath) {
4041
4440
  }
4042
4441
 
4043
4442
  // src/lib/host-config.ts
4044
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync11, renameSync as renameSync4, unlinkSync as unlinkSync5, writeFileSync as writeFileSync8 } from "node:fs";
4045
- import { basename as basename5, dirname as dirname8, isAbsolute, join as join12, 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";
4046
4445
  var HOST_CONFIG_MODE = 420;
4047
4446
  var HOST_CONFIG_FILE = "config.json";
4048
4447
  var WORKSPACES_SUBDIR = "workspaces";
4049
- var DEFAULT_WORKSPACE_NAME = "default";
4448
+ var DEFAULT_WORKSPACE_NAME = "harness";
4449
+ function isRootToolReceipt(receipt) {
4450
+ return receipt.scope === "root";
4451
+ }
4050
4452
  function hostStateHome(env, home) {
4051
4453
  return resolveUserStateHome(env, home);
4052
4454
  }
4053
4455
  function hostConfigPath(env, home) {
4054
- return join12(hostStateHome(env, home), HOST_CONFIG_FILE);
4456
+ return join14(hostStateHome(env, home), HOST_CONFIG_FILE);
4055
4457
  }
4056
4458
  function assertWorkspaceName(name) {
4057
4459
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -4061,14 +4463,14 @@ function assertWorkspaceName(name) {
4061
4463
  }
4062
4464
  }
4063
4465
  function workspacesRoot(env, home) {
4064
- return join12(hostStateHome(env, home), WORKSPACES_SUBDIR);
4466
+ return join14(hostStateHome(env, home), WORKSPACES_SUBDIR);
4065
4467
  }
4066
4468
  function workspaceRoot(name, env, home) {
4067
4469
  assertWorkspaceName(name);
4068
- return join12(workspacesRoot(env, home), name);
4470
+ return join14(workspacesRoot(env, home), name);
4069
4471
  }
4070
4472
  function defaultHarnessRoot(env, home) {
4071
- return join12(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4473
+ return join14(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4072
4474
  }
4073
4475
  function validateHostConfig(value) {
4074
4476
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4090,7 +4492,7 @@ function validateHostConfig(value) {
4090
4492
  const hostHarnesses = record.hostHarnesses;
4091
4493
  if (hostHarnesses !== void 0) validateReceipts(hostHarnesses, "hostHarnesses", "harness");
4092
4494
  const hostTools = record.hostTools;
4093
- if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool");
4495
+ if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool", true);
4094
4496
  return { ...record, version: 1 };
4095
4497
  }
4096
4498
  function absoluteField(value, path4) {
@@ -4100,7 +4502,7 @@ function absoluteField(value, path4) {
4100
4502
  if (!isAbsolute(value)) throw fieldError2(path4, "must be an absolute path");
4101
4503
  return value;
4102
4504
  }
4103
- function validateReceipts(value, field, noun) {
4505
+ function validateReceipts(value, field, noun, allowRootScope = false) {
4104
4506
  if (!value || typeof value !== "object" || Array.isArray(value)) {
4105
4507
  throw fieldError2(field, `must be a JSON object keyed by ${noun} id`);
4106
4508
  }
@@ -4111,8 +4513,15 @@ function validateReceipts(value, field, noun) {
4111
4513
  throw fieldError2(at, "must be a JSON object");
4112
4514
  }
4113
4515
  const entry = receipt;
4114
- absoluteField(entry.prefix, `${at}.prefix`);
4115
- absoluteField(entry.binPath, `${at}.binPath`);
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
+ }
4116
4525
  if (typeof entry.binary !== "string" || entry.binary === "") {
4117
4526
  throw fieldError2(`${at}.binary`, "must be a non-empty string");
4118
4527
  }
@@ -4126,10 +4535,10 @@ function validateReceipts(value, field, noun) {
4126
4535
  }
4127
4536
  function readHostConfig(env, home) {
4128
4537
  const path4 = hostConfigPath(env, home);
4129
- if (!existsSync16(path4)) return { version: 1 };
4538
+ if (!existsSync18(path4)) return { version: 1 };
4130
4539
  let raw;
4131
4540
  try {
4132
- raw = readFileSync11(path4, "utf8");
4541
+ raw = readFileSync12(path4, "utf8");
4133
4542
  } catch (error) {
4134
4543
  const detail = error instanceof Error ? error.message : String(error);
4135
4544
  throw new Error(`could not read ${path4}: ${detail}`);
@@ -4148,13 +4557,13 @@ function writeHostConfig(config, env, home) {
4148
4557
  const body = `${JSON.stringify(validated, null, 2)}
4149
4558
  `;
4150
4559
  const tmp = `${path4}.tmp.${process.pid}`;
4151
- mkdirSync8(dirname8(path4), { recursive: true });
4560
+ mkdirSync9(dirname8(path4), { recursive: true });
4152
4561
  try {
4153
- writeFileSync8(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4562
+ writeFileSync9(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4154
4563
  renameSync4(tmp, path4);
4155
4564
  } catch (error) {
4156
4565
  try {
4157
- unlinkSync5(tmp);
4566
+ unlinkSync6(tmp);
4158
4567
  } catch {
4159
4568
  }
4160
4569
  throw error;
@@ -4177,8 +4586,8 @@ function fieldError2(path4, requirement) {
4177
4586
  }
4178
4587
 
4179
4588
  // src/lib/host-workspace.ts
4180
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
4181
- import { dirname as dirname9, join as join13, 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";
4182
4591
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4183
4592
  var IGNORABLE_ENTRIES = [
4184
4593
  REGISTRY_SUBDIR,
@@ -4190,7 +4599,7 @@ function isDirectory3(path4) {
4190
4599
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4191
4600
  }
4192
4601
  function stateHomes(home) {
4193
- return STATE_HOME_DIRS.map((name) => join13(home, name));
4602
+ return STATE_HOME_DIRS.map((name) => join15(home, name));
4194
4603
  }
4195
4604
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4196
4605
  const target = resolve9(root);
@@ -4198,7 +4607,7 @@ function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4198
4607
  if (enclosing === void 0) return void 0;
4199
4608
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
4200
4609
  A workspace there collides with AGRO state and blocks every command run from ${home}.
4201
- Move it out \u2014 for example \`mv ${enclosing} ${join13(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.
4202
4611
  `;
4203
4612
  }
4204
4613
  function isIgnorableEntry(name) {
@@ -4215,16 +4624,16 @@ function cloneInto(target, run, root, ref) {
4215
4624
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4216
4625
  }
4217
4626
  function cloneThroughStaging(root, run, ref) {
4218
- const staging = mkdtempSync4(join13(dirname9(root), "agro-clone-"));
4627
+ const staging = mkdtempSync4(join15(dirname9(root), "agro-clone-"));
4219
4628
  try {
4220
- const checkout = join13(staging, "agro");
4629
+ const checkout = join15(staging, "agro");
4221
4630
  cloneInto(checkout, run, root, ref);
4222
4631
  for (const name of readdirSync3(checkout)) {
4223
- const destination = join13(root, name);
4224
- if (existsSync17(destination)) {
4632
+ const destination = join15(root, name);
4633
+ if (existsSync19(destination)) {
4225
4634
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4226
4635
  }
4227
- renameSync5(join13(checkout, name), destination);
4636
+ renameSync5(join15(checkout, name), destination);
4228
4637
  }
4229
4638
  } finally {
4230
4639
  rmSync4(staging, { recursive: true, force: true });
@@ -4232,10 +4641,10 @@ function cloneThroughStaging(root, run, ref) {
4232
4641
  }
4233
4642
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4234
4643
  const root = resolve9(path4);
4235
- if (existsSync17(join13(root, ".git"))) return { root, action: "reused" };
4644
+ if (existsSync19(join15(root, ".git"))) return { root, action: "reused" };
4236
4645
  const stats = statSync5(root, { throwIfNoEntry: false });
4237
4646
  if (stats === void 0) {
4238
- mkdirSync9(dirname9(root), { recursive: true });
4647
+ mkdirSync10(dirname9(root), { recursive: true });
4239
4648
  cloneInto(root, run, root, ref);
4240
4649
  return { root, action: "cloned" };
4241
4650
  }
@@ -4254,11 +4663,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4254
4663
  function listHostWorkspaces(env, home) {
4255
4664
  const root = workspacesRoot(env, home);
4256
4665
  if (!isDirectory3(root)) return [];
4257
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync17(join13(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();
4258
4667
  }
4259
4668
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
4260
4669
  const root = resolve9(path4);
4261
- if (existsSync17(join13(root, ".git"))) return { ok: true, root };
4670
+ if (existsSync19(join15(root, ".git"))) return { ok: true, root };
4262
4671
  const existing = listHostWorkspaces(env, home);
4263
4672
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
4264
4673
  `;
@@ -4288,7 +4697,7 @@ function targetFor(root, run, env) {
4288
4697
  });
4289
4698
  }
4290
4699
  function hostPrefix(home) {
4291
- return join14(home, ".local");
4700
+ return join16(home, ".local");
4292
4701
  }
4293
4702
  function pathEntries(env) {
4294
4703
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -4322,6 +4731,21 @@ async function probeInstalled(target, entry, prefix, user, env) {
4322
4731
  throw err;
4323
4732
  }
4324
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
+ }
4325
4749
  function stateOf(entry, installed, location) {
4326
4750
  return {
4327
4751
  id: entry.id,
@@ -4341,7 +4765,7 @@ function probeableHost(env, home) {
4341
4765
  return void 0;
4342
4766
  }
4343
4767
  const prefix = hostPrefix(home);
4344
- if (!existsSync18(join14(root, ".git")) && !existsSync18(prefix)) return void 0;
4768
+ if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
4345
4769
  return { root, prefix };
4346
4770
  }
4347
4771
  async function collectStates(root, run, home, env, only) {
@@ -4547,7 +4971,15 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4547
4971
  const code = await reconcileHermes(target, io, bin, void 0, root);
4548
4972
  if (code !== 0) return code;
4549
4973
  }
4550
- 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))) {
4551
4983
  io.stdout(`${entry.id}: already installed (${entry.binary})
4552
4984
  `);
4553
4985
  try {
@@ -4711,7 +5143,20 @@ async function runHarnessUninstall(name, opts, io) {
4711
5143
  if (!isReachable(await resolveTargetStatus(target))) {
4712
5144
  return await uninstallOnHost(entry, opts, io, run);
4713
5145
  }
4714
- 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;
4715
5160
  }
4716
5161
  async function runHarnessInstall(name, opts, io) {
4717
5162
  const run = opts.run ?? spawnRunner;
@@ -4737,7 +5182,7 @@ async function runHarnessInstall(name, opts, io) {
4737
5182
  if (code !== 0) return code;
4738
5183
  }
4739
5184
  const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
4740
- if (already === true) {
5185
+ if (already === true && await sandboxMarkerExists(target, entry)) {
4741
5186
  io.stdout(`${entry.id}: already installed (${entry.binary})
4742
5187
  `);
4743
5188
  return 0;
@@ -4768,15 +5213,26 @@ async function runHarnessInstall(name, opts, io) {
4768
5213
  const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4769
5214
  if (code !== 0) return code;
4770
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
+ }
4771
5227
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
4772
5228
  `);
4773
5229
  return 0;
4774
5230
  }
4775
5231
 
4776
5232
  // src/commands/workspace.ts
4777
- import { existsSync as existsSync19, rmSync as rmSync5 } from "node:fs";
5233
+ import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
4778
5234
  import { homedir as homedir4 } from "node:os";
4779
- import { join as join15, resolve as resolve10 } from "node:path";
5235
+ import { join as join17, resolve as resolve10 } from "node:path";
4780
5236
  function homeOf2(opts) {
4781
5237
  return (opts.homedir ?? homedir4)();
4782
5238
  }
@@ -4822,9 +5278,9 @@ async function runWorkspaceCreate(name, opts, io) {
4822
5278
  return 1;
4823
5279
  }
4824
5280
  let action;
4825
- const preexisting = existsSync19(root);
5281
+ const preexisting = existsSync21(root);
4826
5282
  try {
4827
- if (!opts.json && !existsSync19(join15(root, ".git"))) {
5283
+ if (!opts.json && !existsSync21(join17(root, ".git"))) {
4828
5284
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
4829
5285
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
4830
5286
  `);
@@ -4869,7 +5325,7 @@ async function runWorkspaceList(opts, io) {
4869
5325
  const recorded = recordedRoot(env, home);
4870
5326
  const registry = workspacesRoot(env, home);
4871
5327
  rows = listHostWorkspaces(env, home).map((name) => {
4872
- const root = join15(registry, name);
5328
+ const root = join17(registry, name);
4873
5329
  return { name, root, default: recorded === root };
4874
5330
  });
4875
5331
  } catch (err) {
@@ -4894,9 +5350,9 @@ async function runWorkspaceList(opts, io) {
4894
5350
  }
4895
5351
 
4896
5352
  // src/commands/tool.ts
4897
- import { existsSync as existsSync20 } from "node:fs";
5353
+ import { existsSync as existsSync22 } from "node:fs";
4898
5354
  import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
4899
- import { delimiter as delimiter3, join as join16 } from "node:path";
5355
+ import { delimiter as delimiter3, join as join18 } from "node:path";
4900
5356
 
4901
5357
  // src/lib/tools/catalog.ts
4902
5358
  var TOOLS_DOC = "docs/installation.md";
@@ -5313,6 +5769,7 @@ function resolveToolInstallArgv(entry, host) {
5313
5769
  }
5314
5770
 
5315
5771
  // src/commands/tool.ts
5772
+ var ROOT_TOOL_REMOVAL_DOCS = `${sourceDocsUrl("docs/installation.md")}#remove-a-root-level-tool`;
5316
5773
  function isReachable2(status) {
5317
5774
  return status === "ready" || status === "starting";
5318
5775
  }
@@ -5326,7 +5783,21 @@ function targetFor2(root, run, env) {
5326
5783
  });
5327
5784
  }
5328
5785
  function hostPrefix2(home) {
5329
- return join16(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));
5330
5801
  }
5331
5802
  function pathEntries2(env) {
5332
5803
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -5406,7 +5877,7 @@ function probeableHost2(env, home) {
5406
5877
  return void 0;
5407
5878
  }
5408
5879
  const prefix = hostPrefix2(home);
5409
- if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
5880
+ if (!existsSync22(join18(root, ".git")) && !existsSync22(prefix)) return void 0;
5410
5881
  return { root, prefix };
5411
5882
  }
5412
5883
  async function collectRows(root, run, home, env, only) {
@@ -5622,7 +6093,15 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5622
6093
  const prefix = hostPrefix2(home);
5623
6094
  const installEnv = { NPM_USER_PREFIX: prefix };
5624
6095
  const target = hostTargetFor2(root, prefix, run, env);
5625
- 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))) {
5626
6105
  io.stdout(`${entry.id}: already installed (${entry.binary})
5627
6106
  `);
5628
6107
  try {
@@ -5657,13 +6136,8 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5657
6136
  `);
5658
6137
  return r.exitCode;
5659
6138
  }
5660
- const receipt = {
5661
- prefix,
5662
- binary: entry.binary,
5663
- binPath: harnessBinPath(prefix),
5664
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
5665
- workspaceRoot: root
5666
- };
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 };
5667
6141
  try {
5668
6142
  const config = readHostConfig(env, home);
5669
6143
  writeHostConfig(
@@ -5723,7 +6197,7 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5723
6197
  }
5724
6198
  if (entry.installArgv === void 0) return refuseContainer();
5725
6199
  const already = await probeInstalled2(target, entry, "sandbox");
5726
- if (already === true) {
6200
+ if (already === true && await sandboxMarkerExists2(target, entry)) {
5727
6201
  io.stdout(`${entry.id}: already installed (${entry.binary})
5728
6202
  `);
5729
6203
  return 0;
@@ -5745,6 +6219,17 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5745
6219
  `);
5746
6220
  return r.exitCode;
5747
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
+ }
5748
6233
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)}
5749
6234
  `);
5750
6235
  return 0;
@@ -5805,7 +6290,7 @@ Removing it could delete a tool you installed yourself. Re-run with \`--force\`
5805
6290
  );
5806
6291
  return 1;
5807
6292
  }
5808
- const prefix = receipt?.prefix ?? computed;
6293
+ const prefix = receipt === void 0 || isRootToolReceipt(receipt) ? computed : receipt.prefix;
5809
6294
  const target = hostTargetFor2(receipt?.workspaceRoot ?? workspace, prefix, run, env);
5810
6295
  const outcome = await removeTool(entry, target, prefix, void 0, opts, io, true);
5811
6296
  if (outcome.dropReceipt && receipt !== void 0) {
@@ -5828,6 +6313,15 @@ async function runToolUninstall(name, opts, io) {
5828
6313
  const root = resolveProjectRoot(opts.cwd);
5829
6314
  const entry = findTool(name);
5830
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
+ }
5831
6325
  if (entry.uninstallArgv === null) {
5832
6326
  io.stderr(`${opts.bin} tool: ${entry.id} cannot be removed by this command.
5833
6327
 
@@ -5840,7 +6334,20 @@ async function runToolUninstall(name, opts, io) {
5840
6334
  if (!isReachable2(await resolveTargetStatus(target))) {
5841
6335
  return await uninstallOnHost2(entry, opts, io, run);
5842
6336
  }
5843
- 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;
5844
6351
  }
5845
6352
 
5846
6353
  // src/lib/remote.ts
@@ -5920,7 +6427,7 @@ var INTEGRATIONS = {
5920
6427
  )
5921
6428
  }
5922
6429
  };
5923
- function isHelpFlag(arg) {
6430
+ function isHelpFlag2(arg) {
5924
6431
  return arg === "--help" || arg === "-h" || arg === "help";
5925
6432
  }
5926
6433
  function isVersionFlag(arg) {
@@ -5937,7 +6444,7 @@ function printAgroHelp(product = AGRO_PRODUCT) {
5937
6444
  process.stdout.write(`${bin} \u2014 ${title} (v${AGRO_VERSION})
5938
6445
 
5939
6446
  Usage:
5940
- ${bin} sandbox <args...> Create and list sandboxes (install|list)
6447
+ ${bin} sandbox <args...> Create, list, and upgrade sandboxes (install|list|upgrade)
5941
6448
  ${bin} shell [name] Open a zsh shell in the running sandbox container
5942
6449
  ${bin} config <args...> Read and write ${stateNames(bin).configFile} (show|set), or run a wizard
5943
6450
  ${bin} secret <args...> Read and write the gitignored root .env (set|list)
@@ -6087,66 +6594,6 @@ Flags:
6087
6594
  --force Override the up-to-date / downgrade gate.
6088
6595
  `);
6089
6596
  }
6090
- function runtimeLines() {
6091
- const width = Math.max(...RUNTIME_CATALOG.map((r) => r.id.length));
6092
- return RUNTIME_CATALOG.map(
6093
- (r) => ` ${r.id.padEnd(width)} ${r.provisionable ? "provisionable" : "planned"}`
6094
- ).join("\n");
6095
- }
6096
- function printSandboxHelp(bin = AGRO_PRODUCT.bin) {
6097
- process.stdout.write(`${bin} sandbox \u2014 Create and list sandboxes
6098
-
6099
- Usage:
6100
- ${bin} sandbox install <runtime> [--name <name>] [--checkout <dir>]
6101
- [--home-mount <dir>] [--yes]
6102
- [--version <X.Y.Z> | --image[=<ref>]]
6103
- [--no-build] [--print-argv]
6104
- ${bin} sandbox list [--json]
6105
-
6106
- \`install\` writes a sandbox entry under \${${stateNames(bin).envPrefix}HOME:-~/${stateNames(bin).userStateDir}}/sandboxes/<name>/,
6107
- materialises the compose files and the compose wrapper into it, then starts the
6108
- container. It needs no project checkout and runs from any directory. Without
6109
- --checkout the sandbox runs the prebuilt image; with --checkout <dir> that
6110
- checkout is bound at /home/sandbox/harness; the sandbox builds locally only
6111
- when that directory holds .devcontainer/Dockerfile.
6112
-
6113
- On a terminal without --yes it asks for the sandbox name, the timezone, the git
6114
- identity, SSH (with its host port), the host Docker socket, and the host path
6115
- for /home/sandbox. With --checkout it seeds those answers from that
6116
- checkout's ${stateNames(bin).configFile}.
6117
-
6118
- Flags:
6119
- --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
6120
- --checkout <dir> Bind this checkout into the sandbox and seed the defaults
6121
- from its ${stateNames(bin).configFile}. The sandbox builds
6122
- locally only when that directory holds
6123
- .devcontainer/Dockerfile
6124
- --repo <dir> Deprecated alias for --checkout
6125
- --home-mount <dir>
6126
- Bind this host directory at /home/sandbox instead of the
6127
- Docker-managed volume. Relative paths resolve against the
6128
- working directory. The directory is created when absent
6129
- --yes Non-interactive: keep every default and ask nothing
6130
- --version <X.Y.Z>
6131
- Run the official release image ghcr.io/mifunedev/agro:<X.Y.Z>
6132
- (implies --image). A leading v and a -<channel>.<n>
6133
- pre-release suffix are accepted
6134
- --image Run the prebuilt image instead of building (implies
6135
- --no-build). The ref resolves first-match: --version or
6136
- --image=<ref> > ${stateNames(bin).envPrefix}SANDBOX_IMAGE >
6137
- ${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
6138
- --image=<ref> Run a custom image (implies --image); conflicts with --version
6139
- --no-build Suppress the local build and reuse an existing image
6140
- --print-argv Print the docker compose argv that would run, then exit
6141
- without writing an entry
6142
- --json Machine-readable output (list)
6143
-
6144
- Runtimes:
6145
- ${runtimeLines()}
6146
-
6147
- Next: ${bin} shell <name>
6148
- `);
6149
- }
6150
6597
  function printShellHelp(bin = AGRO_PRODUCT.bin) {
6151
6598
  process.stdout.write(`${bin} shell \u2014 Open a shell in the running sandbox container
6152
6599
 
@@ -6171,8 +6618,8 @@ Usage:
6171
6618
 
6172
6619
  \`install\` and \`uninstall\` act on the running sandbox. When no sandbox is
6173
6620
  reachable they act on the host. A host install needs an existing AGRO workspace
6174
- and creates none \u2014 run \`${bin} workspace create <name>\` first. The default root is
6175
- the workspace registry entry \`~/.agro/workspaces/default\`. \`--workspace <name>\`
6621
+ and creates none \u2014 run \`${bin} workspace create\` first. The fallback root is
6622
+ the workspace registry entry \`~/.agro/workspaces/harness\`. \`--workspace <name>\`
6176
6623
  chooses another entry and \`--path <dir>\` a directory outside the registry. A
6177
6624
  successful install records the root as \`harnessRoot\` in \`~/.agro/config.json\`. The
6178
6625
  install prefix is always \`~/.local\`.
@@ -6202,8 +6649,8 @@ Usage:
6202
6649
 
6203
6650
  A host workspace is an AGRO checkout under \`~/.agro/workspaces/<name>\`. \`create\`
6204
6651
  clones ${AGRO_REPO_URL} into that entry, and reuses an existing checkout. The
6205
- default name is \`default\`. \`--path <dir>\` creates the workspace outside the
6206
- registry instead.
6652
+ implicit name is \`harness\`. Pass \`default\` to create that named workspace.
6653
+ \`--path <dir>\` creates the workspace outside the registry instead.
6207
6654
 
6208
6655
  \`create\` records no default. A host install selects its root with
6209
6656
  \`${bin} harness install <harness> --workspace <name>\`, and that install records the
@@ -6266,11 +6713,13 @@ only on these tools:
6266
6713
  ${hostCapableToolIds().map((t) => ` ${t}`).join("\n")}
6267
6714
 
6268
6715
  A host install needs an existing AGRO workspace \u2014 \`--path <dir>\`, then
6269
- \`harnessRoot\` in \`~/.agro/config.json\`, then \`~/.agro/workspaces/default\`. It creates
6270
- no workspace: run \`${bin} workspace create <name>\` first. It installs into
6716
+ \`harnessRoot\` in \`~/.agro/config.json\`, then \`~/.agro/workspaces/harness\`. It creates
6717
+ no workspace: run \`${bin} workspace create\` first. It installs into
6271
6718
  \`~/.local\` and records the install as \`hostTools\` in that same file. On the host,
6272
6719
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
6273
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.
6274
6723
 
6275
6724
  Flags:
6276
6725
  --yes Accept a large download without prompting
@@ -6360,7 +6809,7 @@ function extractSandboxFlag(command, rest) {
6360
6809
  }
6361
6810
  var LANGFUSE_VERBS = ["apply", "status", "disable"];
6362
6811
  function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
6363
- 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 } };
6364
6813
  const [head, ...tail] = rest;
6365
6814
  if (!LANGFUSE_VERBS.includes(head)) {
6366
6815
  return {
@@ -6369,7 +6818,7 @@ function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
6369
6818
  showHelp: true
6370
6819
  };
6371
6820
  }
6372
- if (isHelpFlag(tail[0])) return { ok: true, args: { help: true } };
6821
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { help: true } };
6373
6822
  if (tail.length > 0) {
6374
6823
  return { ok: false, error: `${bin} langfuse ${head}: unexpected argument "${tail[0]}"` };
6375
6824
  }
@@ -6387,12 +6836,12 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6387
6836
  ...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox },
6388
6837
  ...forced ? { force: true } : {}
6389
6838
  };
6390
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6839
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6391
6840
  return { ok: true, args: { ...args, help: true } };
6392
6841
  }
6393
6842
  const [head, ...tail] = rest;
6394
6843
  if (!CONFIG_VERBS.includes(head)) {
6395
- if (tail.length > 0 && isHelpFlag(tail[0])) {
6844
+ if (tail.length > 0 && isHelpFlag2(tail[0])) {
6396
6845
  return { ok: true, args: { ...args, integration: head, integrationHelp: true } };
6397
6846
  }
6398
6847
  if (tail.length > 0) {
@@ -6404,7 +6853,7 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6404
6853
  return { ok: true, args: { ...args, integration: head } };
6405
6854
  }
6406
6855
  const verb = head;
6407
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6856
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6408
6857
  if (verb === "show" || verb === "repo") {
6409
6858
  if (tail.length > 0) {
6410
6859
  return { ok: false, error: `${bin} config ${verb}: unexpected argument "${tail[0]}"` };
@@ -6436,7 +6885,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6436
6885
  help: false,
6437
6886
  ...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox }
6438
6887
  };
6439
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6888
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6440
6889
  return { ok: true, args: { ...args, help: true } };
6441
6890
  }
6442
6891
  const [head, ...tail] = rest;
@@ -6448,7 +6897,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6448
6897
  };
6449
6898
  }
6450
6899
  const verb = head;
6451
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6900
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6452
6901
  if (verb === "list") {
6453
6902
  if (tail.length > 0) {
6454
6903
  return { ok: false, error: `${bin} secret list: unexpected argument "${tail[0]}"` };
@@ -6469,7 +6918,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6469
6918
  }
6470
6919
  function parseDestroyArgs(rest, bin = AGRO_PRODUCT.bin) {
6471
6920
  const args = { help: false, yes: false };
6472
- if (isHelpFlag(rest[0])) return { ok: true, args: { ...args, help: true } };
6921
+ if (isHelpFlag2(rest[0])) return { ok: true, args: { ...args, help: true } };
6473
6922
  for (const token of rest) {
6474
6923
  if (token === "--yes") {
6475
6924
  args.yes = true;
@@ -6488,7 +6937,7 @@ function parseDestroyArgs(rest, bin = AGRO_PRODUCT.bin) {
6488
6937
  }
6489
6938
  function parseComposeArgs(rest, bin = AGRO_PRODUCT.bin) {
6490
6939
  const args = { help: false, passthrough: [] };
6491
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6940
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6492
6941
  return { ok: true, args: { ...args, help: true } };
6493
6942
  }
6494
6943
  if (rest[0] !== "config") {
@@ -6500,7 +6949,7 @@ function parseComposeArgs(rest, bin = AGRO_PRODUCT.bin) {
6500
6949
  }
6501
6950
  args.subcommand = "config";
6502
6951
  const tail = rest.slice(1);
6503
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6952
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6504
6953
  const sep4 = tail.indexOf("--");
6505
6954
  if (sep4 === -1 && tail.length > 0) {
6506
6955
  return {
@@ -6526,7 +6975,7 @@ function parseSelfUpgradeArgs(rest, bin = AGRO_PRODUCT.bin) {
6526
6975
  args.dryRun = true;
6527
6976
  continue;
6528
6977
  }
6529
- if (isHelpFlag(arg)) {
6978
+ if (isHelpFlag2(arg)) {
6530
6979
  args.help = true;
6531
6980
  return { ok: true, args };
6532
6981
  }
@@ -6568,7 +7017,7 @@ function parseVendorArgs(rest, bin = AGRO_PRODUCT.bin) {
6568
7017
  args.force = true;
6569
7018
  continue;
6570
7019
  }
6571
- if (isHelpFlag(arg)) {
7020
+ if (isHelpFlag2(arg)) {
6572
7021
  args.help = true;
6573
7022
  return { ok: true, args };
6574
7023
  }
@@ -6585,121 +7034,9 @@ function parseVendorArgs(rest, bin = AGRO_PRODUCT.bin) {
6585
7034
  }
6586
7035
  return { ok: true, args };
6587
7036
  }
6588
- var SANDBOX_VALUE_FLAGS = {
6589
- "--name": "name",
6590
- "--checkout": "checkout",
6591
- "--repo": "checkout",
6592
- "--home-mount": "homeMount"
6593
- };
6594
- function parseSandboxArgs(rest, bin = AGRO_PRODUCT.bin) {
6595
- const args = {
6596
- help: false,
6597
- yes: false,
6598
- image: false,
6599
- noBuild: false,
6600
- printArgv: false,
6601
- json: false
6602
- };
6603
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6604
- return { ok: true, args: { ...args, help: true } };
6605
- }
6606
- const [head, ...tail] = rest;
6607
- if (head !== "install" && head !== "list") {
6608
- return {
6609
- ok: false,
6610
- error: `${bin} sandbox: unknown subcommand "${head}" \u2014 expected install or list`,
6611
- showHelp: true
6612
- };
6613
- }
6614
- args.subcommand = head;
6615
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6616
- const positionals = [];
6617
- const checkoutSpellings = /* @__PURE__ */ new Set();
6618
- let version;
6619
- for (let i = 0; i < tail.length; i++) {
6620
- const token = tail[i];
6621
- const valueFlag = SANDBOX_VALUE_FLAGS[token];
6622
- if (valueFlag !== void 0) {
6623
- const value = tail[i + 1];
6624
- if (value === void 0) {
6625
- return { ok: false, error: `${bin} sandbox ${head}: ${token} requires a value` };
6626
- }
6627
- if (valueFlag === "checkout") checkoutSpellings.add(token);
6628
- args[valueFlag] = value;
6629
- i++;
6630
- } else if (token === "--yes") {
6631
- args.yes = true;
6632
- } else if (token === "--no-build") {
6633
- args.noBuild = true;
6634
- } else if (token === "--print-argv") {
6635
- args.printArgv = true;
6636
- } else if (token === "--image") {
6637
- args.image = true;
6638
- } else if (token.startsWith("--image=")) {
6639
- const ref = token.slice("--image=".length);
6640
- if (ref === "") {
6641
- return { ok: false, error: `${bin} sandbox: --image=<ref> requires a non-empty image ref` };
6642
- }
6643
- args.image = true;
6644
- args.imageRef = ref;
6645
- } else if (token === "--version" || token.startsWith("--version=")) {
6646
- const value = token === "--version" ? tail[++i] : token.slice("--version=".length);
6647
- if (value === void 0 || value === "") {
6648
- return { ok: false, error: `${bin} sandbox ${head}: --version requires a value` };
6649
- }
6650
- version = parseReleaseVersion(value);
6651
- if (version === void 0) {
6652
- return {
6653
- ok: false,
6654
- error: `${bin} sandbox ${head}: --version "${value}" is not a release version \u2014 expected X.Y.Z`
6655
- };
6656
- }
6657
- } else if (token === "--json") {
6658
- args.json = true;
6659
- } else if (token.startsWith("-")) {
6660
- return { ok: false, error: `${bin} sandbox ${head}: unknown flag "${token}"` };
6661
- } else {
6662
- positionals.push(token);
6663
- }
6664
- }
6665
- if (checkoutSpellings.has("--checkout") && checkoutSpellings.has("--repo")) {
6666
- return {
6667
- ok: false,
6668
- error: `${bin} sandbox ${head}: --checkout conflicts with --repo \u2014 pass exactly one, and prefer --checkout`
6669
- };
6670
- }
6671
- if (version !== void 0) {
6672
- if (args.imageRef !== void 0) {
6673
- return {
6674
- ok: false,
6675
- error: `${bin} sandbox ${head}: --version conflicts with --image=<ref> \u2014 pass --version for an official release or --image=<ref> for a custom image`
6676
- };
6677
- }
6678
- args.image = true;
6679
- args.imageRef = officialImageRef(version);
6680
- }
6681
- if (head === "list") {
6682
- if (positionals.length > 0) {
6683
- return { ok: false, error: `${bin} sandbox list: unexpected argument "${positionals[0]}"` };
6684
- }
6685
- return { ok: true, args };
6686
- }
6687
- if (positionals.length === 0) {
6688
- return {
6689
- ok: false,
6690
- error: `${bin} sandbox install: a runtime is required, e.g. \`${bin} sandbox install docker\``,
6691
- showHelp: true
6692
- };
6693
- }
6694
- if (positionals.length > 1) {
6695
- return { ok: false, error: `${bin} sandbox install: unexpected argument "${positionals[1]}"` };
6696
- }
6697
- args.runtime = positionals[0];
6698
- return { ok: true, args };
6699
- }
6700
7037
  function parseShellArgs(rest, bin = AGRO_PRODUCT.bin) {
6701
7038
  const args = { help: false };
6702
- if (isHelpFlag(rest[0])) return { ok: true, args: { help: true } };
7039
+ if (isHelpFlag2(rest[0])) return { ok: true, args: { help: true } };
6703
7040
  for (const token of rest) {
6704
7041
  if (token.startsWith("-")) {
6705
7042
  return { ok: false, error: `${bin} shell: unknown flag "${token}"` };
@@ -6713,7 +7050,7 @@ function parseShellArgs(rest, bin = AGRO_PRODUCT.bin) {
6713
7050
  }
6714
7051
  function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6715
7052
  const args = { help: false, json: false };
6716
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7053
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6717
7054
  return { ok: true, args: { ...args, help: true } };
6718
7055
  }
6719
7056
  const positionals = [];
@@ -6785,7 +7122,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
6785
7122
  host: false,
6786
7123
  force: false
6787
7124
  };
6788
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7125
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6789
7126
  return { ok: true, args: { ...args, help: true } };
6790
7127
  }
6791
7128
  const positionals = [];
@@ -6866,7 +7203,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
6866
7203
  }
6867
7204
  function parseToolArgs(rest, bin = AGRO_PRODUCT.bin) {
6868
7205
  const args = { help: false, yes: false, json: false, host: false, force: false };
6869
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7206
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6870
7207
  return { ok: true, args: { ...args, help: true } };
6871
7208
  }
6872
7209
  const positionals = [];
@@ -6924,11 +7261,11 @@ function parseGatewayArgs(rest) {
6924
7261
  }
6925
7262
  return { ok: true, args: { help: false, passthrough: [...rest] } };
6926
7263
  }
6927
- function bundledPayloadExists(bundled, exists = existsSync21) {
6928
- return exists(join17(bundled.sourceControlDir, "manifest.json"));
7264
+ function bundledPayloadExists(bundled, exists = existsSync23) {
7265
+ return exists(join19(bundled.sourceControlDir, "manifest.json"));
6929
7266
  }
6930
7267
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6931
- const exists = bundled.exists ?? existsSync21;
7268
+ const exists = bundled.exists ?? existsSync23;
6932
7269
  if (args.fromDir !== void 0) {
6933
7270
  return { kind: "local", fromDir: resolve11(args.fromDir) };
6934
7271
  }
@@ -6948,7 +7285,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6948
7285
  function readPayloadVersion(checkoutDir) {
6949
7286
  try {
6950
7287
  const parsed = JSON.parse(
6951
- readFileSync12(join17(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7288
+ readFileSync13(join19(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
6952
7289
  );
6953
7290
  if (parsed && typeof parsed.version === "string") return parsed.version;
6954
7291
  } catch {
@@ -6957,7 +7294,7 @@ function readPayloadVersion(checkoutDir) {
6957
7294
  }
6958
7295
  async function runWithRemoteSource(hooks, fn) {
6959
7296
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
6960
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join17(tmpdir4(), "oh-remote-")));
7297
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join19(tmpdir4(), "oh-remote-")));
6961
7298
  const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
6962
7299
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
6963
7300
  const checkoutDir = mkdtemp();
@@ -6974,7 +7311,7 @@ async function main(argv) {
6974
7311
  const product = resolveProduct(process.argv[1]);
6975
7312
  const bin = product.bin;
6976
7313
  const [first, second] = argv;
6977
- if (!first || isHelpFlag(first)) {
7314
+ if (!first || isHelpFlag2(first)) {
6978
7315
  printAgroHelp(product);
6979
7316
  return 0;
6980
7317
  }
@@ -7117,37 +7454,7 @@ async function main(argv) {
7117
7454
  (checkoutDir) => runUpdate({ bin, targetDir, fromDir: checkoutDir, force, dryRun }, io)
7118
7455
  );
7119
7456
  }
7120
- if (first === "sandbox") {
7121
- const parsed = parseSandboxArgs(argv.slice(1), bin);
7122
- if (!parsed.ok) {
7123
- process.stderr.write(`${parsed.error}
7124
- `);
7125
- if (parsed.showHelp) printSandboxHelp(bin);
7126
- return 1;
7127
- }
7128
- const a = parsed.args;
7129
- if (a.help) {
7130
- printSandboxHelp(bin);
7131
- return a.subcommand === void 0 ? 1 : 0;
7132
- }
7133
- const io = lifecycleIo();
7134
- if (a.subcommand === "list") return await runSandboxList({ bin, json: a.json }, io);
7135
- return await runSandboxInstall(
7136
- {
7137
- bin,
7138
- runtime: a.runtime,
7139
- ...a.name !== void 0 ? { name: a.name } : {},
7140
- ...a.checkout !== void 0 ? { checkout: a.checkout } : {},
7141
- ...a.homeMount !== void 0 ? { homeMount: a.homeMount } : {},
7142
- yes: a.yes,
7143
- image: a.image,
7144
- ...a.imageRef !== void 0 ? { imageRef: a.imageRef } : {},
7145
- noBuild: a.noBuild,
7146
- printArgv: a.printArgv
7147
- },
7148
- io
7149
- );
7150
- }
7457
+ if (first === "sandbox") return runSandboxCommand(argv.slice(1), bin);
7151
7458
  if (first === "shell") {
7152
7459
  const parsed = parseShellArgs(argv.slice(1), bin);
7153
7460
  if (!parsed.ok) {
@@ -7232,7 +7539,7 @@ async function main(argv) {
7232
7539
  if (composeVerbs().includes(first)) {
7233
7540
  const verb = first;
7234
7541
  const rest = argv.slice(1);
7235
- if (isHelpFlag(rest[0])) {
7542
+ if (isHelpFlag2(rest[0])) {
7236
7543
  printComposeVerbHelp(verb, bin);
7237
7544
  return 0;
7238
7545
  }
@@ -7362,7 +7669,7 @@ export {
7362
7669
  SECRET_VERBS,
7363
7670
  bundledPayloadExists,
7364
7671
  extractSandboxFlag,
7365
- isHelpFlag,
7672
+ isHelpFlag2 as isHelpFlag,
7366
7673
  isVersionFlag,
7367
7674
  parseComposeArgs,
7368
7675
  parseConfigArgs,