@mifune/agro 0.15.1 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +18 -6
  2. package/dist/agro.js +792 -457
  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
@@ -611,6 +611,15 @@ ${detail}`);
611
611
  function copyFile(deps, from, to) {
612
612
  deps.writeFile(to, deps.readFile(from), deps.stat(from).mode & 511);
613
613
  }
614
+ var ABSOLUTE_NODE_SHEBANG = /^#!\/\S*\/node$/;
615
+ function withPinnedShebang(current, artifact) {
616
+ const currentEnd = current.indexOf(10);
617
+ const artifactEnd = artifact.indexOf(10);
618
+ if (currentEnd < 0 || artifactEnd < 0) return artifact;
619
+ const firstLine = current.subarray(0, currentEnd);
620
+ if (!ABSOLUTE_NODE_SHEBANG.test(firstLine.toString("utf8"))) return artifact;
621
+ return Buffer.concat([firstLine, artifact.subarray(artifactEnd)]);
622
+ }
614
623
  function removeQuietly(deps, path4, io) {
615
624
  try {
616
625
  deps.unlink(path4);
@@ -645,11 +654,11 @@ async function upgradeStandalone(installation, opts, deps, io) {
645
654
  let prevPresent = false;
646
655
  let replaced = false;
647
656
  try {
648
- deps.writeFile(tmp, artifact, 493);
649
- tmpPresent = true;
650
657
  if (artifact.subarray(0, 2).toString("utf8") !== "#!") {
651
658
  throw new Error(`invalid artifact from ${url}: no #! shebang`);
652
659
  }
660
+ deps.writeFile(tmp, withPinnedShebang(deps.readFile(target), artifact), 493);
661
+ tmpPresent = true;
653
662
  const probe = deps.runNode(tmp, ["--version"]);
654
663
  const available = versionOf(probe);
655
664
  if (parseSemver(available) === void 0) {
@@ -1403,10 +1412,10 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as read
1403
1412
  import { dirname as dirname4, join as join4, resolve as resolve5, sep as sep2 } from "node:path";
1404
1413
 
1405
1414
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.yml
1406
- var docker_compose_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-agro}}\n pull_policy: ${AGRO_PULL_POLICY:-missing}\n container_name: ${SANDBOX_NAME:-agro}\n build:\n context: ${AGRO_REPO_DIR:-..}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n - ${AGRO_REPO_DIR:-..}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
1415
+ var docker_compose_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-sandbox-${SANDBOX_NAME:-agro}}\n pull_policy: ${AGRO_PULL_POLICY:-missing}\n container_name: ${SANDBOX_NAME:-agro}\n build:\n context: ${AGRO_REPO_DIR:-..}\n dockerfile: .devcontainer/Dockerfile\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n - ${AGRO_REPO_DIR:-..}:/home/sandbox/harness\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n - TYPESAFE_API_KEY=${TYPESAFE_API_KEY:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
1407
1416
 
1408
1417
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.image-only.yml
1409
- var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-ghcr.io/mifunedev/agro:latest}\n pull_policy: ${AGRO_PULL_POLICY:-always}\n container_name: ${SANDBOX_NAME:-agro}\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
1418
+ var docker_compose_image_only_default = 'name: ${SANDBOX_NAME:-agro}\n\nservices:\n sandbox:\n image: ${AGRO_SANDBOX_IMAGE:-ghcr.io/mifunedev/agro:latest}\n pull_policy: ${AGRO_PULL_POLICY:-always}\n container_name: ${SANDBOX_NAME:-agro}\n volumes:\n - ${AGRO_HOME_MOUNT:-workspace}:/home/sandbox\n extra_hosts:\n - "host.docker.internal:host-gateway"\n environment:\n - SANDBOX_NAME=${SANDBOX_NAME:-agro}\n - SANDBOX_PASSWORD=${SANDBOX_PASSWORD:-test1234}\n - TZ=${TZ:-America/Los_Angeles}\n - CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS=true\n - CC_SAFETY_NET_STRICT=1\n - CC_SAFETY_NET_WORKTREE=1\n - GIT_USER_NAME=${GIT_USER_NAME:-}\n - GIT_USER_EMAIL=${GIT_USER_EMAIL:-}\n - GH_TOKEN=${GH_TOKEN:-}\n - TYPESAFE_API_KEY=${TYPESAFE_API_KEY:-}\n stdin_open: true\n tty: true\n cgroup: private\n cap_add:\n - SYS_ADMIN\n security_opt:\n - apparmor=unconfined\n tmpfs:\n - /run\n - /run/lock\n - /sys/fs\n healthcheck:\n test: ["CMD", "bash", "-c", "exec bash /home/sandbox/harness/.agro/scripts/sandbox-healthcheck.sh"]\n interval: 30s\n timeout: 10s\n retries: 3\n start_period: 600s\n restart: unless-stopped\n\nvolumes:\n workspace:\n';
1410
1419
 
1411
1420
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.ssh.yml
1412
1421
  var docker_compose_ssh_default = 'services:\n sandbox:\n ports:\n - "127.0.0.1:${SANDBOX_SSH_PORT:-2222}:22"\n';
@@ -1484,8 +1493,7 @@ if [ -f "$REPO_DIR/harness.yaml" ] && [ -f "$MIGRATOR" ]; then
1484
1493
  sh "$MIGRATOR" "$REPO_DIR" >&2 || true
1485
1494
  fi
1486
1495
 
1487
- ENV_FILE="$REPO_DIR/.env"
1488
- [ -f "$ENV_FILE" ] || ENV_FILE="$REPO_DIR/.devcontainer/.env"
1496
+ ENV_FILE="$(agro_env_file "$REPO_DIR")"
1489
1497
 
1490
1498
  compose_path() {
1491
1499
  case "$1" in
@@ -1632,6 +1640,14 @@ agro_config_file() {
1632
1640
  printf '%s\\n' "$1/$AGRO_CONFIG_FILE"
1633
1641
  }
1634
1642
 
1643
+ agro_env_file() {
1644
+ if [ -f "$1/.env" ]; then
1645
+ printf '%s\\n' "$1/.env"
1646
+ else
1647
+ printf '%s\\n' "$1/.devcontainer/.env"
1648
+ fi
1649
+ }
1650
+
1635
1651
  agro_env_value() {
1636
1652
  local key="AGRO_$1"
1637
1653
  printf '%s\\n' "\${!key:-}"
@@ -2222,7 +2238,7 @@ function resolveExecutionTarget(opts) {
2222
2238
  }
2223
2239
 
2224
2240
  // src/lib/version.ts
2225
- var AGRO_VERSION = true ? "0.15.1" : "0.0.0-dev";
2241
+ var AGRO_VERSION = true ? "0.16.1" : "0.0.0-dev";
2226
2242
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2227
2243
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2228
2244
  function parseReleaseVersion(value) {
@@ -2852,6 +2868,7 @@ function sameRemote(url, owner, name) {
2852
2868
  }
2853
2869
 
2854
2870
  // src/commands/secret.ts
2871
+ import { spawnSync as spawnSync3 } from "node:child_process";
2855
2872
  import { existsSync as existsSync12 } from "node:fs";
2856
2873
  import { join as join7 } from "node:path";
2857
2874
 
@@ -2883,8 +2900,16 @@ function appendGitignoreLines(root, lines) {
2883
2900
  function secretsRoot(opts) {
2884
2901
  return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
2885
2902
  }
2903
+ function isGitIgnored(root, path4) {
2904
+ const result = spawnSync3("git", ["check-ignore", "-q", "--no-index", path4], {
2905
+ cwd: root,
2906
+ stdio: "ignore"
2907
+ });
2908
+ return result.status === 0;
2909
+ }
2886
2910
  function ignoreSecretsFile(root) {
2887
2911
  if (!existsSync12(join7(root, ".git"))) return;
2912
+ if (isGitIgnored(root, ".env")) return;
2888
2913
  appendGitignoreLines(root, [".env"]);
2889
2914
  }
2890
2915
  function secretKeyList() {
@@ -3129,7 +3154,7 @@ var HARNESS_CATALOG = [
3129
3154
  installArgv: [
3130
3155
  "bash",
3131
3156
  "-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]'`
3157
+ `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
3158
  ],
3134
3159
  installUser: "sandbox",
3135
3160
  verifyArgv: ["hermes", "--version"],
@@ -3744,7 +3769,7 @@ var RUNTIME_CATALOG = Object.freeze([
3744
3769
  tier: "container",
3745
3770
  state: "active",
3746
3771
  provisionable: true,
3747
- docsPath: "docs/runtimes/docker.md"
3772
+ docsPath: "docs/runtimes/overview.md"
3748
3773
  }),
3749
3774
  Object.freeze({
3750
3775
  id: "microsandbox",
@@ -3752,7 +3777,7 @@ var RUNTIME_CATALOG = Object.freeze([
3752
3777
  tier: "microvm",
3753
3778
  state: "planned",
3754
3779
  provisionable: false,
3755
- notProvisionableReason: (bin) => `microsandbox is not a provisionable runtime yet; see docs/rfcs/rfc-runtime-support.md. Inside a sandbox run \`${bin} tool install microsandbox\`.`,
3780
+ notProvisionableReason: (bin) => `microsandbox is not a provisionable runtime yet; see https://github.com/mifunedev/agro/issues/592. Inside a sandbox run \`${bin} tool install microsandbox\`.`,
3756
3781
  docsPath: "docs/runtimes/microsandbox.md"
3757
3782
  })
3758
3783
  ]);
@@ -3784,6 +3809,55 @@ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTT
3784
3809
  }
3785
3810
  }
3786
3811
 
3812
+ // src/commands/sandbox-list.ts
3813
+ async function entryStatus(root, name, run) {
3814
+ try {
3815
+ const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
3816
+ return await target.status();
3817
+ } catch {
3818
+ return "unknown";
3819
+ }
3820
+ }
3821
+ async function runSandboxList(opts, io) {
3822
+ const run = opts.run ?? spawnRunner;
3823
+ const rows = [];
3824
+ for (const name of listEntries()) {
3825
+ const root = entryRoot(name);
3826
+ const config = readAgroConfig(agroConfigPath(root));
3827
+ const checkout = configCheckout(config) ?? "-";
3828
+ rows.push({
3829
+ name,
3830
+ runtime: config.runtime ?? "docker",
3831
+ checkout,
3832
+ repo: checkout,
3833
+ status: await entryStatus(root, name, run)
3834
+ });
3835
+ }
3836
+ if (opts.json === true) {
3837
+ io.stdout(`${JSON.stringify(rows, null, 2)}
3838
+ `);
3839
+ return 0;
3840
+ }
3841
+ if (rows.length === 0) {
3842
+ io.stdout(
3843
+ `no sandbox is registered in ${registryRoot()} \u2014 create one with \`${opts.bin} sandbox install docker\`
3844
+ `
3845
+ );
3846
+ return 0;
3847
+ }
3848
+ const width = (pick) => Math.max(...rows.map((row) => pick(row).length));
3849
+ const nameWidth = width((row) => row.name);
3850
+ const runtimeWidth = width((row) => row.runtime);
3851
+ const statusWidth = width((row) => row.status);
3852
+ for (const row of rows) {
3853
+ io.stdout(
3854
+ `${row.name.padEnd(nameWidth)} ${row.runtime.padEnd(runtimeWidth)} ${row.status.padEnd(statusWidth)} ${row.checkout}
3855
+ `
3856
+ );
3857
+ }
3858
+ return 0;
3859
+ }
3860
+
3787
3861
  // src/commands/sandbox.ts
3788
3862
  function hostTimezone() {
3789
3863
  const tz = process.env.TZ;
@@ -4001,191 +4075,527 @@ async function runSandboxInstall(opts, io) {
4001
4075
  }
4002
4076
  return code;
4003
4077
  }
4004
- async function entryStatus(root, name, run) {
4005
- try {
4006
- const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
4007
- return await target.status();
4008
- } catch {
4009
- return "unknown";
4010
- }
4078
+
4079
+ // src/services/sandbox-upgrade.ts
4080
+ import { closeSync as closeSync2, existsSync as existsSync17, openSync as openSync2, readFileSync as readFileSync11, unlinkSync as unlinkSync5, writeSync } from "node:fs";
4081
+ import { join as join13 } from "node:path";
4082
+ function errorMessage(error) {
4083
+ return error instanceof Error ? error.message : String(error);
4011
4084
  }
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
- });
4085
+ function conflictingDotenvImage(root, imageRef) {
4086
+ const rootEnv = join13(root, ".env");
4087
+ const fallbackEnv = join13(root, ".devcontainer", ".env");
4088
+ const file = existsSync17(rootEnv) ? rootEnv : fallbackEnv;
4089
+ if (!existsSync17(file)) return void 0;
4090
+ for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
4091
+ const match = /^AGRO_SANDBOX_IMAGE=(.*)$/.exec(line);
4092
+ if (!match) continue;
4093
+ const value = match[1].trim().replace(/^(?:"(.*)"|'(.*)')$/, (_whole, double, single) => double ?? single ?? "");
4094
+ if (value !== imageRef) return file;
4026
4095
  }
4027
- if (opts.json === true) {
4028
- io.stdout(`${JSON.stringify(rows, null, 2)}
4096
+ return void 0;
4097
+ }
4098
+ async function runSandboxUpgrade(opts, io) {
4099
+ const prefix = `${opts.bin} sandbox upgrade:`;
4100
+ const version = parseReleaseVersion(opts.version);
4101
+ if (version === void 0) {
4102
+ io.stderr(`${prefix} invalid release version "${opts.version}"
4029
4103
  `);
4030
- return 0;
4031
- }
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;
4038
- }
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
- );
4104
+ return 1;
4048
4105
  }
4049
- return 0;
4050
- }
4051
-
4052
- // src/commands/harness.ts
4053
- import { existsSync as existsSync19 } from "node:fs";
4054
- import { homedir as homedir3 } from "node:os";
4055
- import { delimiter as delimiter2, join as join15 } from "node:path";
4056
-
4057
- // src/lib/docs.ts
4058
- var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
4059
- function sourceDocsUrl(docsPath) {
4060
- const segments = docsPath.split("/");
4061
- if (segments[0] !== "docs" || segments.length < 2 || segments.some((segment) => segment === "" || segment === "." || segment === "..") || docsPath.includes("\\") || docsPath.includes("?") || docsPath.includes("#")) {
4062
- throw new Error(`source docs path must be a normalized docs/ path: ${docsPath}`);
4106
+ if (runningInsideSandbox()) {
4107
+ io.stderr(`${prefix} host-only \u2014 run this command on the host
4108
+ `);
4109
+ return 1;
4063
4110
  }
4064
- return new URL(segments.map(encodeURIComponent).join("/"), SOURCE_DOCS_BASE).toString();
4065
- }
4066
-
4067
- // 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";
4070
- var HOST_CONFIG_MODE = 420;
4071
- var HOST_CONFIG_FILE = "config.json";
4072
- var WORKSPACES_SUBDIR = "workspaces";
4073
- var DEFAULT_WORKSPACE_NAME = "harness";
4074
- function hostStateHome(env, home) {
4075
- return resolveUserStateHome(env, home);
4076
- }
4077
- function hostConfigPath(env, home) {
4078
- return join13(hostStateHome(env, home), HOST_CONFIG_FILE);
4079
- }
4080
- function assertWorkspaceName(name) {
4081
- if (!SANDBOX_NAME_PATTERN.test(name)) {
4082
- throw new Error(
4083
- `invalid workspace name "${name}" \u2014 use lowercase letters, digits and dashes, starting with a letter or digit`
4084
- );
4111
+ let root;
4112
+ try {
4113
+ root = entryRoot(opts.name);
4114
+ } catch (error) {
4115
+ io.stderr(`${prefix} ${errorMessage(error)}
4116
+ `);
4117
+ return 1;
4085
4118
  }
4086
- }
4087
- function workspacesRoot(env, home) {
4088
- return join13(hostStateHome(env, home), WORKSPACES_SUBDIR);
4089
- }
4090
- function workspaceRoot(name, env, home) {
4091
- assertWorkspaceName(name);
4092
- return join13(workspacesRoot(env, home), name);
4093
- }
4094
- function defaultHarnessRoot(env, home) {
4095
- return join13(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4096
- }
4097
- function validateHostConfig(value) {
4098
- if (!value || typeof value !== "object" || Array.isArray(value)) {
4099
- throw new Error(`${HOST_CONFIG_FILE}: must contain a JSON object`);
4119
+ const configFile = agroConfigPath(root);
4120
+ if (!existsSync17(configFile)) {
4121
+ io.stderr(`${prefix} no sandbox entry named "${opts.name}"
4122
+ `);
4123
+ return 1;
4100
4124
  }
4101
- const record = value;
4102
- if (record.version !== void 0 && record.version !== 1) {
4103
- throw fieldError2("version", "must be 1");
4125
+ const lock = join13(root, ".sandbox-upgrade.lock");
4126
+ let fd;
4127
+ try {
4128
+ fd = openSync2(lock, "wx", 384);
4129
+ } catch (error) {
4130
+ 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);
4131
+ io.stderr(`${prefix} ${message}
4132
+ `);
4133
+ return 1;
4104
4134
  }
4105
- const harnessRoot = record.harnessRoot;
4106
- if (harnessRoot !== void 0) {
4107
- if (typeof harnessRoot !== "string" || harnessRoot === "") {
4108
- throw fieldError2("harnessRoot", "must be a non-empty string");
4135
+ try {
4136
+ writeSync(fd, `${process.pid}
4137
+ `);
4138
+ const config = readAgroConfig(configFile);
4139
+ if (config.image?.mode !== "image") {
4140
+ io.stderr(`${prefix} ${opts.name} requires image.mode "image"
4141
+ `);
4142
+ return 1;
4109
4143
  }
4110
- if (!isAbsolute(harnessRoot)) {
4111
- throw fieldError2("harnessRoot", "must be an absolute path");
4144
+ const imageRef = officialImageRef(version);
4145
+ if (process.env.AGRO_SANDBOX_IMAGE !== void 0 && process.env.AGRO_SANDBOX_IMAGE !== "" && process.env.AGRO_SANDBOX_IMAGE !== imageRef) {
4146
+ io.stderr(`${prefix} conflicting AGRO_SANDBOX_IMAGE in the host environment
4147
+ `);
4148
+ return 1;
4112
4149
  }
4113
- }
4114
- const hostHarnesses = record.hostHarnesses;
4115
- if (hostHarnesses !== void 0) validateReceipts(hostHarnesses, "hostHarnesses", "harness");
4116
- const hostTools = record.hostTools;
4117
- if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool");
4118
- return { ...record, version: 1 };
4119
- }
4120
- function absoluteField(value, path4) {
4121
- if (typeof value !== "string" || value === "") {
4122
- throw fieldError2(path4, "must be a non-empty string");
4123
- }
4124
- if (!isAbsolute(value)) throw fieldError2(path4, "must be an absolute path");
4125
- return value;
4126
- }
4127
- function validateReceipts(value, field, noun) {
4128
- if (!value || typeof value !== "object" || Array.isArray(value)) {
4129
- throw fieldError2(field, `must be a JSON object keyed by ${noun} id`);
4130
- }
4131
- for (const [id, receipt] of Object.entries(value)) {
4132
- if (id === "") throw fieldError2(field, `must use a non-empty ${noun} id as each key`);
4133
- const at = `${field}.${id}`;
4134
- if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
4135
- throw fieldError2(at, "must be a JSON object");
4150
+ const dotenv = conflictingDotenvImage(root, imageRef);
4151
+ if (dotenv !== void 0) {
4152
+ io.stderr(`${prefix} conflicting AGRO_SANDBOX_IMAGE in ${dotenv}
4153
+ `);
4154
+ return 1;
4136
4155
  }
4137
- const entry = receipt;
4138
- absoluteField(entry.prefix, `${at}.prefix`);
4139
- absoluteField(entry.binPath, `${at}.binPath`);
4140
- if (typeof entry.binary !== "string" || entry.binary === "") {
4141
- throw fieldError2(`${at}.binary`, "must be a non-empty string");
4156
+ const run = opts.run ?? spawnRunner;
4157
+ let priorRef = config.image.ref;
4158
+ if (!priorRef) {
4159
+ const name = configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
4160
+ const inspected = run("docker", ["inspect", "-f", "{{.Config.Image}}", name], { stdio: "capture" });
4161
+ priorRef = inspected.status === 0 && !inspected.error ? inspected.stdout?.trim() : void 0;
4162
+ if (!priorRef) {
4163
+ io.stderr(`${prefix} cannot determine previous image for ${name}; no changes made
4164
+ `);
4165
+ return 1;
4166
+ }
4142
4167
  }
4143
- if (typeof entry.installedAt !== "string" || entry.installedAt === "") {
4144
- throw fieldError2(`${at}.installedAt`, "must be a non-empty string");
4168
+ const apply = (ref) => runSandbox({ bin: opts.bin, cwd: root, imageRef: ref, run }, io);
4169
+ let failure;
4170
+ try {
4171
+ const code = await apply(imageRef);
4172
+ if (code !== 0) failure = `exit ${code}`;
4173
+ } catch (error) {
4174
+ failure = errorMessage(error);
4145
4175
  }
4146
- if (entry.workspaceRoot !== void 0) {
4147
- absoluteField(entry.workspaceRoot, `${at}.workspaceRoot`);
4176
+ if (failure === void 0) {
4177
+ try {
4178
+ (opts.writeConfig ?? writeAgroConfig)(root, { ...config, image: { ...config.image, ref: imageRef } });
4179
+ return 0;
4180
+ } catch (error) {
4181
+ failure = `config persistence failed (${errorMessage(error)})`;
4182
+ }
4183
+ } else {
4184
+ failure = `provisioning failed (${failure})`;
4148
4185
  }
4149
- }
4150
- }
4151
- function readHostConfig(env, home) {
4152
- const path4 = hostConfigPath(env, home);
4153
- if (!existsSync17(path4)) return { version: 1 };
4154
- let raw;
4155
- try {
4156
- raw = readFileSync11(path4, "utf8");
4157
- } catch (error) {
4158
- const detail = error instanceof Error ? error.message : String(error);
4159
- throw new Error(`could not read ${path4}: ${detail}`);
4160
- }
4161
- let parsed;
4162
- try {
4163
- parsed = JSON.parse(raw);
4164
- } catch {
4165
- throw new Error(`${basename5(path4)} is not valid JSON: ${path4}`);
4166
- }
4167
- return validateHostConfig(parsed);
4168
- }
4169
- function writeHostConfig(config, env, home) {
4170
- const path4 = hostConfigPath(env, home);
4171
- const validated = validateHostConfig({ ...config, version: 1 });
4172
- const body = `${JSON.stringify(validated, null, 2)}
4173
- `;
4174
- const tmp = `${path4}.tmp.${process.pid}`;
4175
- mkdirSync9(dirname8(path4), { recursive: true });
4176
- try {
4177
- writeFileSync9(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4178
- renameSync4(tmp, path4);
4179
- } catch (error) {
4186
+ io.stderr(`${prefix} ${failure}; restoring previous image
4187
+ `);
4180
4188
  try {
4181
- unlinkSync5(tmp);
4182
- } catch {
4189
+ const code = await apply(priorRef);
4190
+ if (code !== 0) io.stderr(`${prefix} restoration failed (exit ${code})
4191
+ `);
4192
+ } catch (error) {
4193
+ io.stderr(`${prefix} restoration failed (${errorMessage(error)})
4194
+ `);
4183
4195
  }
4184
- throw error;
4196
+ return 1;
4197
+ } catch (error) {
4198
+ io.stderr(`${prefix} ${errorMessage(error)}
4199
+ `);
4200
+ return 1;
4201
+ } finally {
4202
+ closeSync2(fd);
4203
+ unlinkSync5(lock);
4185
4204
  }
4186
4205
  }
4187
- function resolveHarnessRoot(explicit, env, home) {
4188
- if (typeof explicit === "string" && explicit !== "") return resolve8(explicit);
4206
+
4207
+ // src/controllers/sandbox.ts
4208
+ function isHelpFlag(arg) {
4209
+ return arg === "--help" || arg === "-h" || arg === "help";
4210
+ }
4211
+ function runtimeLines() {
4212
+ const width = Math.max(...RUNTIME_CATALOG.map((r) => r.id.length));
4213
+ return RUNTIME_CATALOG.map(
4214
+ (r) => ` ${r.id.padEnd(width)} ${r.provisionable ? "provisionable" : "planned"}`
4215
+ ).join("\n");
4216
+ }
4217
+ function printSandboxHelp(bin = AGRO_PRODUCT.bin) {
4218
+ process.stdout.write(`${bin} sandbox \u2014 Create and list sandboxes
4219
+
4220
+ Usage:
4221
+ ${bin} sandbox install <runtime> [--name <name>] [--checkout <dir>]
4222
+ [--home-mount <dir>] [--yes]
4223
+ [--version <X.Y.Z> | --image[=<ref>]]
4224
+ [--no-build] [--print-argv]
4225
+ ${bin} sandbox list [--json]
4226
+ ${bin} sandbox upgrade <name> --version <X.Y.Z>
4227
+
4228
+ \`install\` writes a sandbox entry under \${${stateNames(bin).envPrefix}HOME:-~/${stateNames(bin).userStateDir}}/sandboxes/<name>/,
4229
+ materialises the compose files and the compose wrapper into it, then starts the
4230
+ container. It needs no project checkout and runs from any directory. Without
4231
+ --checkout the sandbox runs the prebuilt image; with --checkout <dir> that
4232
+ checkout is bound at /home/sandbox/harness; the sandbox builds locally only
4233
+ when that directory holds .devcontainer/Dockerfile.
4234
+
4235
+ On a terminal without --yes it asks for the sandbox name, the timezone, the git
4236
+ identity, SSH (with its host port), the host Docker socket, and the host path
4237
+ for /home/sandbox. With --checkout it seeds those answers from that
4238
+ checkout's ${stateNames(bin).configFile}.
4239
+
4240
+ Flags:
4241
+ --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
4242
+ --checkout <dir> Bind this checkout into the sandbox and seed the defaults
4243
+ from its ${stateNames(bin).configFile}. The sandbox builds
4244
+ locally only when that directory holds
4245
+ .devcontainer/Dockerfile
4246
+ --repo <dir> Deprecated alias for --checkout
4247
+ --home-mount <dir>
4248
+ Bind this host directory at /home/sandbox instead of the
4249
+ Docker-managed volume. Relative paths resolve against the
4250
+ working directory. The directory is created when absent
4251
+ --yes Non-interactive: keep every default and ask nothing
4252
+ --version <X.Y.Z>
4253
+ Run the official release image ghcr.io/mifunedev/agro:<X.Y.Z>
4254
+ (implies --image). A leading v and a -<channel>.<n>
4255
+ pre-release suffix are accepted
4256
+ --image Run the prebuilt image instead of building (implies
4257
+ --no-build). The ref resolves first-match: --version or
4258
+ --image=<ref> > ${stateNames(bin).envPrefix}SANDBOX_IMAGE >
4259
+ ${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
4260
+ --image=<ref> Run a custom image (implies --image); conflicts with --version
4261
+ --no-build Suppress the local build and reuse an existing image
4262
+ --print-argv Print the docker compose argv that would run, then exit
4263
+ without writing an entry
4264
+ --json Machine-readable output (list)
4265
+
4266
+ Runtimes:
4267
+ ${runtimeLines()}
4268
+
4269
+ Next: ${bin} shell <name>
4270
+ `);
4271
+ }
4272
+ var SANDBOX_VALUE_FLAGS = {
4273
+ "--name": "name",
4274
+ "--checkout": "checkout",
4275
+ "--repo": "checkout",
4276
+ "--home-mount": "homeMount"
4277
+ };
4278
+ function parseSandboxArgs(rest, bin = AGRO_PRODUCT.bin) {
4279
+ const args = {
4280
+ help: false,
4281
+ yes: false,
4282
+ image: false,
4283
+ noBuild: false,
4284
+ printArgv: false,
4285
+ json: false
4286
+ };
4287
+ if (rest.length === 0 || isHelpFlag(rest[0])) {
4288
+ return { ok: true, args: { ...args, help: true } };
4289
+ }
4290
+ const [head, ...tail] = rest;
4291
+ if (head === "upgrade") {
4292
+ if (tail.length === 1 && isHelpFlag(tail[0])) {
4293
+ return { ok: true, args: { ...args, subcommand: "upgrade", help: true } };
4294
+ }
4295
+ const [name, flag, value, ...extra] = tail;
4296
+ if (name === void 0 || name === "" || name.startsWith("-")) {
4297
+ return { ok: false, error: `${bin} sandbox upgrade: a name is required` };
4298
+ }
4299
+ if (flag === void 0) {
4300
+ return { ok: false, error: `${bin} sandbox upgrade: --version is required` };
4301
+ }
4302
+ if (flag !== "--version") {
4303
+ return { ok: false, error: `${bin} sandbox upgrade: ${flag.startsWith("-") ? "unknown flag" : "unexpected argument"} "${flag}"` };
4304
+ }
4305
+ if (value === void 0 || value === "") {
4306
+ return { ok: false, error: `${bin} sandbox upgrade: --version requires a value` };
4307
+ }
4308
+ if (value.startsWith("-")) {
4309
+ return { ok: false, error: `${bin} sandbox upgrade: unknown flag "${value}"` };
4310
+ }
4311
+ const version2 = parseReleaseVersion(value);
4312
+ if (version2 === void 0) {
4313
+ return { ok: false, error: `${bin} sandbox upgrade: --version "${value}" is not a release version \u2014 expected X.Y.Z` };
4314
+ }
4315
+ if (extra.length > 0) {
4316
+ const token = extra[0];
4317
+ return { ok: false, error: `${bin} sandbox upgrade: ${token.startsWith("-") && token !== "--version" ? "unknown flag" : "unexpected argument"} "${token}"` };
4318
+ }
4319
+ return { ok: true, args: { ...args, subcommand: "upgrade", name, version: version2 } };
4320
+ }
4321
+ if (head !== "install" && head !== "list") {
4322
+ return {
4323
+ ok: false,
4324
+ error: `${bin} sandbox: unknown subcommand "${head}" \u2014 expected install or list`,
4325
+ showHelp: true
4326
+ };
4327
+ }
4328
+ args.subcommand = head;
4329
+ if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
4330
+ const positionals = [];
4331
+ const checkoutSpellings = /* @__PURE__ */ new Set();
4332
+ let version;
4333
+ for (let i = 0; i < tail.length; i++) {
4334
+ const token = tail[i];
4335
+ const valueFlag = SANDBOX_VALUE_FLAGS[token];
4336
+ if (valueFlag !== void 0) {
4337
+ const value = tail[i + 1];
4338
+ if (value === void 0) {
4339
+ return { ok: false, error: `${bin} sandbox ${head}: ${token} requires a value` };
4340
+ }
4341
+ if (valueFlag === "checkout") checkoutSpellings.add(token);
4342
+ args[valueFlag] = value;
4343
+ i++;
4344
+ } else if (token === "--yes") {
4345
+ args.yes = true;
4346
+ } else if (token === "--no-build") {
4347
+ args.noBuild = true;
4348
+ } else if (token === "--print-argv") {
4349
+ args.printArgv = true;
4350
+ } else if (token === "--image") {
4351
+ args.image = true;
4352
+ } else if (token.startsWith("--image=")) {
4353
+ const ref = token.slice("--image=".length);
4354
+ if (ref === "") {
4355
+ return { ok: false, error: `${bin} sandbox: --image=<ref> requires a non-empty image ref` };
4356
+ }
4357
+ args.image = true;
4358
+ args.imageRef = ref;
4359
+ } else if (token === "--version" || token.startsWith("--version=")) {
4360
+ const value = token === "--version" ? tail[++i] : token.slice("--version=".length);
4361
+ if (value === void 0 || value === "") {
4362
+ return { ok: false, error: `${bin} sandbox ${head}: --version requires a value` };
4363
+ }
4364
+ version = parseReleaseVersion(value);
4365
+ if (version === void 0) {
4366
+ return {
4367
+ ok: false,
4368
+ error: `${bin} sandbox ${head}: --version "${value}" is not a release version \u2014 expected X.Y.Z`
4369
+ };
4370
+ }
4371
+ } else if (token === "--json") {
4372
+ args.json = true;
4373
+ } else if (token.startsWith("-")) {
4374
+ return { ok: false, error: `${bin} sandbox ${head}: unknown flag "${token}"` };
4375
+ } else {
4376
+ positionals.push(token);
4377
+ }
4378
+ }
4379
+ if (checkoutSpellings.has("--checkout") && checkoutSpellings.has("--repo")) {
4380
+ return {
4381
+ ok: false,
4382
+ error: `${bin} sandbox ${head}: --checkout conflicts with --repo \u2014 pass exactly one, and prefer --checkout`
4383
+ };
4384
+ }
4385
+ if (version !== void 0) {
4386
+ if (args.imageRef !== void 0) {
4387
+ return {
4388
+ ok: false,
4389
+ error: `${bin} sandbox ${head}: --version conflicts with --image=<ref> \u2014 pass --version for an official release or --image=<ref> for a custom image`
4390
+ };
4391
+ }
4392
+ args.image = true;
4393
+ args.imageRef = officialImageRef(version);
4394
+ }
4395
+ if (head === "list") {
4396
+ if (positionals.length > 0) {
4397
+ return { ok: false, error: `${bin} sandbox list: unexpected argument "${positionals[0]}"` };
4398
+ }
4399
+ return { ok: true, args };
4400
+ }
4401
+ if (positionals.length === 0) {
4402
+ return {
4403
+ ok: false,
4404
+ error: `${bin} sandbox install: a runtime is required, e.g. \`${bin} sandbox install docker\``,
4405
+ showHelp: true
4406
+ };
4407
+ }
4408
+ if (positionals.length > 1) {
4409
+ return { ok: false, error: `${bin} sandbox install: unexpected argument "${positionals[1]}"` };
4410
+ }
4411
+ args.runtime = positionals[0];
4412
+ return { ok: true, args };
4413
+ }
4414
+ async function runSandboxCommand(rest, bin) {
4415
+ const parsed = parseSandboxArgs(rest, bin);
4416
+ if (!parsed.ok) {
4417
+ process.stderr.write(`${parsed.error}
4418
+ `);
4419
+ if (parsed.showHelp) printSandboxHelp(bin);
4420
+ return 1;
4421
+ }
4422
+ const a = parsed.args;
4423
+ if (a.help) {
4424
+ printSandboxHelp(bin);
4425
+ return a.subcommand === void 0 ? 1 : 0;
4426
+ }
4427
+ const io = {
4428
+ stdout: (s) => process.stdout.write(s),
4429
+ stderr: (s) => process.stderr.write(s)
4430
+ };
4431
+ if (a.subcommand === "list") return await runSandboxList({ bin, json: a.json }, io);
4432
+ if (a.subcommand === "upgrade") {
4433
+ return await runSandboxUpgrade({ bin, name: a.name, version: a.version }, io);
4434
+ }
4435
+ return await runSandboxInstall(
4436
+ {
4437
+ bin,
4438
+ runtime: a.runtime,
4439
+ ...a.name !== void 0 ? { name: a.name } : {},
4440
+ ...a.checkout !== void 0 ? { checkout: a.checkout } : {},
4441
+ ...a.homeMount !== void 0 ? { homeMount: a.homeMount } : {},
4442
+ yes: a.yes,
4443
+ image: a.image,
4444
+ ...a.imageRef !== void 0 ? { imageRef: a.imageRef } : {},
4445
+ noBuild: a.noBuild,
4446
+ printArgv: a.printArgv
4447
+ },
4448
+ io
4449
+ );
4450
+ }
4451
+
4452
+ // src/commands/harness.ts
4453
+ import { existsSync as existsSync20 } from "node:fs";
4454
+ import { homedir as homedir3 } from "node:os";
4455
+ import { delimiter as delimiter2, join as join16 } from "node:path";
4456
+
4457
+ // src/lib/docs.ts
4458
+ var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
4459
+ function sourceDocsUrl(docsPath) {
4460
+ const segments = docsPath.split("/");
4461
+ if (segments[0] !== "docs" || segments.length < 2 || segments.some((segment) => segment === "" || segment === "." || segment === "..") || docsPath.includes("\\") || docsPath.includes("?") || docsPath.includes("#")) {
4462
+ throw new Error(`source docs path must be a normalized docs/ path: ${docsPath}`);
4463
+ }
4464
+ return new URL(segments.map(encodeURIComponent).join("/"), SOURCE_DOCS_BASE).toString();
4465
+ }
4466
+
4467
+ // src/lib/host-config.ts
4468
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
4469
+ import { basename as basename5, dirname as dirname8, isAbsolute, join as join14, resolve as resolve8 } from "node:path";
4470
+ var HOST_CONFIG_MODE = 420;
4471
+ var HOST_CONFIG_FILE = "config.json";
4472
+ var WORKSPACES_SUBDIR = "workspaces";
4473
+ var DEFAULT_WORKSPACE_NAME = "harness";
4474
+ function isRootToolReceipt(receipt) {
4475
+ return receipt.scope === "root";
4476
+ }
4477
+ function hostStateHome(env, home) {
4478
+ return resolveUserStateHome(env, home);
4479
+ }
4480
+ function hostConfigPath(env, home) {
4481
+ return join14(hostStateHome(env, home), HOST_CONFIG_FILE);
4482
+ }
4483
+ function assertWorkspaceName(name) {
4484
+ if (!SANDBOX_NAME_PATTERN.test(name)) {
4485
+ throw new Error(
4486
+ `invalid workspace name "${name}" \u2014 use lowercase letters, digits and dashes, starting with a letter or digit`
4487
+ );
4488
+ }
4489
+ }
4490
+ function workspacesRoot(env, home) {
4491
+ return join14(hostStateHome(env, home), WORKSPACES_SUBDIR);
4492
+ }
4493
+ function workspaceRoot(name, env, home) {
4494
+ assertWorkspaceName(name);
4495
+ return join14(workspacesRoot(env, home), name);
4496
+ }
4497
+ function defaultHarnessRoot(env, home) {
4498
+ return join14(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4499
+ }
4500
+ function validateHostConfig(value) {
4501
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4502
+ throw new Error(`${HOST_CONFIG_FILE}: must contain a JSON object`);
4503
+ }
4504
+ const record = value;
4505
+ if (record.version !== void 0 && record.version !== 1) {
4506
+ throw fieldError2("version", "must be 1");
4507
+ }
4508
+ const harnessRoot = record.harnessRoot;
4509
+ if (harnessRoot !== void 0) {
4510
+ if (typeof harnessRoot !== "string" || harnessRoot === "") {
4511
+ throw fieldError2("harnessRoot", "must be a non-empty string");
4512
+ }
4513
+ if (!isAbsolute(harnessRoot)) {
4514
+ throw fieldError2("harnessRoot", "must be an absolute path");
4515
+ }
4516
+ }
4517
+ const hostHarnesses = record.hostHarnesses;
4518
+ if (hostHarnesses !== void 0) validateReceipts(hostHarnesses, "hostHarnesses", "harness");
4519
+ const hostTools = record.hostTools;
4520
+ if (hostTools !== void 0) validateReceipts(hostTools, "hostTools", "tool", true);
4521
+ return { ...record, version: 1 };
4522
+ }
4523
+ function absoluteField(value, path4) {
4524
+ if (typeof value !== "string" || value === "") {
4525
+ throw fieldError2(path4, "must be a non-empty string");
4526
+ }
4527
+ if (!isAbsolute(value)) throw fieldError2(path4, "must be an absolute path");
4528
+ return value;
4529
+ }
4530
+ function validateReceipts(value, field, noun, allowRootScope = false) {
4531
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4532
+ throw fieldError2(field, `must be a JSON object keyed by ${noun} id`);
4533
+ }
4534
+ for (const [id, receipt] of Object.entries(value)) {
4535
+ if (id === "") throw fieldError2(field, `must use a non-empty ${noun} id as each key`);
4536
+ const at = `${field}.${id}`;
4537
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
4538
+ throw fieldError2(at, "must be a JSON object");
4539
+ }
4540
+ const entry = receipt;
4541
+ if (allowRootScope && entry.scope !== void 0) {
4542
+ if (entry.scope !== "root") throw fieldError2(`${at}.scope`, 'must be "root" when present');
4543
+ for (const key of ["prefix", "binPath"]) {
4544
+ if (entry[key] !== void 0) throw fieldError2(`${at}.${key}`, 'must be absent when scope is "root"');
4545
+ }
4546
+ } else {
4547
+ absoluteField(entry.prefix, `${at}.prefix`);
4548
+ absoluteField(entry.binPath, `${at}.binPath`);
4549
+ }
4550
+ if (typeof entry.binary !== "string" || entry.binary === "") {
4551
+ throw fieldError2(`${at}.binary`, "must be a non-empty string");
4552
+ }
4553
+ if (typeof entry.installedAt !== "string" || entry.installedAt === "") {
4554
+ throw fieldError2(`${at}.installedAt`, "must be a non-empty string");
4555
+ }
4556
+ if (entry.workspaceRoot !== void 0) {
4557
+ absoluteField(entry.workspaceRoot, `${at}.workspaceRoot`);
4558
+ }
4559
+ }
4560
+ }
4561
+ function readHostConfig(env, home) {
4562
+ const path4 = hostConfigPath(env, home);
4563
+ if (!existsSync18(path4)) return { version: 1 };
4564
+ let raw;
4565
+ try {
4566
+ raw = readFileSync12(path4, "utf8");
4567
+ } catch (error) {
4568
+ const detail = error instanceof Error ? error.message : String(error);
4569
+ throw new Error(`could not read ${path4}: ${detail}`);
4570
+ }
4571
+ let parsed;
4572
+ try {
4573
+ parsed = JSON.parse(raw);
4574
+ } catch {
4575
+ throw new Error(`${basename5(path4)} is not valid JSON: ${path4}`);
4576
+ }
4577
+ return validateHostConfig(parsed);
4578
+ }
4579
+ function writeHostConfig(config, env, home) {
4580
+ const path4 = hostConfigPath(env, home);
4581
+ const validated = validateHostConfig({ ...config, version: 1 });
4582
+ const body = `${JSON.stringify(validated, null, 2)}
4583
+ `;
4584
+ const tmp = `${path4}.tmp.${process.pid}`;
4585
+ mkdirSync9(dirname8(path4), { recursive: true });
4586
+ try {
4587
+ writeFileSync9(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
4588
+ renameSync4(tmp, path4);
4589
+ } catch (error) {
4590
+ try {
4591
+ unlinkSync6(tmp);
4592
+ } catch {
4593
+ }
4594
+ throw error;
4595
+ }
4596
+ }
4597
+ function resolveHarnessRoot(explicit, env, home) {
4598
+ if (typeof explicit === "string" && explicit !== "") return resolve8(explicit);
4189
4599
  const configured = readHostConfig(env, home).harnessRoot;
4190
4600
  if (typeof configured === "string" && configured !== "") return resolve8(configured);
4191
4601
  return defaultHarnessRoot(env, home);
@@ -4201,8 +4611,8 @@ function fieldError2(path4, requirement) {
4201
4611
  }
4202
4612
 
4203
4613
  // 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";
4614
+ 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";
4615
+ import { dirname as dirname9, join as join15, resolve as resolve9 } from "node:path";
4206
4616
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4207
4617
  var IGNORABLE_ENTRIES = [
4208
4618
  REGISTRY_SUBDIR,
@@ -4214,7 +4624,7 @@ function isDirectory3(path4) {
4214
4624
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4215
4625
  }
4216
4626
  function stateHomes(home) {
4217
- return STATE_HOME_DIRS.map((name) => join14(home, name));
4627
+ return STATE_HOME_DIRS.map((name) => join15(home, name));
4218
4628
  }
4219
4629
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4220
4630
  const target = resolve9(root);
@@ -4222,7 +4632,7 @@ function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4222
4632
  if (enclosing === void 0) return void 0;
4223
4633
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
4224
4634
  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.
4635
+ Move it out \u2014 for example \`mv ${enclosing} ${join15(home, "agro")}\` \u2014 then re-run this command.
4226
4636
  `;
4227
4637
  }
4228
4638
  function isIgnorableEntry(name) {
@@ -4239,16 +4649,16 @@ function cloneInto(target, run, root, ref) {
4239
4649
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4240
4650
  }
4241
4651
  function cloneThroughStaging(root, run, ref) {
4242
- const staging = mkdtempSync4(join14(dirname9(root), "agro-clone-"));
4652
+ const staging = mkdtempSync4(join15(dirname9(root), "agro-clone-"));
4243
4653
  try {
4244
- const checkout = join14(staging, "agro");
4654
+ const checkout = join15(staging, "agro");
4245
4655
  cloneInto(checkout, run, root, ref);
4246
4656
  for (const name of readdirSync3(checkout)) {
4247
- const destination = join14(root, name);
4248
- if (existsSync18(destination)) {
4657
+ const destination = join15(root, name);
4658
+ if (existsSync19(destination)) {
4249
4659
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4250
4660
  }
4251
- renameSync5(join14(checkout, name), destination);
4661
+ renameSync5(join15(checkout, name), destination);
4252
4662
  }
4253
4663
  } finally {
4254
4664
  rmSync4(staging, { recursive: true, force: true });
@@ -4256,7 +4666,7 @@ function cloneThroughStaging(root, run, ref) {
4256
4666
  }
4257
4667
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4258
4668
  const root = resolve9(path4);
4259
- if (existsSync18(join14(root, ".git"))) return { root, action: "reused" };
4669
+ if (existsSync19(join15(root, ".git"))) return { root, action: "reused" };
4260
4670
  const stats = statSync5(root, { throwIfNoEntry: false });
4261
4671
  if (stats === void 0) {
4262
4672
  mkdirSync10(dirname9(root), { recursive: true });
@@ -4278,11 +4688,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4278
4688
  function listHostWorkspaces(env, home) {
4279
4689
  const root = workspacesRoot(env, home);
4280
4690
  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();
4691
+ 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
4692
  }
4283
4693
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
4284
4694
  const root = resolve9(path4);
4285
- if (existsSync18(join14(root, ".git"))) return { ok: true, root };
4695
+ if (existsSync19(join15(root, ".git"))) return { ok: true, root };
4286
4696
  const existing = listHostWorkspaces(env, home);
4287
4697
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
4288
4698
  `;
@@ -4312,7 +4722,7 @@ function targetFor(root, run, env) {
4312
4722
  });
4313
4723
  }
4314
4724
  function hostPrefix(home) {
4315
- return join15(home, ".local");
4725
+ return join16(home, ".local");
4316
4726
  }
4317
4727
  function pathEntries(env) {
4318
4728
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -4346,6 +4756,21 @@ async function probeInstalled(target, entry, prefix, user, env) {
4346
4756
  throw err;
4347
4757
  }
4348
4758
  }
4759
+ function sandboxMarkerPath(entry) {
4760
+ return `${SANDBOX_HARNESS_PREFIX}/share/agro/harnesses/${entry.id}.installed`;
4761
+ }
4762
+ async function sandboxMarkerExists(target, entry) {
4763
+ const r = await target.exec({
4764
+ argv: ["test", "-f", sandboxMarkerPath(entry)],
4765
+ user: "sandbox",
4766
+ stdio: "capture",
4767
+ timeoutMs: PROBE_TIMEOUT_MS
4768
+ });
4769
+ return r.exitCode === 0;
4770
+ }
4771
+ function hostBinaryUnderPrefix(entry, prefix) {
4772
+ return existsSync20(join16(harnessBinPath(prefix), entry.binary));
4773
+ }
4349
4774
  function stateOf(entry, installed, location) {
4350
4775
  return {
4351
4776
  id: entry.id,
@@ -4365,7 +4790,7 @@ function probeableHost(env, home) {
4365
4790
  return void 0;
4366
4791
  }
4367
4792
  const prefix = hostPrefix(home);
4368
- if (!existsSync19(join15(root, ".git")) && !existsSync19(prefix)) return void 0;
4793
+ if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
4369
4794
  return { root, prefix };
4370
4795
  }
4371
4796
  async function collectStates(root, run, home, env, only) {
@@ -4571,7 +4996,15 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4571
4996
  const code = await reconcileHermes(target, io, bin, void 0, root);
4572
4997
  if (code !== 0) return code;
4573
4998
  }
4574
- if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true) {
4999
+ let hasReceipt;
5000
+ try {
5001
+ hasReceipt = readHostConfig(env, home).hostHarnesses?.[entry.id] !== void 0;
5002
+ } catch (err) {
5003
+ io.stderr(`${bin} harness: ${messageOf(err)}
5004
+ `);
5005
+ return 1;
5006
+ }
5007
+ if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true && (hasReceipt || !hostBinaryUnderPrefix(entry, prefix))) {
4575
5008
  io.stdout(`${entry.id}: already installed (${entry.binary})
4576
5009
  `);
4577
5010
  try {
@@ -4735,7 +5168,20 @@ async function runHarnessUninstall(name, opts, io) {
4735
5168
  if (!isReachable(await resolveTargetStatus(target))) {
4736
5169
  return await uninstallOnHost(entry, opts, io, run);
4737
5170
  }
4738
- return (await removeHarness(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
5171
+ const outcome = await removeHarness(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io);
5172
+ if (outcome.dropReceipt) {
5173
+ const cleared = await target.exec({
5174
+ argv: ["rm", "-f", sandboxMarkerPath(entry)],
5175
+ user: "sandbox",
5176
+ stdio: "capture"
5177
+ });
5178
+ if (cleared.exitCode !== 0) {
5179
+ io.stderr(`${opts.bin} harness: could not clear the install record (exit ${cleared.exitCode}).
5180
+ `);
5181
+ return 1;
5182
+ }
5183
+ }
5184
+ return outcome.code;
4739
5185
  }
4740
5186
  async function runHarnessInstall(name, opts, io) {
4741
5187
  const run = opts.run ?? spawnRunner;
@@ -4761,7 +5207,7 @@ async function runHarnessInstall(name, opts, io) {
4761
5207
  if (code !== 0) return code;
4762
5208
  }
4763
5209
  const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
4764
- if (already === true) {
5210
+ if (already === true && await sandboxMarkerExists(target, entry)) {
4765
5211
  io.stdout(`${entry.id}: already installed (${entry.binary})
4766
5212
  `);
4767
5213
  return 0;
@@ -4792,15 +5238,26 @@ async function runHarnessInstall(name, opts, io) {
4792
5238
  const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4793
5239
  if (code !== 0) return code;
4794
5240
  }
5241
+ const marker = sandboxMarkerPath(entry);
5242
+ const marked = await target.exec({
5243
+ argv: ["sh", "-c", `mkdir -p "$(dirname "$1")" && : > "$1"`, "sh", marker],
5244
+ user: "sandbox",
5245
+ stdio: "capture"
5246
+ });
5247
+ if (marked.exitCode !== 0) {
5248
+ io.stderr(`${opts.bin} harness: could not record the install at ${marker} (exit ${marked.exitCode}).
5249
+ `);
5250
+ return 1;
5251
+ }
4795
5252
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
4796
5253
  `);
4797
5254
  return 0;
4798
5255
  }
4799
5256
 
4800
5257
  // src/commands/workspace.ts
4801
- import { existsSync as existsSync20, rmSync as rmSync5 } from "node:fs";
5258
+ import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
4802
5259
  import { homedir as homedir4 } from "node:os";
4803
- import { join as join16, resolve as resolve10 } from "node:path";
5260
+ import { join as join17, resolve as resolve10 } from "node:path";
4804
5261
  function homeOf2(opts) {
4805
5262
  return (opts.homedir ?? homedir4)();
4806
5263
  }
@@ -4846,9 +5303,9 @@ async function runWorkspaceCreate(name, opts, io) {
4846
5303
  return 1;
4847
5304
  }
4848
5305
  let action;
4849
- const preexisting = existsSync20(root);
5306
+ const preexisting = existsSync21(root);
4850
5307
  try {
4851
- if (!opts.json && !existsSync20(join16(root, ".git"))) {
5308
+ if (!opts.json && !existsSync21(join17(root, ".git"))) {
4852
5309
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
4853
5310
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
4854
5311
  `);
@@ -4893,7 +5350,7 @@ async function runWorkspaceList(opts, io) {
4893
5350
  const recorded = recordedRoot(env, home);
4894
5351
  const registry = workspacesRoot(env, home);
4895
5352
  rows = listHostWorkspaces(env, home).map((name) => {
4896
- const root = join16(registry, name);
5353
+ const root = join17(registry, name);
4897
5354
  return { name, root, default: recorded === root };
4898
5355
  });
4899
5356
  } catch (err) {
@@ -4918,9 +5375,9 @@ async function runWorkspaceList(opts, io) {
4918
5375
  }
4919
5376
 
4920
5377
  // src/commands/tool.ts
4921
- import { existsSync as existsSync21 } from "node:fs";
5378
+ import { existsSync as existsSync22 } from "node:fs";
4922
5379
  import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
4923
- import { delimiter as delimiter3, join as join17 } from "node:path";
5380
+ import { delimiter as delimiter3, join as join18 } from "node:path";
4924
5381
 
4925
5382
  // src/lib/tools/catalog.ts
4926
5383
  var TOOLS_DOC = "docs/installation.md";
@@ -5337,6 +5794,7 @@ function resolveToolInstallArgv(entry, host) {
5337
5794
  }
5338
5795
 
5339
5796
  // src/commands/tool.ts
5797
+ var ROOT_TOOL_REMOVAL_DOCS = `${sourceDocsUrl("docs/installation.md")}#remove-a-root-level-tool`;
5340
5798
  function isReachable2(status) {
5341
5799
  return status === "ready" || status === "starting";
5342
5800
  }
@@ -5350,7 +5808,21 @@ function targetFor2(root, run, env) {
5350
5808
  });
5351
5809
  }
5352
5810
  function hostPrefix2(home) {
5353
- return join17(home, ".local");
5811
+ return join18(home, ".local");
5812
+ }
5813
+ function sandboxMarkerPath2(entry) {
5814
+ return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
5815
+ }
5816
+ async function sandboxMarkerExists2(target, entry) {
5817
+ const r = await target.exec({
5818
+ argv: ["test", "-f", sandboxMarkerPath2(entry)],
5819
+ user: "sandbox",
5820
+ stdio: "capture"
5821
+ });
5822
+ return r.exitCode === 0;
5823
+ }
5824
+ function hostBinaryUnderPrefix2(entry, prefix) {
5825
+ return existsSync22(join18(harnessBinPath(prefix), entry.binary));
5354
5826
  }
5355
5827
  function pathEntries2(env) {
5356
5828
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -5430,7 +5902,7 @@ function probeableHost2(env, home) {
5430
5902
  return void 0;
5431
5903
  }
5432
5904
  const prefix = hostPrefix2(home);
5433
- if (!existsSync21(join17(root, ".git")) && !existsSync21(prefix)) return void 0;
5905
+ if (!existsSync22(join18(root, ".git")) && !existsSync22(prefix)) return void 0;
5434
5906
  return { root, prefix };
5435
5907
  }
5436
5908
  async function collectRows(root, run, home, env, only) {
@@ -5646,7 +6118,15 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5646
6118
  const prefix = hostPrefix2(home);
5647
6119
  const installEnv = { NPM_USER_PREFIX: prefix };
5648
6120
  const target = hostTargetFor2(root, prefix, run, env);
5649
- if (await probeInstalled2(target, entry, void 0, installEnv) === true) {
6121
+ let hasReceipt;
6122
+ try {
6123
+ hasReceipt = readHostConfig(env, home).hostTools?.[entry.id] !== void 0;
6124
+ } catch (err) {
6125
+ io.stderr(`${bin} tool: ${messageOf3(err)}
6126
+ `);
6127
+ return 1;
6128
+ }
6129
+ if (await probeInstalled2(target, entry, void 0, installEnv) === true && (hasReceipt || entry.hostInstallUser === "root" || !hostBinaryUnderPrefix2(entry, prefix))) {
5650
6130
  io.stdout(`${entry.id}: already installed (${entry.binary})
5651
6131
  `);
5652
6132
  try {
@@ -5681,13 +6161,8 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5681
6161
  `);
5682
6162
  return r.exitCode;
5683
6163
  }
5684
- const receipt = {
5685
- prefix,
5686
- binary: entry.binary,
5687
- binPath: harnessBinPath(prefix),
5688
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
5689
- workspaceRoot: root
5690
- };
6164
+ const installedAt = (/* @__PURE__ */ new Date()).toISOString();
6165
+ const receipt = entry.hostInstallUser === "root" ? { scope: "root", binary: entry.binary, installedAt, workspaceRoot: root } : { prefix, binary: entry.binary, binPath: harnessBinPath(prefix), installedAt, workspaceRoot: root };
5691
6166
  try {
5692
6167
  const config = readHostConfig(env, home);
5693
6168
  writeHostConfig(
@@ -5747,7 +6222,7 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5747
6222
  }
5748
6223
  if (entry.installArgv === void 0) return refuseContainer();
5749
6224
  const already = await probeInstalled2(target, entry, "sandbox");
5750
- if (already === true) {
6225
+ if (already === true && await sandboxMarkerExists2(target, entry)) {
5751
6226
  io.stdout(`${entry.id}: already installed (${entry.binary})
5752
6227
  `);
5753
6228
  return 0;
@@ -5769,6 +6244,17 @@ ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5769
6244
  `);
5770
6245
  return r.exitCode;
5771
6246
  }
6247
+ const marker = sandboxMarkerPath2(entry);
6248
+ const marked = await target.exec({
6249
+ argv: ["sh", "-c", `mkdir -p "$(dirname "$1")" && : > "$1"`, "sh", marker],
6250
+ user: "sandbox",
6251
+ stdio: "capture"
6252
+ });
6253
+ if (marked.exitCode !== 0) {
6254
+ io.stderr(`${opts.bin} tool: could not record the install at ${marker} (exit ${marked.exitCode}).
6255
+ `);
6256
+ return 1;
6257
+ }
5772
6258
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)}
5773
6259
  `);
5774
6260
  return 0;
@@ -5829,7 +6315,7 @@ Removing it could delete a tool you installed yourself. Re-run with \`--force\`
5829
6315
  );
5830
6316
  return 1;
5831
6317
  }
5832
- const prefix = receipt?.prefix ?? computed;
6318
+ const prefix = receipt === void 0 || isRootToolReceipt(receipt) ? computed : receipt.prefix;
5833
6319
  const target = hostTargetFor2(receipt?.workspaceRoot ?? workspace, prefix, run, env);
5834
6320
  const outcome = await removeTool(entry, target, prefix, void 0, opts, io, true);
5835
6321
  if (outcome.dropReceipt && receipt !== void 0) {
@@ -5852,6 +6338,15 @@ async function runToolUninstall(name, opts, io) {
5852
6338
  const root = resolveProjectRoot(opts.cwd);
5853
6339
  const entry = findTool(name);
5854
6340
  if (!entry) return unknownTool(name, io, opts.bin);
6341
+ if (entry.hostInstallUser === "root") {
6342
+ io.stderr(
6343
+ `${opts.bin} tool: ${entry.id} cannot be removed by this command.
6344
+ ${opts.bin} does not remove system packages that it installed as root.
6345
+ Remove ${entry.id} by hand: ${ROOT_TOOL_REMOVAL_DOCS}
6346
+ `
6347
+ );
6348
+ return 1;
6349
+ }
5855
6350
  if (entry.uninstallArgv === null) {
5856
6351
  io.stderr(`${opts.bin} tool: ${entry.id} cannot be removed by this command.
5857
6352
 
@@ -5864,15 +6359,28 @@ async function runToolUninstall(name, opts, io) {
5864
6359
  if (!isReachable2(await resolveTargetStatus(target))) {
5865
6360
  return await uninstallOnHost2(entry, opts, io, run);
5866
6361
  }
5867
- return (await removeTool(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io)).code;
6362
+ const outcome = await removeTool(entry, target, SANDBOX_HARNESS_PREFIX, "sandbox", opts, io);
6363
+ if (outcome.dropReceipt) {
6364
+ const cleared = await target.exec({
6365
+ argv: ["rm", "-f", sandboxMarkerPath2(entry)],
6366
+ user: "sandbox",
6367
+ stdio: "capture"
6368
+ });
6369
+ if (cleared.exitCode !== 0) {
6370
+ io.stderr(`${opts.bin} tool: could not clear the install record (exit ${cleared.exitCode}).
6371
+ `);
6372
+ return 1;
6373
+ }
6374
+ }
6375
+ return outcome.code;
5868
6376
  }
5869
6377
 
5870
6378
  // src/lib/remote.ts
5871
- import { spawnSync as spawnSync3 } from "node:child_process";
6379
+ import { spawnSync as spawnSync4 } from "node:child_process";
5872
6380
  var DEFAULT_REPO_URL = "https://github.com/mifunedev/agro";
5873
6381
  var DEFAULT_CLONE_TIMEOUT_MS = 12e4;
5874
6382
  var spawnRunner2 = (cmd, args, opts) => {
5875
- const r = spawnSync3(cmd, args, {
6383
+ const r = spawnSync4(cmd, args, {
5876
6384
  env: opts.env,
5877
6385
  timeout: opts.timeoutMs,
5878
6386
  stdio: ["ignore", "ignore", "pipe"],
@@ -5930,6 +6438,35 @@ function fetchRemoteSource(opts) {
5930
6438
  return opts.destDir;
5931
6439
  }
5932
6440
 
6441
+ // src/command-table.ts
6442
+ var CONFIG_VERBS = ["show", "set", "repo"];
6443
+ var CONFIG_INTEGRATIONS = ["langfuse"];
6444
+ var SECRET_VERBS = ["set", "list"];
6445
+ var LANGFUSE_VERBS = ["apply", "status", "disable"];
6446
+ var SANDBOX_SUBCOMMANDS = ["install", "list", "upgrade"];
6447
+ var WORKSPACE_SUBCOMMANDS = ["create", "list"];
6448
+ var HARNESS_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
6449
+ var TOOL_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
6450
+ function isOneOf(values, value) {
6451
+ return value !== void 0 && values.includes(value);
6452
+ }
6453
+ var AGRO_COMMANDS = Object.freeze({
6454
+ config: [...CONFIG_VERBS, ...CONFIG_INTEGRATIONS],
6455
+ secret: SECRET_VERBS,
6456
+ langfuse: LANGFUSE_VERBS,
6457
+ update: null,
6458
+ "self-upgrade": null,
6459
+ vendor: null,
6460
+ sandbox: SANDBOX_SUBCOMMANDS,
6461
+ shell: null,
6462
+ compose: null,
6463
+ workspace: WORKSPACE_SUBCOMMANDS,
6464
+ harness: HARNESS_SUBCOMMANDS,
6465
+ tool: TOOL_SUBCOMMANDS,
6466
+ gateway: null,
6467
+ ...Object.fromEntries(composeVerbs().map((verb) => [verb, null]))
6468
+ });
6469
+
5933
6470
  // src/cli.ts
5934
6471
  var DEFAULT_SOURCE_CONTROL_DIR = resolve11(
5935
6472
  dirname10(fileURLToPath(import.meta.url)),
@@ -5944,7 +6481,7 @@ var INTEGRATIONS = {
5944
6481
  )
5945
6482
  }
5946
6483
  };
5947
- function isHelpFlag(arg) {
6484
+ function isHelpFlag2(arg) {
5948
6485
  return arg === "--help" || arg === "-h" || arg === "help";
5949
6486
  }
5950
6487
  function isVersionFlag(arg) {
@@ -5961,7 +6498,7 @@ function printAgroHelp(product = AGRO_PRODUCT) {
5961
6498
  process.stdout.write(`${bin} \u2014 ${title} (v${AGRO_VERSION})
5962
6499
 
5963
6500
  Usage:
5964
- ${bin} sandbox <args...> Create and list sandboxes (install|list)
6501
+ ${bin} sandbox <args...> Create, list, and upgrade sandboxes (install|list|upgrade)
5965
6502
  ${bin} shell [name] Open a zsh shell in the running sandbox container
5966
6503
  ${bin} config <args...> Read and write ${stateNames(bin).configFile} (show|set), or run a wizard
5967
6504
  ${bin} secret <args...> Read and write the gitignored root .env (set|list)
@@ -6111,66 +6648,6 @@ Flags:
6111
6648
  --force Override the up-to-date / downgrade gate.
6112
6649
  `);
6113
6650
  }
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
6651
  function printShellHelp(bin = AGRO_PRODUCT.bin) {
6175
6652
  process.stdout.write(`${bin} shell \u2014 Open a shell in the running sandbox container
6176
6653
 
@@ -6295,6 +6772,8 @@ no workspace: run \`${bin} workspace create\` first. It installs into
6295
6772
  \`~/.local\` and records the install as \`hostTools\` in that same file. On the host,
6296
6773
  \`uninstall\` removes only what \`install\` recorded, from the prefix in that record.
6297
6774
  Without a record it refuses, and \`--force\` overrides.
6775
+ \`uninstall\` refuses each root-level tool, marked \`(root)\` in \`${bin} tool list\`,
6776
+ with and without \`--force\`. The refusal links the manual removal steps.
6298
6777
 
6299
6778
  Flags:
6300
6779
  --yes Accept a large download without prompting
@@ -6382,9 +6861,8 @@ function extractSandboxFlag(command, rest) {
6382
6861
  }
6383
6862
  return { ok: true, args: sandbox === void 0 ? { rest: kept } : { rest: kept, sandbox } };
6384
6863
  }
6385
- var LANGFUSE_VERBS = ["apply", "status", "disable"];
6386
6864
  function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
6387
- if (rest.length === 0 || isHelpFlag(rest[0])) return { ok: true, args: { help: true } };
6865
+ if (rest.length === 0 || isHelpFlag2(rest[0])) return { ok: true, args: { help: true } };
6388
6866
  const [head, ...tail] = rest;
6389
6867
  if (!LANGFUSE_VERBS.includes(head)) {
6390
6868
  return {
@@ -6393,13 +6871,12 @@ function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
6393
6871
  showHelp: true
6394
6872
  };
6395
6873
  }
6396
- if (isHelpFlag(tail[0])) return { ok: true, args: { help: true } };
6874
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { help: true } };
6397
6875
  if (tail.length > 0) {
6398
6876
  return { ok: false, error: `${bin} langfuse ${head}: unexpected argument "${tail[0]}"` };
6399
6877
  }
6400
6878
  return { ok: true, args: { help: false, verb: head } };
6401
6879
  }
6402
- var CONFIG_VERBS = ["show", "set", "repo"];
6403
6880
  function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6404
6881
  const scoped = extractSandboxFlag(`${bin} config`, input);
6405
6882
  if (!scoped.ok) return scoped;
@@ -6411,12 +6888,12 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6411
6888
  ...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox },
6412
6889
  ...forced ? { force: true } : {}
6413
6890
  };
6414
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6891
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6415
6892
  return { ok: true, args: { ...args, help: true } };
6416
6893
  }
6417
6894
  const [head, ...tail] = rest;
6418
6895
  if (!CONFIG_VERBS.includes(head)) {
6419
- if (tail.length > 0 && isHelpFlag(tail[0])) {
6896
+ if (tail.length > 0 && isHelpFlag2(tail[0])) {
6420
6897
  return { ok: true, args: { ...args, integration: head, integrationHelp: true } };
6421
6898
  }
6422
6899
  if (tail.length > 0) {
@@ -6428,7 +6905,7 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6428
6905
  return { ok: true, args: { ...args, integration: head } };
6429
6906
  }
6430
6907
  const verb = head;
6431
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6908
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6432
6909
  if (verb === "show" || verb === "repo") {
6433
6910
  if (tail.length > 0) {
6434
6911
  return { ok: false, error: `${bin} config ${verb}: unexpected argument "${tail[0]}"` };
@@ -6451,7 +6928,6 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
6451
6928
  }
6452
6929
  return { ok: true, args: { ...args, verb, key, value } };
6453
6930
  }
6454
- var SECRET_VERBS = ["set", "list"];
6455
6931
  function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6456
6932
  const scoped = extractSandboxFlag(`${bin} secret`, input);
6457
6933
  if (!scoped.ok) return scoped;
@@ -6460,7 +6936,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6460
6936
  help: false,
6461
6937
  ...scoped.args.sandbox === void 0 ? {} : { sandbox: scoped.args.sandbox }
6462
6938
  };
6463
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6939
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6464
6940
  return { ok: true, args: { ...args, help: true } };
6465
6941
  }
6466
6942
  const [head, ...tail] = rest;
@@ -6472,7 +6948,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6472
6948
  };
6473
6949
  }
6474
6950
  const verb = head;
6475
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
6951
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6476
6952
  if (verb === "list") {
6477
6953
  if (tail.length > 0) {
6478
6954
  return { ok: false, error: `${bin} secret list: unexpected argument "${tail[0]}"` };
@@ -6493,7 +6969,7 @@ function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
6493
6969
  }
6494
6970
  function parseDestroyArgs(rest, bin = AGRO_PRODUCT.bin) {
6495
6971
  const args = { help: false, yes: false };
6496
- if (isHelpFlag(rest[0])) return { ok: true, args: { ...args, help: true } };
6972
+ if (isHelpFlag2(rest[0])) return { ok: true, args: { ...args, help: true } };
6497
6973
  for (const token of rest) {
6498
6974
  if (token === "--yes") {
6499
6975
  args.yes = true;
@@ -6512,7 +6988,7 @@ function parseDestroyArgs(rest, bin = AGRO_PRODUCT.bin) {
6512
6988
  }
6513
6989
  function parseComposeArgs(rest, bin = AGRO_PRODUCT.bin) {
6514
6990
  const args = { help: false, passthrough: [] };
6515
- if (rest.length === 0 || isHelpFlag(rest[0])) {
6991
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6516
6992
  return { ok: true, args: { ...args, help: true } };
6517
6993
  }
6518
6994
  if (rest[0] !== "config") {
@@ -6524,7 +7000,7 @@ function parseComposeArgs(rest, bin = AGRO_PRODUCT.bin) {
6524
7000
  }
6525
7001
  args.subcommand = "config";
6526
7002
  const tail = rest.slice(1);
6527
- if (isHelpFlag(tail[0])) return { ok: true, args: { ...args, help: true } };
7003
+ if (isHelpFlag2(tail[0])) return { ok: true, args: { ...args, help: true } };
6528
7004
  const sep4 = tail.indexOf("--");
6529
7005
  if (sep4 === -1 && tail.length > 0) {
6530
7006
  return {
@@ -6550,7 +7026,7 @@ function parseSelfUpgradeArgs(rest, bin = AGRO_PRODUCT.bin) {
6550
7026
  args.dryRun = true;
6551
7027
  continue;
6552
7028
  }
6553
- if (isHelpFlag(arg)) {
7029
+ if (isHelpFlag2(arg)) {
6554
7030
  args.help = true;
6555
7031
  return { ok: true, args };
6556
7032
  }
@@ -6592,7 +7068,7 @@ function parseVendorArgs(rest, bin = AGRO_PRODUCT.bin) {
6592
7068
  args.force = true;
6593
7069
  continue;
6594
7070
  }
6595
- if (isHelpFlag(arg)) {
7071
+ if (isHelpFlag2(arg)) {
6596
7072
  args.help = true;
6597
7073
  return { ok: true, args };
6598
7074
  }
@@ -6609,121 +7085,9 @@ function parseVendorArgs(rest, bin = AGRO_PRODUCT.bin) {
6609
7085
  }
6610
7086
  return { ok: true, args };
6611
7087
  }
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
7088
  function parseShellArgs(rest, bin = AGRO_PRODUCT.bin) {
6725
7089
  const args = { help: false };
6726
- if (isHelpFlag(rest[0])) return { ok: true, args: { help: true } };
7090
+ if (isHelpFlag2(rest[0])) return { ok: true, args: { help: true } };
6727
7091
  for (const token of rest) {
6728
7092
  if (token.startsWith("-")) {
6729
7093
  return { ok: false, error: `${bin} shell: unknown flag "${token}"` };
@@ -6737,7 +7101,7 @@ function parseShellArgs(rest, bin = AGRO_PRODUCT.bin) {
6737
7101
  }
6738
7102
  function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6739
7103
  const args = { help: false, json: false };
6740
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7104
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6741
7105
  return { ok: true, args: { ...args, help: true } };
6742
7106
  }
6743
7107
  const positionals = [];
@@ -6773,7 +7137,7 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6773
7137
  return { ok: false, error: `${bin} workspace: --ref requires a branch or tag` };
6774
7138
  }
6775
7139
  const [sub, name, ...extra] = positionals;
6776
- if (sub !== "create" && sub !== "list") {
7140
+ if (!isOneOf(WORKSPACE_SUBCOMMANDS, sub)) {
6777
7141
  return {
6778
7142
  ok: false,
6779
7143
  error: `${bin} workspace: unknown subcommand "${sub}" \u2014 expected create or list`,
@@ -6809,7 +7173,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
6809
7173
  host: false,
6810
7174
  force: false
6811
7175
  };
6812
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7176
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6813
7177
  return { ok: true, args: { ...args, help: true } };
6814
7178
  }
6815
7179
  const positionals = [];
@@ -6859,7 +7223,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
6859
7223
  };
6860
7224
  }
6861
7225
  const [sub, name, ...extra] = positionals;
6862
- if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
7226
+ if (!isOneOf(HARNESS_SUBCOMMANDS, sub)) {
6863
7227
  return {
6864
7228
  ok: false,
6865
7229
  error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
@@ -6890,7 +7254,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
6890
7254
  }
6891
7255
  function parseToolArgs(rest, bin = AGRO_PRODUCT.bin) {
6892
7256
  const args = { help: false, yes: false, json: false, host: false, force: false };
6893
- if (rest.length === 0 || isHelpFlag(rest[0])) {
7257
+ if (rest.length === 0 || isHelpFlag2(rest[0])) {
6894
7258
  return { ok: true, args: { ...args, help: true } };
6895
7259
  }
6896
7260
  const positionals = [];
@@ -6916,7 +7280,7 @@ function parseToolArgs(rest, bin = AGRO_PRODUCT.bin) {
6916
7280
  return { ok: false, error: `${bin} tool: --path requires a directory` };
6917
7281
  }
6918
7282
  const [sub, name, ...extra] = positionals;
6919
- if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
7283
+ if (!isOneOf(TOOL_SUBCOMMANDS, sub)) {
6920
7284
  return {
6921
7285
  ok: false,
6922
7286
  error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
@@ -6948,11 +7312,11 @@ function parseGatewayArgs(rest) {
6948
7312
  }
6949
7313
  return { ok: true, args: { help: false, passthrough: [...rest] } };
6950
7314
  }
6951
- function bundledPayloadExists(bundled, exists = existsSync22) {
6952
- return exists(join18(bundled.sourceControlDir, "manifest.json"));
7315
+ function bundledPayloadExists(bundled, exists = existsSync23) {
7316
+ return exists(join19(bundled.sourceControlDir, "manifest.json"));
6953
7317
  }
6954
7318
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6955
- const exists = bundled.exists ?? existsSync22;
7319
+ const exists = bundled.exists ?? existsSync23;
6956
7320
  if (args.fromDir !== void 0) {
6957
7321
  return { kind: "local", fromDir: resolve11(args.fromDir) };
6958
7322
  }
@@ -6972,7 +7336,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
6972
7336
  function readPayloadVersion(checkoutDir) {
6973
7337
  try {
6974
7338
  const parsed = JSON.parse(
6975
- readFileSync12(join18(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7339
+ readFileSync13(join19(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
6976
7340
  );
6977
7341
  if (parsed && typeof parsed.version === "string") return parsed.version;
6978
7342
  } catch {
@@ -6981,7 +7345,7 @@ function readPayloadVersion(checkoutDir) {
6981
7345
  }
6982
7346
  async function runWithRemoteSource(hooks, fn) {
6983
7347
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
6984
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join18(tmpdir4(), "oh-remote-")));
7348
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join19(tmpdir4(), "oh-remote-")));
6985
7349
  const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
6986
7350
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
6987
7351
  const checkoutDir = mkdtemp();
@@ -6998,7 +7362,7 @@ async function main(argv) {
6998
7362
  const product = resolveProduct(process.argv[1]);
6999
7363
  const bin = product.bin;
7000
7364
  const [first, second] = argv;
7001
- if (!first || isHelpFlag(first)) {
7365
+ if (!first || isHelpFlag2(first)) {
7002
7366
  printAgroHelp(product);
7003
7367
  return 0;
7004
7368
  }
@@ -7007,6 +7371,7 @@ async function main(argv) {
7007
7371
  `);
7008
7372
  return 0;
7009
7373
  }
7374
+ if (!Object.hasOwn(AGRO_COMMANDS, first)) return unknownCommand(first, product);
7010
7375
  if (first === "config") {
7011
7376
  const parsed = parseConfigArgs(argv.slice(1), bin);
7012
7377
  if (!parsed.ok) {
@@ -7141,37 +7506,7 @@ async function main(argv) {
7141
7506
  (checkoutDir) => runUpdate({ bin, targetDir, fromDir: checkoutDir, force, dryRun }, io)
7142
7507
  );
7143
7508
  }
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
- }
7509
+ if (first === "sandbox") return runSandboxCommand(argv.slice(1), bin);
7175
7510
  if (first === "shell") {
7176
7511
  const parsed = parseShellArgs(argv.slice(1), bin);
7177
7512
  if (!parsed.ok) {
@@ -7256,7 +7591,7 @@ async function main(argv) {
7256
7591
  if (composeVerbs().includes(first)) {
7257
7592
  const verb = first;
7258
7593
  const rest = argv.slice(1);
7259
- if (isHelpFlag(rest[0])) {
7594
+ if (isHelpFlag2(rest[0])) {
7260
7595
  printComposeVerbHelp(verb, bin);
7261
7596
  return 0;
7262
7597
  }
@@ -7359,7 +7694,10 @@ async function main(argv) {
7359
7694
  }
7360
7695
  return runGateway(parsed.args.passthrough, { bin });
7361
7696
  }
7362
- process.stderr.write(`${bin}: unknown command "${first}"
7697
+ return unknownCommand(first, product);
7698
+ }
7699
+ function unknownCommand(first, product) {
7700
+ process.stderr.write(`${product.bin}: unknown command "${first}"
7363
7701
 
7364
7702
  `);
7365
7703
  printAgroHelp(product);
@@ -7381,12 +7719,9 @@ main(process.argv.slice(2)).then(
7381
7719
  }
7382
7720
  );
7383
7721
  export {
7384
- CONFIG_VERBS,
7385
- LANGFUSE_VERBS,
7386
- SECRET_VERBS,
7387
7722
  bundledPayloadExists,
7388
7723
  extractSandboxFlag,
7389
- isHelpFlag,
7724
+ isHelpFlag2 as isHelpFlag,
7390
7725
  isVersionFlag,
7391
7726
  parseComposeArgs,
7392
7727
  parseConfigArgs,