@mifune/agro 0.17.0-rc.1 → 0.18.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 +6 -6
  2. package/dist/agro.js +323 -660
  3. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -7,7 +7,7 @@ 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 join20, resolve as resolve13 } from "node:path";
10
+ import { dirname as dirname10, join as join19, resolve as resolve11 } from "node:path";
11
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
 
@@ -531,7 +531,7 @@ function refuseUnsupported(installation, bin) {
531
531
  return new Error(`${at} \u2014 rebuild from the checkout: npm --prefix .agro/cli run build`);
532
532
  default:
533
533
  return new Error(
534
- `unknown installation (${reason ?? target}) \u2014 install with npm install -g ${PACKAGE} or get-agro.sh`
534
+ `unknown installation (${reason ?? target}) \u2014 install with npm install -g ${PACKAGE} or install.sh`
535
535
  );
536
536
  }
537
537
  }
@@ -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);
@@ -629,7 +638,7 @@ async function upgradeStandalone(installation, opts, deps, io) {
629
638
  deps.access(dir, constants.W_OK);
630
639
  } catch {
631
640
  throw new Error(
632
- `${dir} is not writable; re-run as the user who owns ${target}, or reinstall into a writable directory with get-agro.sh`
641
+ `${dir} is not writable; re-run as the user who owns ${target}, or reinstall into a writable directory with install.sh`
633
642
  );
634
643
  }
635
644
  let artifact;
@@ -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) {
@@ -835,49 +844,6 @@ import { resolve as resolve3, sep } from "node:path";
835
844
  // src/lib/agro-config.ts
836
845
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
837
846
  import { basename as basename3, dirname as dirname2, resolve as resolve2 } from "node:path";
838
-
839
- // src/lib/runtimes/catalog.ts
840
- var CATALOG = [
841
- {
842
- id: "docker",
843
- title: "Docker container",
844
- tier: "container",
845
- state: "active",
846
- provisionable: true,
847
- docsPath: "docs/runtimes/docker.md"
848
- },
849
- {
850
- id: "microsandbox",
851
- title: "MicroSandbox",
852
- tier: "microvm",
853
- state: "planned",
854
- provisionable: false,
855
- notProvisionableReason: (bin) => `microsandbox is not a provisionable runtime yet; see docs/rfcs/rfc-runtime-support.md. Inside a sandbox run \`${bin} tool install microsandbox\`.`,
856
- docsPath: "docs/runtimes/microsandbox.md"
857
- },
858
- {
859
- id: "openshell",
860
- title: "NVIDIA OpenShell",
861
- tier: "container",
862
- state: "active",
863
- provisionable: true,
864
- docsPath: "docs/runtimes/openshell.md"
865
- }
866
- ];
867
- var RUNTIME_CATALOG = Object.freeze(
868
- CATALOG.map((entry) => Object.freeze({ ...entry }))
869
- );
870
- var PROVISIONABLE_RUNTIMES = Object.freeze(
871
- CATALOG.filter((entry) => entry.provisionable).map((entry) => entry.id)
872
- );
873
- function findRuntime(id) {
874
- return RUNTIME_CATALOG.find((r) => r.id === id);
875
- }
876
- function runtimeIds() {
877
- return RUNTIME_CATALOG.map((r) => r.id);
878
- }
879
-
880
- // src/lib/agro-config.ts
881
847
  var AGRO_CONFIG_FILE = NAMES.configFile;
882
848
  var AGRO_CONFIG_MODE = 420;
883
849
  var RESERVED_HOME_PATHS = [
@@ -899,7 +865,7 @@ var RESERVED_HOME_PATHS = [
899
865
  "/usr",
900
866
  "/var"
901
867
  ];
902
- var SANDBOX_RUNTIMES = PROVISIONABLE_RUNTIMES;
868
+ var SANDBOX_RUNTIMES = ["docker"];
903
869
  function configCheckout(config) {
904
870
  const value = config.checkout ?? config.repo;
905
871
  return typeof value === "string" && value !== "" ? value : void 0;
@@ -928,13 +894,6 @@ function defaultAgroConfig(name) {
928
894
  composeOverrides: []
929
895
  };
930
896
  }
931
- function entryRuntime(root) {
932
- try {
933
- return readAgroConfig(agroConfigPath(root)).runtime ?? "docker";
934
- } catch {
935
- return "docker";
936
- }
937
- }
938
897
  function readAgroConfig(path4) {
939
898
  if (!existsSync4(path4)) return defaultAgroConfig(basename3(dirname2(resolve2(path4))));
940
899
  let raw;
@@ -1324,8 +1283,8 @@ function redact(token) {
1324
1283
  async function ask(question) {
1325
1284
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1326
1285
  try {
1327
- return await new Promise((resolve14) => {
1328
- rl.question(` ${question} `, (answer) => resolve14(answer.trim()));
1286
+ return await new Promise((resolve12) => {
1287
+ rl.question(` ${question} `, (answer) => resolve12(answer.trim()));
1329
1288
  });
1330
1289
  } finally {
1331
1290
  rl.close();
@@ -1337,7 +1296,7 @@ async function askSecret(question) {
1337
1296
  process.stdin.setRawMode(true);
1338
1297
  process.stdin.resume();
1339
1298
  const bytes = [];
1340
- return await new Promise((resolve14) => {
1299
+ return await new Promise((resolve12) => {
1341
1300
  const cleanup = () => {
1342
1301
  try {
1343
1302
  process.stdin.setRawMode(false);
@@ -1355,7 +1314,7 @@ async function askSecret(question) {
1355
1314
  } else if (byte === 13 || byte === 10) {
1356
1315
  cleanup();
1357
1316
  process.stdout.write("\n");
1358
- resolve14(Buffer.from(bytes).toString("utf8").trim());
1317
+ resolve12(Buffer.from(bytes).toString("utf8").trim());
1359
1318
  return;
1360
1319
  } else if (byte === 127 || byte === 8) {
1361
1320
  if (bytes.length > 0) {
@@ -1405,10 +1364,10 @@ async function askSecretPiped(question) {
1405
1364
  };
1406
1365
  process.on("SIGINT", onSigint);
1407
1366
  try {
1408
- return await new Promise((resolve14) => {
1367
+ return await new Promise((resolve12) => {
1409
1368
  rl.question(` ${question} `, (answer) => {
1410
1369
  process.stdout.write("\n");
1411
- resolve14(answer.trim());
1370
+ resolve12(answer.trim());
1412
1371
  });
1413
1372
  });
1414
1373
  } finally {
@@ -1453,10 +1412,10 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as read
1453
1412
  import { dirname as dirname4, join as join4, resolve as resolve5, sep as sep2 } from "node:path";
1454
1413
 
1455
1414
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.yml
1456
- 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';
1457
1416
 
1458
1417
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.image-only.yml
1459
- 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';
1460
1419
 
1461
1420
  // agro-asset:/home/runner/work/agro/agro/.devcontainer/docker-compose.ssh.yml
1462
1421
  var docker_compose_ssh_default = 'services:\n sandbox:\n ports:\n - "127.0.0.1:${SANDBOX_SSH_PORT:-2222}:22"\n';
@@ -1534,8 +1493,7 @@ if [ -f "$REPO_DIR/harness.yaml" ] && [ -f "$MIGRATOR" ]; then
1534
1493
  sh "$MIGRATOR" "$REPO_DIR" >&2 || true
1535
1494
  fi
1536
1495
 
1537
- ENV_FILE="$REPO_DIR/.env"
1538
- [ -f "$ENV_FILE" ] || ENV_FILE="$REPO_DIR/.devcontainer/.env"
1496
+ ENV_FILE="$(agro_env_file "$REPO_DIR")"
1539
1497
 
1540
1498
  compose_path() {
1541
1499
  case "$1" in
@@ -1682,6 +1640,14 @@ agro_config_file() {
1682
1640
  printf '%s\\n' "$1/$AGRO_CONFIG_FILE"
1683
1641
  }
1684
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
+
1685
1651
  agro_env_value() {
1686
1652
  local key="AGRO_$1"
1687
1653
  printf '%s\\n' "\${!key:-}"
@@ -1786,9 +1752,6 @@ printf 'free\\n'
1786
1752
  exit 0
1787
1753
  `;
1788
1754
 
1789
- // agro-asset:/home/runner/work/agro/agro/.devcontainer/openshell-policy.yaml
1790
- var openshell_policy_default = "version: 1\n\nfilesystem_policy:\n include_workdir: true\n read_only:\n - /bin\n - /usr\n - /lib\n - /etc\n - /opt\n - /proc\n - /var/log\n - /dev/urandom\n read_write:\n - /home/sandbox\n - /tmp\n - /dev/null\n\nprocess:\n run_as_user: sandbox\n run_as_group: sandbox\n\nnetwork_policies:\n github:\n endpoints:\n - host: github.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-write\n - host: api.github.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-write\n - host: release-assets.githubusercontent.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-only\n binaries:\n - path: /usr/bin/git\n - path: /usr/bin/gh\n - path: /usr/bin/curl\n npm:\n endpoints:\n - host: registry.npmjs.org\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-only\n allow_encoded_slash: true\n binaries:\n - path: /usr/local/bin/node\n anthropic:\n endpoints:\n - host: api.anthropic.com\n port: 443\n protocol: rest\n enforcement: enforce\n access: read-write\n binaries:\n - path: /home/sandbox/.local/lib/node_modules/@anthropic-ai/claude-code/bin/*\n";
1791
-
1792
1755
  // src/lib/registry.ts
1793
1756
  var SANDBOX_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
1794
1757
  var DEFAULT_NAME_PREFIX = "agro-sbx-";
@@ -1845,12 +1808,10 @@ function nextDefaultName(run = spawnRunner) {
1845
1808
  if (!taken.has(candidate)) return candidate;
1846
1809
  }
1847
1810
  }
1848
- function openshellFiles(entry) {
1849
- return [{ dest: join4(entry, "openshell-policy.yaml"), body: openshell_policy_default, mode: 420 }];
1850
- }
1851
- function dockerFiles(entry, opts) {
1811
+ function materialize(root, opts = {}) {
1812
+ const entry = resolve5(root);
1852
1813
  const scripts = join4(resolveProjectLayout(entry).controlDir, "scripts");
1853
- return [
1814
+ const files = [
1854
1815
  {
1855
1816
  dest: join4(entry, ".devcontainer", "docker-compose.yml"),
1856
1817
  body: (opts.checkout ?? opts.repo) === void 0 ? docker_compose_image_only_default : docker_compose_default,
@@ -1862,10 +1823,6 @@ function dockerFiles(entry, opts) {
1862
1823
  { dest: join4(scripts, "paths.sh"), body: paths_default, mode: 420 },
1863
1824
  { dest: join4(scripts, "check-host-port.sh"), body: check_host_port_default, mode: 493 }
1864
1825
  ];
1865
- }
1866
- function materialize(root, opts = {}) {
1867
- const entry = resolve5(root);
1868
- const files = entryRuntime(entry) === "openshell" ? openshellFiles(entry) : dockerFiles(entry, opts);
1869
1826
  for (const file of files) {
1870
1827
  const dest = resolve5(file.dest);
1871
1828
  if (!dest.startsWith(entry + sep2)) {
@@ -2024,32 +1981,29 @@ function stripQuotes2(s) {
2024
1981
  // src/commands/lifecycle.ts
2025
1982
  import { existsSync as existsSync10, mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
2026
1983
  import { tmpdir } from "node:os";
2027
- import { basename as basename6, join as join6, sep as sep3 } from "node:path";
2028
-
2029
- // src/lib/execution/index.ts
2030
- import { basename as basename4, resolve as resolve7 } from "node:path";
2031
-
2032
- // src/lib/version.ts
2033
- var AGRO_VERSION = true ? "0.17.0-rc.1" : "0.0.0-dev";
2034
- var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2035
- var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2036
- function parseReleaseVersion(value) {
2037
- const bare = value.startsWith("v") ? value.slice(1) : value;
2038
- return RELEASE_VERSION.test(bare) ? bare : void 0;
2039
- }
2040
- function officialImageRef(version) {
2041
- return `${OFFICIAL_IMAGE}:${RELEASE_VERSION.test(version) ? version : "latest"}`;
2042
- }
1984
+ import { basename as basename4, join as join5, sep as sep3 } from "node:path";
2043
1985
 
2044
1986
  // src/lib/execution/detect.ts
2045
1987
  import { existsSync as existsSync9 } from "node:fs";
2046
- var SANDBOX_MARKER_FILE = "/.dockerenv";
2047
- var EXECUTION_TARGET_ENV = "AGRO_EXECUTION_TARGET";
2048
- function runningInsideSandbox(env = process.env, fileExists = existsSync9) {
1988
+ var SANDBOX_MARKER_FILE = "/etc/agro/sandbox";
1989
+ var DOCKERENV_FILE = "/.dockerenv";
1990
+ function detectSandbox(env = process.env, fileExists = existsSync9) {
2049
1991
  const override = agroEnvValue(env, "EXECUTION_TARGET");
2050
- if (override === "local") return true;
2051
- if (override === "docker-compose") return false;
2052
- return fileExists(SANDBOX_MARKER_FILE) && (env.SANDBOX_NAME ?? "") !== "";
1992
+ if (override === "local") return { inside: true, rule: "override" };
1993
+ if (override === "docker-compose") return { inside: false, rule: "override" };
1994
+ if (fileExists(SANDBOX_MARKER_FILE)) return { inside: true, rule: "marker" };
1995
+ if (fileExists(DOCKERENV_FILE) && (env.SANDBOX_NAME ?? "") !== "") {
1996
+ return { inside: true, rule: "fallback" };
1997
+ }
1998
+ return { inside: false, rule: "none" };
1999
+ }
2000
+ function runningInsideSandbox(env = process.env, fileExists = existsSync9) {
2001
+ return detectSandbox(env, fileExists).inside;
2002
+ }
2003
+ function sandboxFallbackWarning(env = process.env, fileExists = existsSync9) {
2004
+ if (detectSandbox(env, fileExists).rule !== "fallback") return void 0;
2005
+ return `agro: warning: ${SANDBOX_MARKER_FILE} is missing; detected the sandbox from ${DOCKERENV_FILE} and SANDBOX_NAME. Upgrade the sandbox image.
2006
+ `;
2053
2007
  }
2054
2008
 
2055
2009
  // src/lib/execution/docker-compose-target.ts
@@ -2270,243 +2224,6 @@ function defaultIdentity() {
2270
2224
  return { name: info2.username, uid: info2.uid };
2271
2225
  }
2272
2226
 
2273
- // src/lib/execution/openshell-target.ts
2274
- import { join as join5 } from "node:path";
2275
- var OPENSHELL_BIN = "openshell";
2276
- var OPENSHELL_POLICY_FILE = "openshell-policy.yaml";
2277
- var OPENSHELL_MAIN = "/usr/local/bin/openshell-main.sh";
2278
- var OPENSHELL_WORKDIR = "/home/sandbox/harness";
2279
- var OPENSHELL_USER = "sandbox";
2280
- var OPENSHELL_PINNED_VERSION = "v0.1.2";
2281
- var OPENSHELL_LOCAL_GATEWAY = "https://127.0.0.1:17670";
2282
- var OPENSHELL_INSTALL_URL = "https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh";
2283
- var LOGIN_SHELLS = /* @__PURE__ */ new Set(["zsh", "bash"]);
2284
- var NOT_FOUND = /sandbox (?:'[^']*' )?not found/;
2285
- var OPENSHELL_PHASES = [
2286
- "Unspecified",
2287
- "Provisioning",
2288
- "Ready",
2289
- "Error",
2290
- "Deleting",
2291
- "Unknown",
2292
- "Stopping",
2293
- "Stopped",
2294
- "Starting",
2295
- "Completed"
2296
- ];
2297
- var OPENSHELL_PHASE_STATUS = Object.freeze({
2298
- Ready: "ready",
2299
- Provisioning: "starting",
2300
- Starting: "starting",
2301
- Stopping: "stopped",
2302
- Stopped: "stopped",
2303
- Deleting: "stopped",
2304
- Completed: "stopped",
2305
- Error: "failed",
2306
- Unknown: "failed",
2307
- Unspecified: "failed"
2308
- });
2309
- function createArgv(opts) {
2310
- return [
2311
- "sandbox",
2312
- "create",
2313
- "--name",
2314
- opts.name,
2315
- "--from",
2316
- opts.image,
2317
- "--policy",
2318
- join5(opts.entryRoot, OPENSHELL_POLICY_FILE),
2319
- "--env",
2320
- `${EXECUTION_TARGET_ENV}=local`,
2321
- "--env",
2322
- `SANDBOX_NAME=${opts.name}`,
2323
- "--detach",
2324
- "--",
2325
- OPENSHELL_MAIN
2326
- ];
2327
- }
2328
- function attachArgv(name, request = { argv: ["zsh"] }) {
2329
- return [
2330
- "sandbox",
2331
- "exec",
2332
- "-n",
2333
- name,
2334
- "--tty",
2335
- "--workdir",
2336
- request.cwd ?? OPENSHELL_WORKDIR,
2337
- ...envFlags(request.env),
2338
- "--",
2339
- ...asLoginShell(request.argv)
2340
- ];
2341
- }
2342
- function execArgv(name, request) {
2343
- return [
2344
- "sandbox",
2345
- "exec",
2346
- "-n",
2347
- name,
2348
- ...request.cwd !== void 0 ? ["--workdir", request.cwd] : [],
2349
- ...envFlags(request.env),
2350
- ...request.timeoutMs !== void 0 ? ["--timeout", String(Math.ceil(request.timeoutMs / 1e3))] : [],
2351
- "--",
2352
- ...request.argv
2353
- ];
2354
- }
2355
- function getArgv(name) {
2356
- return ["sandbox", "get", name, "-o", "json"];
2357
- }
2358
- function deleteArgv(name) {
2359
- return ["sandbox", "delete", name];
2360
- }
2361
- function parseOpenShellPhase(json) {
2362
- let parsed;
2363
- try {
2364
- parsed = JSON.parse(json);
2365
- } catch {
2366
- return "Unknown";
2367
- }
2368
- const phase = typeof parsed === "object" && parsed !== null ? parsed.phase : void 0;
2369
- return OPENSHELL_PHASES.find((known) => known === phase) ?? "Unknown";
2370
- }
2371
- function openshellSandboxMissing(output) {
2372
- return NOT_FOUND.test(output);
2373
- }
2374
- var OPENSHELL_VERB_HINTS = Object.freeze({
2375
- stop: (name) => `openshell sandbox stop ${name}`,
2376
- restart: (name) => `openshell sandbox stop ${name} && openshell sandbox start ${name}`,
2377
- logs: (name) => `openshell logs ${name}`,
2378
- ps: (name) => `openshell sandbox get ${name}`,
2379
- config: (name) => `openshell policy get ${name}`
2380
- });
2381
- function openshellInstallHint() {
2382
- return [
2383
- "install the OpenShell CLI on the host, then run the command again:",
2384
- ` curl -LsSf ${OPENSHELL_INSTALL_URL} | OPENSHELL_VERSION=${OPENSHELL_PINNED_VERSION} sh`,
2385
- "review-first alternative \u2014 download the script, inspect it, then run it:",
2386
- ` curl -LsSf -o openshell-install.sh ${OPENSHELL_INSTALL_URL}`,
2387
- " less openshell-install.sh",
2388
- ` OPENSHELL_VERSION=${OPENSHELL_PINNED_VERSION} sh openshell-install.sh`
2389
- ].join("\n");
2390
- }
2391
- function openshellGatewayHint() {
2392
- return [
2393
- "the OpenShell gateway is not connected; start the local gateway and register it:",
2394
- " systemctl --user restart openshell-gateway",
2395
- ` openshell gateway add ${OPENSHELL_LOCAL_GATEWAY} --local --name openshell`,
2396
- "then confirm with: openshell status"
2397
- ].join("\n");
2398
- }
2399
- function openshellCleanupHint(name) {
2400
- return `the OpenShell gateway can keep a failed sandbox record in the Error phase; if ${name} is not another sandbox, remove it with: openshell sandbox delete ${name}`;
2401
- }
2402
- function openshellPreflight(run) {
2403
- const version = run(OPENSHELL_BIN, ["--version"], { stdio: "capture" });
2404
- if (version.error?.code === "ENOENT") {
2405
- return { ok: false, reason: "missing-binary", hint: openshellInstallHint() };
2406
- }
2407
- assertSpawned(version, `${OPENSHELL_BIN} --version`);
2408
- const status = run(OPENSHELL_BIN, ["status", "-o", "json"], { stdio: "capture" });
2409
- assertSpawned(status, `${OPENSHELL_BIN} status`);
2410
- if (status.status !== 0 || !gatewayConnected(status.stdout ?? "")) {
2411
- return { ok: false, reason: "gateway-unreachable", hint: openshellGatewayHint() };
2412
- }
2413
- return { ok: true };
2414
- }
2415
- var OpenShellExecutionTarget = class {
2416
- kind = "openshell";
2417
- contractVersion = 1;
2418
- workspace;
2419
- name;
2420
- entryRoot;
2421
- image;
2422
- run;
2423
- env;
2424
- constructor(opts) {
2425
- this.name = opts.name;
2426
- this.entryRoot = opts.entryRoot;
2427
- this.image = opts.image;
2428
- this.run = opts.run ?? spawnRunner;
2429
- this.env = opts.env;
2430
- this.workspace = { hostRoot: opts.entryRoot, targetRoot: OPENSHELL_WORKDIR };
2431
- }
2432
- async provision() {
2433
- const argv = createArgv({ name: this.name, image: this.image, entryRoot: this.entryRoot });
2434
- this.runInherited(argv, "sandbox create");
2435
- }
2436
- async destroy() {
2437
- this.runInherited(deleteArgv(this.name), "sandbox delete");
2438
- }
2439
- async status() {
2440
- const r = this.run(OPENSHELL_BIN, getArgv(this.name), { stdio: "capture", ...this.childEnv() });
2441
- assertSpawned(r, this.what("sandbox get"));
2442
- if (r.status !== 0) {
2443
- if (openshellSandboxMissing(`${r.stdout ?? ""}
2444
- ${r.stderr ?? ""}`)) return "absent";
2445
- throw new ExecutionExitError(this.what("sandbox get"), r.status ?? 1);
2446
- }
2447
- return OPENSHELL_PHASE_STATUS[parseOpenShellPhase(r.stdout ?? "")];
2448
- }
2449
- async capabilities() {
2450
- return /* @__PURE__ */ new Set(["exec", "pty"]);
2451
- }
2452
- async exec(request) {
2453
- requireSandboxUser(request);
2454
- const inherit = request.stdio === "inherit";
2455
- const r = this.run(OPENSHELL_BIN, execArgv(this.name, request), {
2456
- stdio: inherit ? "inherit" : "capture",
2457
- ...this.childEnv()
2458
- });
2459
- assertSpawned(r, this.what("sandbox exec"));
2460
- return {
2461
- exitCode: r.status ?? 1,
2462
- stdout: r.stdout ?? "",
2463
- stderr: r.stderr ?? ""
2464
- };
2465
- }
2466
- attach(request) {
2467
- requireSandboxUser(request);
2468
- const r = this.run(OPENSHELL_BIN, attachArgv(this.name, request), { stdio: "inherit", ...this.childEnv() });
2469
- assertSpawned(r, this.what("sandbox exec"));
2470
- return r.status ?? 1;
2471
- }
2472
- describe() {
2473
- return `openshell sandbox ${this.name} (entry ${this.entryRoot})`;
2474
- }
2475
- runInherited(argv, verb) {
2476
- const r = this.run(OPENSHELL_BIN, argv, { stdio: "inherit", ...this.childEnv() });
2477
- assertSpawned(r, this.what(verb));
2478
- const code = r.status ?? 1;
2479
- if (code !== 0) throw new ExecutionExitError(this.what(verb), code);
2480
- }
2481
- childEnv() {
2482
- return this.env ? { env: this.env } : {};
2483
- }
2484
- what(verb) {
2485
- return `${OPENSHELL_BIN} ${verb} ${this.name}`;
2486
- }
2487
- };
2488
- function asLoginShell(argv) {
2489
- return argv.length === 1 && LOGIN_SHELLS.has(argv[0]) ? [argv[0], "-l"] : [...argv];
2490
- }
2491
- function envFlags(env) {
2492
- return Object.entries(env ?? {}).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
2493
- }
2494
- function gatewayConnected(json) {
2495
- try {
2496
- const parsed = JSON.parse(json);
2497
- return typeof parsed === "object" && parsed !== null && parsed.status === "connected";
2498
- } catch {
2499
- return false;
2500
- }
2501
- }
2502
- function requireSandboxUser(request) {
2503
- if (request.user !== void 0 && request.user !== OPENSHELL_USER) {
2504
- throw new Error(
2505
- `the openshell runtime runs every command as "${OPENSHELL_USER}" and cannot run as "${request.user}"`
2506
- );
2507
- }
2508
- }
2509
-
2510
2227
  // src/lib/execution/status.ts
2511
2228
  var RUNTIME_ABSENT_STATUS = "no container runtime";
2512
2229
  async function resolveTargetStatus(target) {
@@ -2530,23 +2247,19 @@ function resolveExecutionTarget(opts) {
2530
2247
  ...opts.env ? { env: opts.env } : {}
2531
2248
  });
2532
2249
  }
2533
- if (entryRuntime(opts.projectRoot) === "openshell") {
2534
- const config = readAgroConfig(agroConfigPath(opts.projectRoot));
2535
- return new OpenShellExecutionTarget({
2536
- name: opts.container ?? nonEmpty(config.name) ?? basename4(resolve7(opts.projectRoot)),
2537
- entryRoot: opts.projectRoot,
2538
- image: openshellImage(config),
2539
- ...opts.run ? { run: opts.run } : {},
2540
- ...opts.env ? { env: opts.env } : {}
2541
- });
2542
- }
2543
2250
  return new DockerComposeExecutionTarget(opts);
2544
2251
  }
2545
- function openshellImage(config) {
2546
- return nonEmpty(config.image?.ref) ?? officialImageRef(AGRO_VERSION);
2252
+
2253
+ // src/lib/version.ts
2254
+ var AGRO_VERSION = true ? "0.18.0" : "0.0.0-dev";
2255
+ var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2256
+ var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2257
+ function parseReleaseVersion(value) {
2258
+ const bare = value.startsWith("v") ? value.slice(1) : value;
2259
+ return RELEASE_VERSION.test(bare) ? bare : void 0;
2547
2260
  }
2548
- function nonEmpty(value) {
2549
- return value === void 0 || value === "" ? void 0 : value;
2261
+ function officialImageRef(version) {
2262
+ return `${OFFICIAL_IMAGE}:${RELEASE_VERSION.test(version) ? version : "latest"}`;
2550
2263
  }
2551
2264
 
2552
2265
  // src/lib/config-render.ts
@@ -2612,103 +2325,13 @@ function renderComposeEnv(config) {
2612
2325
  `;
2613
2326
  }
2614
2327
 
2615
- // src/lib/runtimes/verbs.ts
2616
- import { basename as basename5, resolve as resolve8 } from "node:path";
2617
- var viaCompose = Object.freeze({ action: "route", via: "compose" });
2618
- var viaTarget = Object.freeze({ action: "route", via: "target" });
2619
- function refuse(hint) {
2620
- return Object.freeze({ action: "refuse", hint });
2621
- }
2622
- var LIFECYCLE_COMMANDS = Object.freeze({
2623
- shell: "shell",
2624
- stop: "stop",
2625
- restart: "restart",
2626
- logs: "logs",
2627
- ps: "ps",
2628
- destroy: "destroy",
2629
- config: "compose config",
2630
- upgrade: "sandbox upgrade"
2631
- });
2632
- var DOCKER_VERBS = Object.freeze({
2633
- shell: viaTarget,
2634
- stop: viaCompose,
2635
- restart: viaCompose,
2636
- logs: viaCompose,
2637
- ps: viaCompose,
2638
- destroy: viaCompose,
2639
- config: viaCompose,
2640
- upgrade: viaTarget
2641
- });
2642
- var OPENSHELL_VERBS = Object.freeze({
2643
- shell: viaTarget,
2644
- stop: refuse(OPENSHELL_VERB_HINTS.stop),
2645
- restart: refuse(OPENSHELL_VERB_HINTS.restart),
2646
- logs: refuse(OPENSHELL_VERB_HINTS.logs),
2647
- ps: refuse(OPENSHELL_VERB_HINTS.ps),
2648
- destroy: viaTarget,
2649
- config: refuse(OPENSHELL_VERB_HINTS.config),
2650
- upgrade: refuse(
2651
- (name) => `${activeBin()} destroy ${name}, then ${activeBin()} sandbox install openshell --name ${name} --image=<ref>`
2652
- )
2653
- });
2654
- var RUNTIME_VERBS = Object.freeze({
2655
- docker: DOCKER_VERBS,
2656
- openshell: OPENSHELL_VERBS
2657
- });
2658
- function configuredEntryName(root) {
2659
- try {
2660
- const name = readAgroConfig(agroConfigPath(root)).name;
2661
- return name === "" ? void 0 : name;
2662
- } catch {
2663
- return void 0;
2664
- }
2665
- }
2666
- function entryDeclaredName(root) {
2667
- return configuredEntryName(root) ?? basename5(resolve8(root));
2668
- }
2669
- var RUNTIME_NAMING = Object.freeze({
2670
- docker: Object.freeze({
2671
- entryName: () => void 0,
2672
- shellFailure: (bin, name) => `container \`${name}\` not running? start it with \`${bin} sandbox install docker\``
2673
- }),
2674
- openshell: Object.freeze({
2675
- entryName: entryDeclaredName,
2676
- shellFailure: (bin, name) => `sandbox \`${name}\` did not accept the shell; check its status with \`${bin} sandbox list\``
2677
- })
2678
- });
2679
- function entrySandboxName(root) {
2680
- return RUNTIME_NAMING[entryRuntime(root)].entryName(root);
2681
- }
2682
- function shellFailureHint(runtime, bin, name) {
2683
- return RUNTIME_NAMING[runtime].shellFailure(bin, name);
2684
- }
2685
- var RuntimeUnsupportedError = class extends Error {
2686
- runtime;
2687
- verb;
2688
- hint;
2689
- constructor(runtime, verb, hint) {
2690
- const command = LIFECYCLE_COMMANDS[verb];
2691
- super(`${activeBin()} ${command}: the ${runtime} runtime does not support ${command}; run: ${hint}`);
2692
- this.name = "RuntimeUnsupportedError";
2693
- this.runtime = runtime;
2694
- this.verb = verb;
2695
- this.hint = hint;
2696
- }
2697
- };
2698
- function routeVerb(root, verb, name) {
2699
- const runtime = entryRuntime(root);
2700
- const policy = RUNTIME_VERBS[runtime][verb];
2701
- if (policy.action === "refuse") throw new RuntimeUnsupportedError(runtime, verb, policy.hint(name));
2702
- return { runtime, via: policy.via };
2703
- }
2704
-
2705
2328
  // src/commands/lifecycle.ts
2706
2329
  var COMPOSE_ENV_DIR_PREFIX = "oh-compose-env-";
2707
2330
  function composeEnvDir() {
2708
- return mkdtempSync(join6(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
2331
+ return mkdtempSync(join5(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
2709
2332
  }
2710
2333
  function writeComposeEnv(root, dir) {
2711
- const file = join6(dir, "compose.env");
2334
+ const file = join5(dir, "compose.env");
2712
2335
  writeFileSync5(file, renderComposeEnv(readAgroConfig(agroConfigPath(root))), {
2713
2336
  mode: 384,
2714
2337
  encoding: "utf8"
@@ -2766,7 +2389,7 @@ async function maybePromptDockerSocket(root, io, bin) {
2766
2389
  if (dockerSocketConfigured(root)) return;
2767
2390
  const interactive = process.stdin.isTTY === true || io.ask !== void 0;
2768
2391
  if (!interactive) return;
2769
- const envDir = join6(root, ".devcontainer");
2392
+ const envDir = join5(root, ".devcontainer");
2770
2393
  if (!existsSync10(envDir)) return;
2771
2394
  const askFn = io.ask ?? ask;
2772
2395
  const answer = (await askFn(
@@ -2847,27 +2470,25 @@ async function runSandbox(opts, io) {
2847
2470
  function configuredContainerName(root) {
2848
2471
  return fromProcessEnv("SANDBOX_NAME") ?? configuredString(root, "name");
2849
2472
  }
2850
- function sandboxName(root) {
2851
- return entrySandboxName(root) ?? configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2852
- }
2853
2473
  function runShell(opts, io) {
2854
2474
  const run = opts.run ?? spawnRunner;
2855
2475
  const root = sandboxRoot(opts);
2856
- const name = sandboxName(root);
2857
- const { runtime } = routeVerb(root, "shell", name);
2476
+ const name = configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2858
2477
  const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
2859
2478
  let code;
2860
2479
  try {
2861
2480
  code = target.attach({ argv: ["zsh"], user: "sandbox" });
2862
2481
  } catch (err) {
2863
2482
  if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
2864
- throw new Error(`${runtime} is required for \`${opts.bin} shell\` but was not found on PATH`);
2483
+ throw new Error(`docker is required for \`${opts.bin} shell\` but was not found on PATH`);
2865
2484
  }
2866
2485
  throw err;
2867
2486
  }
2868
2487
  if (code !== 0) {
2869
- io.stderr(`${shellFailureHint(runtime, opts.bin, name)}
2870
- `);
2488
+ io.stderr(
2489
+ `container \`${name}\` not running? start it with \`${opts.bin} sandbox install docker\`
2490
+ `
2491
+ );
2871
2492
  }
2872
2493
  return code;
2873
2494
  }
@@ -2884,7 +2505,6 @@ function composeVerbs() {
2884
2505
  function runComposeVerb(verb, opts, extra = []) {
2885
2506
  const run = opts.run ?? spawnRunner;
2886
2507
  const root = sandboxRoot(opts);
2887
- routeVerb(root, verb, sandboxName(root));
2888
2508
  const script = requireLifecycleScript(root, "docker-compose.sh");
2889
2509
  return withComposeEnvFile(root, (extraArgs) => {
2890
2510
  const r = run("bash", [script, ...extraArgs, ...COMPOSE_VERBS[verb], ...extra], {
@@ -2897,7 +2517,6 @@ function runComposeVerb(verb, opts, extra = []) {
2897
2517
  function runComposeConfig(opts, extra = []) {
2898
2518
  const run = opts.run ?? spawnRunner;
2899
2519
  const root = sandboxRoot(opts);
2900
- routeVerb(root, "config", sandboxName(root));
2901
2520
  const script = requireLifecycleScript(root, "docker-compose.sh");
2902
2521
  return withComposeEnvFile(root, (extraArgs) => {
2903
2522
  const r = run("bash", [script, ...extraArgs, "config", ...extra], { stdio: "inherit" });
@@ -2908,7 +2527,7 @@ function runComposeConfig(opts, extra = []) {
2908
2527
  function namedVolumes(root) {
2909
2528
  let text;
2910
2529
  try {
2911
- text = readFileSync7(join6(root, ".devcontainer", "docker-compose.yml"), "utf8");
2530
+ text = readFileSync7(join5(root, ".devcontainer", "docker-compose.yml"), "utf8");
2912
2531
  } catch {
2913
2532
  return [];
2914
2533
  }
@@ -2924,23 +2543,11 @@ function namedVolumes(root) {
2924
2543
  return names;
2925
2544
  }
2926
2545
  function destroyConfirmationPhrase(root) {
2927
- return sandboxName(root);
2928
- }
2929
- async function destroyThroughTarget(root, name, run) {
2930
- const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
2931
- if (target.destroy === void 0) throw new Error(`${target.describe()} cannot destroy its sandbox`);
2932
- try {
2933
- await target.destroy();
2934
- return 0;
2935
- } catch (err) {
2936
- if (err instanceof ExecutionExitError) return err.exitCode;
2937
- throw err;
2938
- }
2546
+ return configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2939
2547
  }
2940
2548
  async function runDestroy(opts, io) {
2941
2549
  const root = sandboxRoot(opts);
2942
2550
  const name = destroyConfirmationPhrase(root);
2943
- const { via } = routeVerb(root, "destroy", name);
2944
2551
  if (opts.yes !== true) {
2945
2552
  const interactive = process.stdin.isTTY === true || io.ask !== void 0;
2946
2553
  if (!interactive) {
@@ -2955,14 +2562,7 @@ async function runDestroy(opts, io) {
2955
2562
  ${bold(`${opts.bin} destroy \u2014 ${name}`)}
2956
2563
 
2957
2564
  `);
2958
- if (via === "target") {
2959
- io.stdout(
2960
- `This deletes the sandbox \`${name}\` and everything inside it \u2014 every agent CLI
2961
- login, the gh CLI token, and the SSH keys. Sign-in starts over.
2962
-
2963
- `
2964
- );
2965
- } else if (homePath !== void 0 && homePath !== "") {
2565
+ if (homePath !== void 0 && homePath !== "") {
2966
2566
  io.stdout("`docker compose down -v` removes the containers.\n");
2967
2567
  io.stdout(
2968
2568
  `This sandbox keeps its home on a host bind at ${homePath}, which \`down -v\`
@@ -2993,7 +2593,7 @@ stay there. Delete that directory yourself if you also want them gone.
2993
2593
  return 1;
2994
2594
  }
2995
2595
  }
2996
- const code = via === "target" ? await destroyThroughTarget(root, name, opts.run ?? spawnRunner) : runComposeVerb("destroy", { ...opts, name: basename6(root) });
2596
+ const code = runComposeVerb("destroy", { ...opts, name: basename4(root) });
2997
2597
  if (code === 0 && root.startsWith(registryRoot() + sep3)) {
2998
2598
  rmSync(root, { recursive: true, force: true });
2999
2599
  io.stdout(`removed the sandbox entry ${root}
@@ -3281,14 +2881,15 @@ function sameRemote(url, owner, name) {
3281
2881
  }
3282
2882
 
3283
2883
  // src/commands/secret.ts
2884
+ import { spawnSync as spawnSync3 } from "node:child_process";
3284
2885
  import { existsSync as existsSync12 } from "node:fs";
3285
- import { join as join8 } from "node:path";
2886
+ import { join as join7 } from "node:path";
3286
2887
 
3287
2888
  // src/lib/gitignore.ts
3288
2889
  import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
3289
- import { dirname as dirname5, join as join7 } from "node:path";
2890
+ import { dirname as dirname5, join as join6 } from "node:path";
3290
2891
  function appendGitignoreLines(root, lines) {
3291
- const target = join7(root, ".gitignore");
2892
+ const target = join6(root, ".gitignore");
3292
2893
  const existing = existsSync11(target) ? readFileSync8(target, "utf8") : "";
3293
2894
  const present = new Set(existing.split("\n").map((line) => line.trimEnd()));
3294
2895
  const added = [];
@@ -3312,8 +2913,16 @@ function appendGitignoreLines(root, lines) {
3312
2913
  function secretsRoot(opts) {
3313
2914
  return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
3314
2915
  }
2916
+ function isGitIgnored(root, path4) {
2917
+ const result = spawnSync3("git", ["check-ignore", "-q", "--no-index", path4], {
2918
+ cwd: root,
2919
+ stdio: "ignore"
2920
+ });
2921
+ return result.status === 0;
2922
+ }
3315
2923
  function ignoreSecretsFile(root) {
3316
- if (!existsSync12(join8(root, ".git"))) return;
2924
+ if (!existsSync12(join7(root, ".git"))) return;
2925
+ if (isGitIgnored(root, ".env")) return;
3317
2926
  appendGitignoreLines(root, [".env"]);
3318
2927
  }
3319
2928
  function secretKeyList() {
@@ -3371,10 +2980,10 @@ async function runSecretList(opts, io) {
3371
2980
  // src/commands/langfuse.ts
3372
2981
  import { copyFileSync as copyFileSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync10, rmSync as rmSync2, unlinkSync as unlinkSync4 } from "node:fs";
3373
2982
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
3374
- import { dirname as dirname7, join as join11, relative } from "node:path";
2983
+ import { dirname as dirname7, join as join10, relative } from "node:path";
3375
2984
 
3376
2985
  // src/lib/tracing/harness-writers.ts
3377
- import { join as join9 } from "node:path";
2986
+ import { join as join8 } from "node:path";
3378
2987
 
3379
2988
  // src/lib/tracing/writer.ts
3380
2989
  import { chmodSync, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
@@ -3419,13 +3028,13 @@ function isPlainObject(value) {
3419
3028
  // src/lib/tracing/harness-writers.ts
3420
3029
  var CODEX_TRACING_TAGS = ["codex"];
3421
3030
  function claudeCodeSettingsPath(home) {
3422
- return join9(home, ".claude", "settings.json");
3031
+ return join8(home, ".claude", "settings.json");
3423
3032
  }
3424
3033
  function piTracingConfigPath(home) {
3425
- return join9(home, ".pi", "agent", "langfuse.json");
3034
+ return join8(home, ".pi", "agent", "langfuse.json");
3426
3035
  }
3427
3036
  function codexTracingConfigPath(home) {
3428
- return join9(home, ".codex", "langfuse.json");
3037
+ return join8(home, ".codex", "langfuse.json");
3429
3038
  }
3430
3039
  var claudeCodeTracingWriter = (home, settings) => {
3431
3040
  const path4 = claudeCodeSettingsPath(home);
@@ -3597,6 +3206,21 @@ var HARNESS_CATALOG = [
3597
3206
  kind: "installable",
3598
3207
  bypassPermissionsFlag: "--dangerously-skip-permissions"
3599
3208
  },
3209
+ {
3210
+ id: "fx",
3211
+ title: "fx",
3212
+ binary: "fx",
3213
+ installArgv: [
3214
+ "bash",
3215
+ "-lc",
3216
+ `set -o pipefail; curl -fsSL https://fx.sh/setup.sh | FX_INSTALL_DIR="${HARNESS_PREFIX_TOKEN}/bin" bash -s v0.0.13`
3217
+ ],
3218
+ installUser: "sandbox",
3219
+ verifyArgv: ["fx", "--version"],
3220
+ uninstallArgv: ["rm", "-f", `${HARNESS_PREFIX_TOKEN}/bin/fx`],
3221
+ docsPath: "docs/harnesses/fx.md",
3222
+ kind: "installable"
3223
+ },
3600
3224
  {
3601
3225
  id: "t3code",
3602
3226
  title: "T3 Code",
@@ -3635,7 +3259,7 @@ function harnessBinPath(prefix) {
3635
3259
  }
3636
3260
 
3637
3261
  // src/lib/tracing/providers/langfuse.ts
3638
- import { join as join10 } from "node:path";
3262
+ import { join as join9 } from "node:path";
3639
3263
  var LANGFUSE_FRAGMENT_KEYS = [
3640
3264
  "LANGFUSE_PUBLIC_KEY",
3641
3265
  "LANGFUSE_SECRET_KEY",
@@ -3643,7 +3267,7 @@ var LANGFUSE_FRAGMENT_KEYS = [
3643
3267
  "LANGFUSE_TRACING_ENVIRONMENT"
3644
3268
  ];
3645
3269
  function langfuseFragmentPath(home) {
3646
- return join10(home, ".config", "agro", "langfuse.env");
3270
+ return join9(home, ".config", "agro", "langfuse.env");
3647
3271
  }
3648
3272
  function loadLangfuseCredentials(root) {
3649
3273
  const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
@@ -3807,7 +3431,7 @@ function displayPath(home, path4) {
3807
3431
  return rel.startsWith("..") ? path4 : `~/${rel}`;
3808
3432
  }
3809
3433
  function withTempHome(fn) {
3810
- const dir = mkdtempSync2(join11(tmpdir2(), "agro-langfuse-"));
3434
+ const dir = mkdtempSync2(join10(tmpdir2(), "agro-langfuse-"));
3811
3435
  try {
3812
3436
  return fn(dir);
3813
3437
  } finally {
@@ -3816,10 +3440,10 @@ function withTempHome(fn) {
3816
3440
  }
3817
3441
  function probeGeneratedFile(home, render) {
3818
3442
  const rel = withTempHome((scratch) => relative(scratch, render(scratch).path));
3819
- const path4 = join11(home, rel);
3443
+ const path4 = join10(home, rel);
3820
3444
  if (!existsSync14(path4)) return { path: path4, state: "missing" };
3821
3445
  const outcome = withTempHome((scratch) => {
3822
- const copy = join11(scratch, rel);
3446
+ const copy = join10(scratch, rel);
3823
3447
  mkdirSync6(dirname7(copy), { recursive: true });
3824
3448
  copyFileSync2(path4, copy);
3825
3449
  return render(scratch).outcome;
@@ -3882,7 +3506,7 @@ async function runLangfuseApply(opts, io) {
3882
3506
  return renderAll(ctx, resolved, io, opts.bin, "apply");
3883
3507
  }
3884
3508
  function zshenvWarning(home) {
3885
- const zshenv = join11(home, ".zshenv");
3509
+ const zshenv = join10(home, ".zshenv");
3886
3510
  if (!existsSync14(zshenv)) return void 0;
3887
3511
  if (readFileSync10(zshenv, "utf8").includes("langfuse.env")) return void 0;
3888
3512
  return `~/.zshenv exists but does not source ${LANGFUSE_FRAGMENT_LABEL} \u2014 non-interactive shells will carry no Langfuse credentials; add the source block from .agro/install/.zshenv
@@ -4163,18 +3787,45 @@ async function runLangfuseSetup(opts, io) {
4163
3787
  // src/commands/sandbox.ts
4164
3788
  import { existsSync as existsSync16, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
4165
3789
  import { tmpdir as tmpdir3 } from "node:os";
4166
- import { join as join13, resolve as resolve9 } from "node:path";
3790
+ import { join as join12, resolve as resolve7 } from "node:path";
3791
+
3792
+ // src/lib/runtimes/catalog.ts
3793
+ var RUNTIME_CATALOG = Object.freeze([
3794
+ Object.freeze({
3795
+ id: "docker",
3796
+ title: "Docker container",
3797
+ tier: "container",
3798
+ state: "active",
3799
+ provisionable: true,
3800
+ docsPath: "docs/runtimes/overview.md"
3801
+ }),
3802
+ Object.freeze({
3803
+ id: "microsandbox",
3804
+ title: "MicroSandbox",
3805
+ tier: "microvm",
3806
+ state: "planned",
3807
+ provisionable: false,
3808
+ 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\`.`,
3809
+ docsPath: "docs/runtimes/microsandbox.md"
3810
+ })
3811
+ ]);
3812
+ function findRuntime(id) {
3813
+ return RUNTIME_CATALOG.find((r) => r.id === id);
3814
+ }
3815
+ function runtimeIds() {
3816
+ return RUNTIME_CATALOG.map((r) => r.id);
3817
+ }
4167
3818
 
4168
3819
  // src/lib/star-prompt.ts
4169
3820
  import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
4170
- import { join as join12 } from "node:path";
3821
+ import { join as join11 } from "node:path";
4171
3822
  var STAR_PROMPT_LINE = "\u2B50 If AGRO helps, star https://github.com/mifunedev/agro";
4172
3823
  var MARKER = "star-prompt-shown";
4173
3824
  function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTTY === true, stateHome = resolveUserStateHome(env)) {
4174
3825
  if (env.AGRO_NO_STAR_PROMPT === "1") return;
4175
3826
  if (env.CI !== void 0 && env.CI !== "") return;
4176
3827
  if (!isTTY) return;
4177
- const marker = join12(stateHome, MARKER);
3828
+ const marker = join11(stateHome, MARKER);
4178
3829
  if (existsSync15(marker)) return;
4179
3830
  io.stdout(`${STAR_PROMPT_LINE}
4180
3831
  `);
@@ -4189,7 +3840,7 @@ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTT
4189
3840
  // src/commands/sandbox-list.ts
4190
3841
  async function entryStatus(root, name, run) {
4191
3842
  try {
4192
- const target = resolveExecutionTarget({ projectRoot: root, container: entrySandboxName(root) ?? name, run });
3843
+ const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
4193
3844
  return await target.status();
4194
3845
  } catch {
4195
3846
  return "unknown";
@@ -4251,10 +3902,10 @@ function gitIdentity(run, key) {
4251
3902
  return (result.stdout ?? "").trim();
4252
3903
  }
4253
3904
  function isBuildCapable(repo) {
4254
- return repo !== void 0 && existsSync16(join13(repo, ".devcontainer", "Dockerfile"));
3905
+ return repo !== void 0 && existsSync16(join12(repo, ".devcontainer", "Dockerfile"));
4255
3906
  }
4256
3907
  function prepareHomeMount(value) {
4257
- const path4 = resolve9(value);
3908
+ const path4 = resolve7(value);
4258
3909
  if (existsSync16(path4)) {
4259
3910
  if (!statSync4(path4).isDirectory()) {
4260
3911
  throw new Error(`--home-mount path exists and is not a directory: ${path4}`);
@@ -4298,18 +3949,18 @@ function mergeSettings(...sources) {
4298
3949
  for (const source of present) overlaySettings(merged, source);
4299
3950
  return merged;
4300
3951
  }
4301
- function nonEmpty2(value) {
3952
+ function nonEmpty(value) {
4302
3953
  return value === void 0 || value === "" ? void 0 : value;
4303
3954
  }
4304
- function seedConfig(name, checkout, seed, run, runtime = "docker") {
3955
+ function seedConfig(name, checkout, seed, run) {
4305
3956
  const config = overlaySettings(defaultAgroConfig(name), seed);
4306
3957
  config.name = name;
4307
- config.runtime = runtime;
3958
+ config.runtime = "docker";
4308
3959
  if (checkout !== void 0) config.checkout = checkout;
4309
- config.timezone = nonEmpty2(seed?.timezone) ?? hostTimezone();
3960
+ config.timezone = nonEmpty(seed?.timezone) ?? hostTimezone();
4310
3961
  config.git = {
4311
- userName: nonEmpty2(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
4312
- userEmail: nonEmpty2(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
3962
+ userName: nonEmpty(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
3963
+ userEmail: nonEmpty(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
4313
3964
  };
4314
3965
  config.image = {
4315
3966
  ...config.image,
@@ -4350,7 +4001,7 @@ async function runWizard(config, io) {
4350
4001
  "Host path for /home/sandbox (blank keeps the Docker-managed volume)",
4351
4002
  config.storage?.homePath ?? ""
4352
4003
  );
4353
- if (homePath !== "") config.storage = { ...config.storage, homePath: resolve9(homePath) };
4004
+ if (homePath !== "") config.storage = { ...config.storage, homePath: resolve7(homePath) };
4354
4005
  }
4355
4006
  async function runSandboxInstall(opts, io) {
4356
4007
  const run = opts.run ?? spawnRunner;
@@ -4367,8 +4018,7 @@ async function runSandboxInstall(opts, io) {
4367
4018
  `);
4368
4019
  return 1;
4369
4020
  }
4370
- if (runtime.id === "openshell") return await installOpenShell(opts, io, run);
4371
- const checkout = opts.checkout === void 0 ? void 0 : resolve9(opts.checkout);
4021
+ const checkout = opts.checkout === void 0 ? void 0 : resolve7(opts.checkout);
4372
4022
  if (checkout !== void 0 && !existsSync16(checkout)) {
4373
4023
  io.stderr(`${opts.bin} sandbox install: --checkout directory does not exist: ${checkout}
4374
4024
  `);
@@ -4386,9 +4036,9 @@ async function runSandboxInstall(opts, io) {
4386
4036
  const config = seedConfig(name, checkout, mergeSettings(readEntryConfig(name), repoSeed), run);
4387
4037
  const buildRequested = config.image?.mode === "build" && opts.noBuild !== true && opts.image !== true && opts.imageRef === void 0;
4388
4038
  if (buildRequested && !isBuildCapable(configCheckout(config))) {
4389
- const target = configCheckout(config) ?? resolve9(opts.checkout ?? ".");
4039
+ const target = configCheckout(config) ?? resolve7(opts.checkout ?? ".");
4390
4040
  io.stderr(
4391
- `${opts.bin} sandbox install: image.mode is "build" but ${join13(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
4041
+ `${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
4392
4042
  `
4393
4043
  );
4394
4044
  return 1;
@@ -4432,7 +4082,7 @@ async function runSandboxInstall(opts, io) {
4432
4082
  ...useNoBuild ? { noBuild: true } : {}
4433
4083
  };
4434
4084
  if (opts.printArgv === true) {
4435
- const preview = mkdtempSync3(join13(tmpdir3(), "oh-sandbox-preview-"));
4085
+ const preview = mkdtempSync3(join12(tmpdir3(), "oh-sandbox-preview-"));
4436
4086
  try {
4437
4087
  writeAgroConfig(preview, config);
4438
4088
  materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
@@ -4453,77 +4103,16 @@ async function runSandboxInstall(opts, io) {
4453
4103
  }
4454
4104
  return code;
4455
4105
  }
4456
- async function installOpenShell(opts, io, run) {
4457
- const fail2 = (message) => {
4458
- io.stderr(`${opts.bin} sandbox install: ${message}
4459
- `);
4460
- return 1;
4461
- };
4462
- const unsupported = (what) => fail2(`${what} is not supported on the openshell runtime`);
4463
- if (opts.checkout !== void 0) return unsupported("--checkout");
4464
- if (opts.homeMount !== void 0) return unsupported("--home-mount");
4465
- const name = opts.name ?? nextDefaultName(run);
4466
- try {
4467
- assertSandboxName(name);
4468
- } catch (error) {
4469
- return fail2(error instanceof Error ? error.message : String(error));
4470
- }
4471
- const config = seedConfig(name, void 0, readEntryConfig(name), run, "openshell");
4472
- if (configCheckout(config) !== void 0) return unsupported("checkout");
4473
- if (nonEmpty2(config.storage?.homePath) !== void 0) return unsupported("storage.homePath");
4474
- const imageChosen = opts.image === true || opts.imageRef !== void 0;
4475
- if (config.image?.mode === "build" && !imageChosen) {
4476
- return unsupported('image.mode "build"');
4477
- }
4478
- config.image = {
4479
- ...config.image,
4480
- mode: "image",
4481
- ...opts.imageRef !== void 0 ? { ref: opts.imageRef } : {}
4482
- };
4483
- const root = entryRoot(name);
4484
- const image = openshellImage(config);
4485
- if (opts.printArgv === true) {
4486
- io.stdout(`${[OPENSHELL_BIN, ...createArgv({ name, image, entryRoot: root })].join(" ")}
4487
- `);
4488
- return 0;
4489
- }
4490
- if (runningInsideSandbox()) return fail2(new HostOnlyError(`\`${opts.bin} sandbox\``).message);
4491
- try {
4492
- const preflight = openshellPreflight(run);
4493
- if (!preflight.ok) return fail2(preflight.hint);
4494
- } catch (error) {
4495
- return fail2(error instanceof Error ? error.message : String(error));
4496
- }
4497
- const created = !existsSync16(root);
4498
- mkdirSync8(root, { recursive: true });
4499
- writeAgroConfig(root, config);
4500
- materialize(root);
4501
- try {
4502
- await new OpenShellExecutionTarget({ name, entryRoot: root, image, run }).provision();
4503
- } catch (error) {
4504
- if (created) rmSync3(root, { recursive: true, force: true });
4505
- io.stderr(`${opts.bin} sandbox install: ${error instanceof Error ? error.message : String(error)}
4506
- `);
4507
- if (!(error instanceof ExecutionExitError)) return 1;
4508
- io.stderr(`${opts.bin} sandbox install: ${openshellCleanupHint(name)}
4509
- `);
4510
- return error.exitCode;
4511
- }
4512
- io.stdout(`next: ${opts.bin} shell ${name}
4513
- `);
4514
- maybePrintStarPrompt(io);
4515
- return 0;
4516
- }
4517
4106
 
4518
4107
  // src/services/sandbox-upgrade.ts
4519
4108
  import { closeSync as closeSync2, existsSync as existsSync17, openSync as openSync2, readFileSync as readFileSync11, unlinkSync as unlinkSync5, writeSync } from "node:fs";
4520
- import { join as join14 } from "node:path";
4109
+ import { join as join13 } from "node:path";
4521
4110
  function errorMessage(error) {
4522
4111
  return error instanceof Error ? error.message : String(error);
4523
4112
  }
4524
4113
  function conflictingDotenvImage(root, imageRef) {
4525
- const rootEnv = join14(root, ".env");
4526
- const fallbackEnv = join14(root, ".devcontainer", ".env");
4114
+ const rootEnv = join13(root, ".env");
4115
+ const fallbackEnv = join13(root, ".devcontainer", ".env");
4527
4116
  const file = existsSync17(rootEnv) ? rootEnv : fallbackEnv;
4528
4117
  if (!existsSync17(file)) return void 0;
4529
4118
  for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
@@ -4561,8 +4150,7 @@ async function runSandboxUpgrade(opts, io) {
4561
4150
  `);
4562
4151
  return 1;
4563
4152
  }
4564
- routeVerb(root, "upgrade", entrySandboxName(root) ?? opts.name);
4565
- const lock = join14(root, ".sandbox-upgrade.lock");
4153
+ const lock = join13(root, ".sandbox-upgrade.lock");
4566
4154
  let fd;
4567
4155
  try {
4568
4156
  fd = openSync2(lock, "wx", 384);
@@ -4677,10 +4265,6 @@ identity, SSH (with its host port), the host Docker socket, and the host path
4677
4265
  for /home/sandbox. With --checkout it seeds those answers from that
4678
4266
  checkout's ${stateNames(bin).configFile}.
4679
4267
 
4680
- \`install openshell\` never prompts. The command needs the host \`openshell\` CLI
4681
- and a connected gateway, and runs the prebuilt image. The command refuses
4682
- --checkout, --home-mount, and image.mode "build".
4683
-
4684
4268
  Flags:
4685
4269
  --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
4686
4270
  --checkout <dir> Bind this checkout into the sandbox and seed the defaults
@@ -4703,7 +4287,7 @@ Flags:
4703
4287
  ${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
4704
4288
  --image=<ref> Run a custom image (implies --image); conflicts with --version
4705
4289
  --no-build Suppress the local build and reuse an existing image
4706
- --print-argv Print the docker compose or openshell argv that would run, then exit
4290
+ --print-argv Print the docker compose argv that would run, then exit
4707
4291
  without writing an entry
4708
4292
  --json Machine-readable output (list)
4709
4293
 
@@ -4896,7 +4480,7 @@ async function runSandboxCommand(rest, bin) {
4896
4480
  // src/commands/harness.ts
4897
4481
  import { existsSync as existsSync20 } from "node:fs";
4898
4482
  import { homedir as homedir3 } from "node:os";
4899
- import { delimiter as delimiter2, join as join17 } from "node:path";
4483
+ import { delimiter as delimiter2, join as join16 } from "node:path";
4900
4484
 
4901
4485
  // src/lib/docs.ts
4902
4486
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4910,7 +4494,7 @@ function sourceDocsUrl(docsPath) {
4910
4494
 
4911
4495
  // src/lib/host-config.ts
4912
4496
  import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
4913
- import { basename as basename7, dirname as dirname8, isAbsolute, join as join15, resolve as resolve10 } from "node:path";
4497
+ import { basename as basename5, dirname as dirname8, isAbsolute, join as join14, resolve as resolve8 } from "node:path";
4914
4498
  var HOST_CONFIG_MODE = 420;
4915
4499
  var HOST_CONFIG_FILE = "config.json";
4916
4500
  var WORKSPACES_SUBDIR = "workspaces";
@@ -4922,7 +4506,7 @@ function hostStateHome(env, home) {
4922
4506
  return resolveUserStateHome(env, home);
4923
4507
  }
4924
4508
  function hostConfigPath(env, home) {
4925
- return join15(hostStateHome(env, home), HOST_CONFIG_FILE);
4509
+ return join14(hostStateHome(env, home), HOST_CONFIG_FILE);
4926
4510
  }
4927
4511
  function assertWorkspaceName(name) {
4928
4512
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -4932,14 +4516,14 @@ function assertWorkspaceName(name) {
4932
4516
  }
4933
4517
  }
4934
4518
  function workspacesRoot(env, home) {
4935
- return join15(hostStateHome(env, home), WORKSPACES_SUBDIR);
4519
+ return join14(hostStateHome(env, home), WORKSPACES_SUBDIR);
4936
4520
  }
4937
4521
  function workspaceRoot(name, env, home) {
4938
4522
  assertWorkspaceName(name);
4939
- return join15(workspacesRoot(env, home), name);
4523
+ return join14(workspacesRoot(env, home), name);
4940
4524
  }
4941
4525
  function defaultHarnessRoot(env, home) {
4942
- return join15(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4526
+ return join14(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4943
4527
  }
4944
4528
  function validateHostConfig(value) {
4945
4529
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -5016,7 +4600,7 @@ function readHostConfig(env, home) {
5016
4600
  try {
5017
4601
  parsed = JSON.parse(raw);
5018
4602
  } catch {
5019
- throw new Error(`${basename7(path4)} is not valid JSON: ${path4}`);
4603
+ throw new Error(`${basename5(path4)} is not valid JSON: ${path4}`);
5020
4604
  }
5021
4605
  return validateHostConfig(parsed);
5022
4606
  }
@@ -5039,15 +4623,15 @@ function writeHostConfig(config, env, home) {
5039
4623
  }
5040
4624
  }
5041
4625
  function resolveHarnessRoot(explicit, env, home) {
5042
- if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4626
+ if (typeof explicit === "string" && explicit !== "") return resolve8(explicit);
5043
4627
  const configured = readHostConfig(env, home).harnessRoot;
5044
- if (typeof configured === "string" && configured !== "") return resolve10(configured);
4628
+ if (typeof configured === "string" && configured !== "") return resolve8(configured);
5045
4629
  return defaultHarnessRoot(env, home);
5046
4630
  }
5047
4631
  function recordHarnessRoot(root, env, home) {
5048
4632
  const config = readHostConfig(env, home);
5049
4633
  const recorded = config.harnessRoot;
5050
- if (typeof recorded === "string" && recorded !== "" && resolve10(recorded) === root) return;
4634
+ if (typeof recorded === "string" && recorded !== "" && resolve8(recorded) === root) return;
5051
4635
  writeHostConfig({ ...config, harnessRoot: root }, env, home);
5052
4636
  }
5053
4637
  function fieldError2(path4, requirement) {
@@ -5056,7 +4640,7 @@ function fieldError2(path4, requirement) {
5056
4640
 
5057
4641
  // src/lib/host-workspace.ts
5058
4642
  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";
5059
- import { dirname as dirname9, join as join16, resolve as resolve11 } from "node:path";
4643
+ import { dirname as dirname9, join as join15, resolve as resolve9 } from "node:path";
5060
4644
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
5061
4645
  var IGNORABLE_ENTRIES = [
5062
4646
  REGISTRY_SUBDIR,
@@ -5068,15 +4652,15 @@ function isDirectory3(path4) {
5068
4652
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
5069
4653
  }
5070
4654
  function stateHomes(home) {
5071
- return STATE_HOME_DIRS.map((name) => join16(home, name));
4655
+ return STATE_HOME_DIRS.map((name) => join15(home, name));
5072
4656
  }
5073
4657
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
5074
- const target = resolve11(root);
4658
+ const target = resolve9(root);
5075
4659
  const enclosing = stateHomes(home).find((state) => target === state);
5076
4660
  if (enclosing === void 0) return void 0;
5077
4661
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
5078
4662
  A workspace there collides with AGRO state and blocks every command run from ${home}.
5079
- Move it out \u2014 for example \`mv ${enclosing} ${join16(home, "agro")}\` \u2014 then re-run this command.
4663
+ Move it out \u2014 for example \`mv ${enclosing} ${join15(home, "agro")}\` \u2014 then re-run this command.
5080
4664
  `;
5081
4665
  }
5082
4666
  function isIgnorableEntry(name) {
@@ -5093,24 +4677,24 @@ function cloneInto(target, run, root, ref) {
5093
4677
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
5094
4678
  }
5095
4679
  function cloneThroughStaging(root, run, ref) {
5096
- const staging = mkdtempSync4(join16(dirname9(root), "agro-clone-"));
4680
+ const staging = mkdtempSync4(join15(dirname9(root), "agro-clone-"));
5097
4681
  try {
5098
- const checkout = join16(staging, "agro");
4682
+ const checkout = join15(staging, "agro");
5099
4683
  cloneInto(checkout, run, root, ref);
5100
4684
  for (const name of readdirSync3(checkout)) {
5101
- const destination = join16(root, name);
4685
+ const destination = join15(root, name);
5102
4686
  if (existsSync19(destination)) {
5103
4687
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
5104
4688
  }
5105
- renameSync5(join16(checkout, name), destination);
4689
+ renameSync5(join15(checkout, name), destination);
5106
4690
  }
5107
4691
  } finally {
5108
4692
  rmSync4(staging, { recursive: true, force: true });
5109
4693
  }
5110
4694
  }
5111
4695
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
5112
- const root = resolve11(path4);
5113
- if (existsSync19(join16(root, ".git"))) return { root, action: "reused" };
4696
+ const root = resolve9(path4);
4697
+ if (existsSync19(join15(root, ".git"))) return { root, action: "reused" };
5114
4698
  const stats = statSync5(root, { throwIfNoEntry: false });
5115
4699
  if (stats === void 0) {
5116
4700
  mkdirSync10(dirname9(root), { recursive: true });
@@ -5132,11 +4716,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
5132
4716
  function listHostWorkspaces(env, home) {
5133
4717
  const root = workspacesRoot(env, home);
5134
4718
  if (!isDirectory3(root)) return [];
5135
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync19(join16(root, name, ".git"))).sort();
4719
+ 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();
5136
4720
  }
5137
4721
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
5138
- const root = resolve11(path4);
5139
- if (existsSync19(join16(root, ".git"))) return { ok: true, root };
4722
+ const root = resolve9(path4);
4723
+ if (existsSync19(join15(root, ".git"))) return { ok: true, root };
5140
4724
  const existing = listHostWorkspaces(env, home);
5141
4725
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
5142
4726
  `;
@@ -5166,7 +4750,7 @@ function targetFor(root, run, env) {
5166
4750
  });
5167
4751
  }
5168
4752
  function hostPrefix(home) {
5169
- return join17(home, ".local");
4753
+ return join16(home, ".local");
5170
4754
  }
5171
4755
  function pathEntries(env) {
5172
4756
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -5213,7 +4797,7 @@ async function sandboxMarkerExists(target, entry) {
5213
4797
  return r.exitCode === 0;
5214
4798
  }
5215
4799
  function hostBinaryUnderPrefix(entry, prefix) {
5216
- return existsSync20(join17(harnessBinPath(prefix), entry.binary));
4800
+ return existsSync20(join16(harnessBinPath(prefix), entry.binary));
5217
4801
  }
5218
4802
  function stateOf(entry, installed, location) {
5219
4803
  return {
@@ -5234,7 +4818,7 @@ function probeableHost(env, home) {
5234
4818
  return void 0;
5235
4819
  }
5236
4820
  const prefix = hostPrefix(home);
5237
- if (!existsSync20(join17(root, ".git")) && !existsSync20(prefix)) return void 0;
4821
+ if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
5238
4822
  return { root, prefix };
5239
4823
  }
5240
4824
  async function collectStates(root, run, home, env, only) {
@@ -5330,10 +4914,20 @@ async function runHarnessStatus(name, opts, io) {
5330
4914
  function hermesTargetRoot(target) {
5331
4915
  return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
5332
4916
  }
4917
+ function shellPath(path4) {
4918
+ return `'${path4.replaceAll("'", "'\\''")}'`;
4919
+ }
4920
+ function hermesLaunch(io, root) {
4921
+ io.stdout(`Run Hermes in the AGRO workspace: HERMES_HOME=${shellPath(`${root}/.hermes`)} hermes
4922
+ `);
4923
+ }
4924
+ function hermesEnv(root) {
4925
+ return { ...agroEnvPair("PROJECT_ROOT", root), HERMES_HOME: `${root}/.hermes` };
4926
+ }
5333
4927
  async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
5334
4928
  const result = await target.exec({
5335
4929
  argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
5336
- env: agroEnvPair("PROJECT_ROOT", root),
4930
+ env: hermesEnv(root),
5337
4931
  ...user ? { user } : {},
5338
4932
  stdio: "inherit"
5339
4933
  });
@@ -5343,6 +4937,43 @@ async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(ta
5343
4937
  }
5344
4938
  return result.exitCode;
5345
4939
  }
4940
+ async function validateHermesHome(target, io, bin, user, inheritedHome, root = hermesTargetRoot(target)) {
4941
+ const result = await target.exec({
4942
+ argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", [
4943
+ "check",
4944
+ root,
4945
+ `${root}/.hermes`,
4946
+ inheritedHome ?? ""
4947
+ ]),
4948
+ env: hermesEnv(root),
4949
+ ...user ? { user } : {},
4950
+ stdio: "capture"
4951
+ });
4952
+ if (result.exitCode !== 0) {
4953
+ io.stderr(result.stderr || `${bin} harness: could not validate Hermes runtime home; explicitly select HERMES_HOME=${shellPath(`${root}/.hermes`)} before retrying.
4954
+ `);
4955
+ }
4956
+ return result.exitCode;
4957
+ }
4958
+ async function configureHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
4959
+ const result = await target.exec({
4960
+ argv: remoteControlDirScript(root, "scripts/hermes-workspace.sh", [
4961
+ "configure",
4962
+ root,
4963
+ `${root}/.hermes`,
4964
+ root,
4965
+ "hermes"
4966
+ ]),
4967
+ env: hermesEnv(root),
4968
+ ...user ? { user } : {},
4969
+ stdio: "inherit"
4970
+ });
4971
+ if (result.exitCode !== 0) {
4972
+ io.stderr(`${bin} harness: Hermes workspace configuration failed (exit ${result.exitCode}); run HERMES_HOME=${shellPath(`${root}/.hermes`)} hermes config set terminal.cwd ${shellPath(root)}
4973
+ `);
4974
+ }
4975
+ return result.exitCode;
4976
+ }
5346
4977
  function unreachableReason(status) {
5347
4978
  return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
5348
4979
  }
@@ -5418,6 +5049,11 @@ async function installOnHost(entry, opts, io, run, status) {
5418
5049
  `);
5419
5050
  const prefix = hostPrefix(home);
5420
5051
  const target = hostTargetFor(root, prefix, run, env);
5052
+ const hermes = entry.id === "hermes";
5053
+ if (hermes) {
5054
+ const code = await validateHermesHome(target, io, bin, void 0, env.HERMES_HOME, root);
5055
+ if (code !== 0) return code;
5056
+ }
5421
5057
  const linked = await target.exec({
5422
5058
  argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init"]),
5423
5059
  env: agroEnvPair("PROJECT_ROOT", root),
@@ -5431,11 +5067,7 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5431
5067
  );
5432
5068
  return 1;
5433
5069
  }
5434
- const hermes = entry.id === "hermes";
5435
- const installEnv = hermes ? {
5436
- ...agroEnvPair("PROJECT_ROOT", root),
5437
- HERMES_HOME: `${root}/.hermes`
5438
- } : void 0;
5070
+ const installEnv = hermes ? hermesEnv(root) : void 0;
5439
5071
  if (hermes) {
5440
5072
  const code = await reconcileHermes(target, io, bin, void 0, root);
5441
5073
  if (code !== 0) return code;
@@ -5449,6 +5081,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5449
5081
  return 1;
5450
5082
  }
5451
5083
  if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true && (hasReceipt || !hostBinaryUnderPrefix(entry, prefix))) {
5084
+ if (hermes) {
5085
+ const code = await configureHermes(target, io, bin, void 0, root);
5086
+ if (code !== 0) return code;
5087
+ }
5452
5088
  io.stdout(`${entry.id}: already installed (${entry.binary})
5453
5089
  `);
5454
5090
  try {
@@ -5458,6 +5094,7 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5458
5094
  `);
5459
5095
  return 1;
5460
5096
  }
5097
+ if (hermes) hermesLaunch(io, root);
5461
5098
  return 0;
5462
5099
  }
5463
5100
  io.stdout(`installing ${entry.title} on the host\u2026
@@ -5480,6 +5117,8 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5480
5117
  }
5481
5118
  const code = await reconcileHermes(target, io, bin, void 0, root);
5482
5119
  if (code !== 0) return code;
5120
+ const configured = await configureHermes(target, io, bin, void 0, root);
5121
+ if (configured !== 0) return configured;
5483
5122
  }
5484
5123
  const receipt = {
5485
5124
  prefix,
@@ -5510,7 +5149,8 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
5510
5149
  io.stdout(`Add this line to your shell profile: export PATH="${harnessBinPath(prefix)}:$PATH"
5511
5150
  `);
5512
5151
  }
5513
- io.stdout(`Run ${entry.title} in the AGRO workspace: cd ${root} && ${harnessLaunchCommand(entry)}
5152
+ if (hermes) hermesLaunch(io, root);
5153
+ else io.stdout(`Run ${entry.title} in the AGRO workspace: cd ${root} && ${harnessLaunchCommand(entry)}
5514
5154
  `);
5515
5155
  return 0;
5516
5156
  }
@@ -5642,18 +5282,22 @@ async function runHarnessInstall(name, opts, io) {
5642
5282
  return await installOnHost(entry, opts, io, run, status);
5643
5283
  }
5644
5284
  const hermes = entry.id === "hermes";
5645
- const installEnv = hermes ? {
5646
- ...agroEnvPair("PROJECT_ROOT", hermesTargetRoot(target)),
5647
- HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
5648
- } : void 0;
5285
+ const installEnv = hermes ? hermesEnv(hermesTargetRoot(target)) : void 0;
5649
5286
  if (hermes) {
5287
+ const checked = await validateHermesHome(target, io, opts.bin, "sandbox", env.HERMES_HOME);
5288
+ if (checked !== 0) return checked;
5650
5289
  const code = await reconcileHermes(target, io, opts.bin, "sandbox");
5651
5290
  if (code !== 0) return code;
5652
5291
  }
5653
5292
  const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
5654
5293
  if (already === true && await sandboxMarkerExists(target, entry)) {
5294
+ if (hermes) {
5295
+ const code = await configureHermes(target, io, opts.bin, "sandbox");
5296
+ if (code !== 0) return code;
5297
+ }
5655
5298
  io.stdout(`${entry.id}: already installed (${entry.binary})
5656
5299
  `);
5300
+ if (hermes) hermesLaunch(io, hermesTargetRoot(target));
5657
5301
  return 0;
5658
5302
  }
5659
5303
  if (already === null) {
@@ -5681,6 +5325,8 @@ async function runHarnessInstall(name, opts, io) {
5681
5325
  }
5682
5326
  const code = await reconcileHermes(target, io, opts.bin, "sandbox");
5683
5327
  if (code !== 0) return code;
5328
+ const configured = await configureHermes(target, io, opts.bin, "sandbox");
5329
+ if (configured !== 0) return configured;
5684
5330
  }
5685
5331
  const marker = sandboxMarkerPath(entry);
5686
5332
  const marked = await target.exec({
@@ -5695,13 +5341,14 @@ async function runHarnessInstall(name, opts, io) {
5695
5341
  }
5696
5342
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
5697
5343
  `);
5344
+ if (hermes) hermesLaunch(io, hermesTargetRoot(target));
5698
5345
  return 0;
5699
5346
  }
5700
5347
 
5701
5348
  // src/commands/workspace.ts
5702
5349
  import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
5703
5350
  import { homedir as homedir4 } from "node:os";
5704
- import { join as join18, resolve as resolve12 } from "node:path";
5351
+ import { join as join17, resolve as resolve10 } from "node:path";
5705
5352
  function homeOf2(opts) {
5706
5353
  return (opts.homedir ?? homedir4)();
5707
5354
  }
@@ -5710,7 +5357,7 @@ function messageOf2(error) {
5710
5357
  }
5711
5358
  function recordedRoot(env, home) {
5712
5359
  const configured = readHostConfig(env, home).harnessRoot;
5713
- return typeof configured === "string" && configured !== "" ? resolve12(configured) : void 0;
5360
+ return typeof configured === "string" && configured !== "" ? resolve10(configured) : void 0;
5714
5361
  }
5715
5362
  function renderTable2(rows, io) {
5716
5363
  const header2 = ["WORKSPACE", "DEFAULT", "PATH"];
@@ -5735,7 +5382,7 @@ async function runWorkspaceCreate(name, opts, io) {
5735
5382
  let root;
5736
5383
  const chosen = name ?? DEFAULT_WORKSPACE_NAME;
5737
5384
  try {
5738
- root = opts.path !== void 0 ? resolve12(opts.path) : workspaceRoot(chosen, env, home);
5385
+ root = opts.path !== void 0 ? resolve10(opts.path) : workspaceRoot(chosen, env, home);
5739
5386
  } catch (err) {
5740
5387
  io.stderr(`${bin} workspace: ${messageOf2(err)}
5741
5388
  `);
@@ -5749,7 +5396,7 @@ async function runWorkspaceCreate(name, opts, io) {
5749
5396
  let action;
5750
5397
  const preexisting = existsSync21(root);
5751
5398
  try {
5752
- if (!opts.json && !existsSync21(join18(root, ".git"))) {
5399
+ if (!opts.json && !existsSync21(join17(root, ".git"))) {
5753
5400
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
5754
5401
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
5755
5402
  `);
@@ -5794,7 +5441,7 @@ async function runWorkspaceList(opts, io) {
5794
5441
  const recorded = recordedRoot(env, home);
5795
5442
  const registry = workspacesRoot(env, home);
5796
5443
  rows = listHostWorkspaces(env, home).map((name) => {
5797
- const root = join18(registry, name);
5444
+ const root = join17(registry, name);
5798
5445
  return { name, root, default: recorded === root };
5799
5446
  });
5800
5447
  } catch (err) {
@@ -5821,7 +5468,7 @@ async function runWorkspaceList(opts, io) {
5821
5468
  // src/commands/tool.ts
5822
5469
  import { existsSync as existsSync22 } from "node:fs";
5823
5470
  import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
5824
- import { delimiter as delimiter3, join as join19 } from "node:path";
5471
+ import { delimiter as delimiter3, join as join18 } from "node:path";
5825
5472
 
5826
5473
  // src/lib/tools/catalog.ts
5827
5474
  var TOOLS_DOC = "docs/installation.md";
@@ -6252,7 +5899,7 @@ function targetFor2(root, run, env) {
6252
5899
  });
6253
5900
  }
6254
5901
  function hostPrefix2(home) {
6255
- return join19(home, ".local");
5902
+ return join18(home, ".local");
6256
5903
  }
6257
5904
  function sandboxMarkerPath2(entry) {
6258
5905
  return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
@@ -6266,7 +5913,7 @@ async function sandboxMarkerExists2(target, entry) {
6266
5913
  return r.exitCode === 0;
6267
5914
  }
6268
5915
  function hostBinaryUnderPrefix2(entry, prefix) {
6269
- return existsSync22(join19(harnessBinPath(prefix), entry.binary));
5916
+ return existsSync22(join18(harnessBinPath(prefix), entry.binary));
6270
5917
  }
6271
5918
  function pathEntries2(env) {
6272
5919
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -6346,7 +5993,7 @@ function probeableHost2(env, home) {
6346
5993
  return void 0;
6347
5994
  }
6348
5995
  const prefix = hostPrefix2(home);
6349
- if (!existsSync22(join19(root, ".git")) && !existsSync22(prefix)) return void 0;
5996
+ if (!existsSync22(join18(root, ".git")) && !existsSync22(prefix)) return void 0;
6350
5997
  return { root, prefix };
6351
5998
  }
6352
5999
  async function collectRows(root, run, home, env, only) {
@@ -6820,11 +6467,11 @@ Remove ${entry.id} by hand: ${ROOT_TOOL_REMOVAL_DOCS}
6820
6467
  }
6821
6468
 
6822
6469
  // src/lib/remote.ts
6823
- import { spawnSync as spawnSync3 } from "node:child_process";
6470
+ import { spawnSync as spawnSync4 } from "node:child_process";
6824
6471
  var DEFAULT_REPO_URL = "https://github.com/mifunedev/agro";
6825
6472
  var DEFAULT_CLONE_TIMEOUT_MS = 12e4;
6826
6473
  var spawnRunner2 = (cmd, args, opts) => {
6827
- const r = spawnSync3(cmd, args, {
6474
+ const r = spawnSync4(cmd, args, {
6828
6475
  env: opts.env,
6829
6476
  timeout: opts.timeoutMs,
6830
6477
  stdio: ["ignore", "ignore", "pipe"],
@@ -6882,8 +6529,37 @@ function fetchRemoteSource(opts) {
6882
6529
  return opts.destDir;
6883
6530
  }
6884
6531
 
6532
+ // src/command-table.ts
6533
+ var CONFIG_VERBS = ["show", "set", "repo"];
6534
+ var CONFIG_INTEGRATIONS = ["langfuse"];
6535
+ var SECRET_VERBS = ["set", "list"];
6536
+ var LANGFUSE_VERBS = ["apply", "status", "disable"];
6537
+ var SANDBOX_SUBCOMMANDS = ["install", "list", "upgrade"];
6538
+ var WORKSPACE_SUBCOMMANDS = ["create", "list"];
6539
+ var HARNESS_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
6540
+ var TOOL_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
6541
+ function isOneOf(values, value) {
6542
+ return value !== void 0 && values.includes(value);
6543
+ }
6544
+ var AGRO_COMMANDS = Object.freeze({
6545
+ config: [...CONFIG_VERBS, ...CONFIG_INTEGRATIONS],
6546
+ secret: SECRET_VERBS,
6547
+ langfuse: LANGFUSE_VERBS,
6548
+ update: null,
6549
+ "self-upgrade": null,
6550
+ vendor: null,
6551
+ sandbox: SANDBOX_SUBCOMMANDS,
6552
+ shell: null,
6553
+ compose: null,
6554
+ workspace: WORKSPACE_SUBCOMMANDS,
6555
+ harness: HARNESS_SUBCOMMANDS,
6556
+ tool: TOOL_SUBCOMMANDS,
6557
+ gateway: null,
6558
+ ...Object.fromEntries(composeVerbs().map((verb) => [verb, null]))
6559
+ });
6560
+
6885
6561
  // src/cli.ts
6886
- var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
6562
+ var DEFAULT_SOURCE_CONTROL_DIR = resolve11(
6887
6563
  dirname10(fileURLToPath(import.meta.url)),
6888
6564
  "../.."
6889
6565
  );
@@ -7018,7 +6694,7 @@ The upgrade follows whichever mechanism installed this executable:
7018
6694
  npm-managed realpath under node_modules/${AGRO_PRODUCT.packageName}/: reads the registry
7019
6695
  version with \`npm view\`, then runs
7020
6696
  \`npm install -g --prefix <owning prefix> ${AGRO_PRODUCT.packageName}@<version>\`.
7021
- standalone a plain file (get-agro.sh): downloads AGRO_JS_URL (default
6697
+ standalone a plain file (install.sh): downloads AGRO_JS_URL (default
7022
6698
  ${DEFAULT_ARTIFACT_URL})
7023
6699
  into the same directory, checks its shebang and \`--version\`,
7024
6700
  renames it over the executable, and keeps <path>.prev until the
@@ -7276,7 +6952,6 @@ function extractSandboxFlag(command, rest) {
7276
6952
  }
7277
6953
  return { ok: true, args: sandbox === void 0 ? { rest: kept } : { rest: kept, sandbox } };
7278
6954
  }
7279
- var LANGFUSE_VERBS = ["apply", "status", "disable"];
7280
6955
  function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
7281
6956
  if (rest.length === 0 || isHelpFlag2(rest[0])) return { ok: true, args: { help: true } };
7282
6957
  const [head, ...tail] = rest;
@@ -7293,7 +6968,6 @@ function parseLangfuseArgs(rest, bin = AGRO_PRODUCT.bin) {
7293
6968
  }
7294
6969
  return { ok: true, args: { help: false, verb: head } };
7295
6970
  }
7296
- var CONFIG_VERBS = ["show", "set", "repo"];
7297
6971
  function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
7298
6972
  const scoped = extractSandboxFlag(`${bin} config`, input);
7299
6973
  if (!scoped.ok) return scoped;
@@ -7345,7 +7019,6 @@ function parseConfigArgs(input, bin = AGRO_PRODUCT.bin) {
7345
7019
  }
7346
7020
  return { ok: true, args: { ...args, verb, key, value } };
7347
7021
  }
7348
- var SECRET_VERBS = ["set", "list"];
7349
7022
  function parseSecretArgs(input, bin = AGRO_PRODUCT.bin) {
7350
7023
  const scoped = extractSandboxFlag(`${bin} secret`, input);
7351
7024
  if (!scoped.ok) return scoped;
@@ -7555,7 +7228,7 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
7555
7228
  return { ok: false, error: `${bin} workspace: --ref requires a branch or tag` };
7556
7229
  }
7557
7230
  const [sub, name, ...extra] = positionals;
7558
- if (sub !== "create" && sub !== "list") {
7231
+ if (!isOneOf(WORKSPACE_SUBCOMMANDS, sub)) {
7559
7232
  return {
7560
7233
  ok: false,
7561
7234
  error: `${bin} workspace: unknown subcommand "${sub}" \u2014 expected create or list`,
@@ -7641,7 +7314,7 @@ function parseHarnessArgs(rest, bin = AGRO_PRODUCT.bin) {
7641
7314
  };
7642
7315
  }
7643
7316
  const [sub, name, ...extra] = positionals;
7644
- if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
7317
+ if (!isOneOf(HARNESS_SUBCOMMANDS, sub)) {
7645
7318
  return {
7646
7319
  ok: false,
7647
7320
  error: `${bin} harness: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
@@ -7698,7 +7371,7 @@ function parseToolArgs(rest, bin = AGRO_PRODUCT.bin) {
7698
7371
  return { ok: false, error: `${bin} tool: --path requires a directory` };
7699
7372
  }
7700
7373
  const [sub, name, ...extra] = positionals;
7701
- if (sub !== "list" && sub !== "install" && sub !== "status" && sub !== "uninstall") {
7374
+ if (!isOneOf(TOOL_SUBCOMMANDS, sub)) {
7702
7375
  return {
7703
7376
  ok: false,
7704
7377
  error: `${bin} tool: unknown subcommand "${sub}" \u2014 expected list, install, uninstall, or status`,
@@ -7731,18 +7404,18 @@ function parseGatewayArgs(rest) {
7731
7404
  return { ok: true, args: { help: false, passthrough: [...rest] } };
7732
7405
  }
7733
7406
  function bundledPayloadExists(bundled, exists = existsSync23) {
7734
- return exists(join20(bundled.sourceControlDir, "manifest.json"));
7407
+ return exists(join19(bundled.sourceControlDir, "manifest.json"));
7735
7408
  }
7736
7409
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
7737
7410
  const exists = bundled.exists ?? existsSync23;
7738
7411
  if (args.fromDir !== void 0) {
7739
- return { kind: "local", fromDir: resolve13(args.fromDir) };
7412
+ return { kind: "local", fromDir: resolve11(args.fromDir) };
7740
7413
  }
7741
7414
  if (args.fromRemote) {
7742
7415
  return { kind: "remote", ref: args.ref };
7743
7416
  }
7744
7417
  if (bundledPayloadExists(bundled, exists)) {
7745
- return { kind: "local", fromDir: resolve13(bundled.sourceControlDir, "..") };
7418
+ return { kind: "local", fromDir: resolve11(bundled.sourceControlDir, "..") };
7746
7419
  }
7747
7420
  return {
7748
7421
  kind: "remote",
@@ -7754,7 +7427,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
7754
7427
  function readPayloadVersion(checkoutDir) {
7755
7428
  try {
7756
7429
  const parsed = JSON.parse(
7757
- readFileSync13(join20(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7430
+ readFileSync13(join19(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7758
7431
  );
7759
7432
  if (parsed && typeof parsed.version === "string") return parsed.version;
7760
7433
  } catch {
@@ -7763,7 +7436,7 @@ function readPayloadVersion(checkoutDir) {
7763
7436
  }
7764
7437
  async function runWithRemoteSource(hooks, fn) {
7765
7438
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
7766
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join20(tmpdir4(), "oh-remote-")));
7439
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join19(tmpdir4(), "oh-remote-")));
7767
7440
  const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
7768
7441
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
7769
7442
  const checkoutDir = mkdtemp();
@@ -7789,6 +7462,9 @@ async function main(argv) {
7789
7462
  `);
7790
7463
  return 0;
7791
7464
  }
7465
+ if (!Object.hasOwn(AGRO_COMMANDS, first)) return unknownCommand(first, product);
7466
+ const fallbackWarning = sandboxFallbackWarning();
7467
+ if (fallbackWarning !== void 0) process.stderr.write(fallbackWarning);
7792
7468
  if (first === "config") {
7793
7469
  const parsed = parseConfigArgs(argv.slice(1), bin);
7794
7470
  if (!parsed.ok) {
@@ -8111,7 +7787,10 @@ async function main(argv) {
8111
7787
  }
8112
7788
  return runGateway(parsed.args.passthrough, { bin });
8113
7789
  }
8114
- process.stderr.write(`${bin}: unknown command "${first}"
7790
+ return unknownCommand(first, product);
7791
+ }
7792
+ function unknownCommand(first, product) {
7793
+ process.stderr.write(`${product.bin}: unknown command "${first}"
8115
7794
 
8116
7795
  `);
8117
7796
  printAgroHelp(product);
@@ -8123,19 +7802,7 @@ function lifecycleIo() {
8123
7802
  stderr: (s) => process.stderr.write(s)
8124
7803
  };
8125
7804
  }
8126
- async function runCli(argv) {
8127
- try {
8128
- return await main(argv);
8129
- } catch (err) {
8130
- if (err instanceof RuntimeUnsupportedError) {
8131
- process.stderr.write(`${err.message}
8132
- `);
8133
- return 1;
8134
- }
8135
- throw err;
8136
- }
8137
- }
8138
- runCli(process.argv.slice(2)).then(
7805
+ main(process.argv.slice(2)).then(
8139
7806
  (code) => process.exit(code),
8140
7807
  (err) => {
8141
7808
  const msg = err instanceof Error ? err.message : String(err);
@@ -8145,9 +7812,6 @@ runCli(process.argv.slice(2)).then(
8145
7812
  }
8146
7813
  );
8147
7814
  export {
8148
- CONFIG_VERBS,
8149
- LANGFUSE_VERBS,
8150
- SECRET_VERBS,
8151
7815
  bundledPayloadExists,
8152
7816
  extractSandboxFlag,
8153
7817
  isHelpFlag2 as isHelpFlag,
@@ -8180,7 +7844,6 @@ export {
8180
7844
  printVendorHelp,
8181
7845
  printWorkspaceHelp,
8182
7846
  resolveVendorSource,
8183
- runCli,
8184
7847
  runWithRemoteSource,
8185
7848
  runtimeLines
8186
7849
  };