@mifune/agro 0.16.0 → 0.17.0-rc.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 (2) hide show
  1. package/dist/agro.js +623 -141
  2. 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 join19, resolve as resolve11 } from "node:path";
10
+ import { dirname as dirname10, join as join20, resolve as resolve13 } 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
 
@@ -835,6 +835,49 @@ import { resolve as resolve3, sep } from "node:path";
835
835
  // src/lib/agro-config.ts
836
836
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
837
837
  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
838
881
  var AGRO_CONFIG_FILE = NAMES.configFile;
839
882
  var AGRO_CONFIG_MODE = 420;
840
883
  var RESERVED_HOME_PATHS = [
@@ -856,7 +899,7 @@ var RESERVED_HOME_PATHS = [
856
899
  "/usr",
857
900
  "/var"
858
901
  ];
859
- var SANDBOX_RUNTIMES = ["docker"];
902
+ var SANDBOX_RUNTIMES = PROVISIONABLE_RUNTIMES;
860
903
  function configCheckout(config) {
861
904
  const value = config.checkout ?? config.repo;
862
905
  return typeof value === "string" && value !== "" ? value : void 0;
@@ -885,6 +928,13 @@ function defaultAgroConfig(name) {
885
928
  composeOverrides: []
886
929
  };
887
930
  }
931
+ function entryRuntime(root) {
932
+ try {
933
+ return readAgroConfig(agroConfigPath(root)).runtime ?? "docker";
934
+ } catch {
935
+ return "docker";
936
+ }
937
+ }
888
938
  function readAgroConfig(path4) {
889
939
  if (!existsSync4(path4)) return defaultAgroConfig(basename3(dirname2(resolve2(path4))));
890
940
  let raw;
@@ -1274,8 +1324,8 @@ function redact(token) {
1274
1324
  async function ask(question) {
1275
1325
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1276
1326
  try {
1277
- return await new Promise((resolve12) => {
1278
- rl.question(` ${question} `, (answer) => resolve12(answer.trim()));
1327
+ return await new Promise((resolve14) => {
1328
+ rl.question(` ${question} `, (answer) => resolve14(answer.trim()));
1279
1329
  });
1280
1330
  } finally {
1281
1331
  rl.close();
@@ -1287,7 +1337,7 @@ async function askSecret(question) {
1287
1337
  process.stdin.setRawMode(true);
1288
1338
  process.stdin.resume();
1289
1339
  const bytes = [];
1290
- return await new Promise((resolve12) => {
1340
+ return await new Promise((resolve14) => {
1291
1341
  const cleanup = () => {
1292
1342
  try {
1293
1343
  process.stdin.setRawMode(false);
@@ -1305,7 +1355,7 @@ async function askSecret(question) {
1305
1355
  } else if (byte === 13 || byte === 10) {
1306
1356
  cleanup();
1307
1357
  process.stdout.write("\n");
1308
- resolve12(Buffer.from(bytes).toString("utf8").trim());
1358
+ resolve14(Buffer.from(bytes).toString("utf8").trim());
1309
1359
  return;
1310
1360
  } else if (byte === 127 || byte === 8) {
1311
1361
  if (bytes.length > 0) {
@@ -1355,10 +1405,10 @@ async function askSecretPiped(question) {
1355
1405
  };
1356
1406
  process.on("SIGINT", onSigint);
1357
1407
  try {
1358
- return await new Promise((resolve12) => {
1408
+ return await new Promise((resolve14) => {
1359
1409
  rl.question(` ${question} `, (answer) => {
1360
1410
  process.stdout.write("\n");
1361
- resolve12(answer.trim());
1411
+ resolve14(answer.trim());
1362
1412
  });
1363
1413
  });
1364
1414
  } finally {
@@ -1736,6 +1786,9 @@ printf 'free\\n'
1736
1786
  exit 0
1737
1787
  `;
1738
1788
 
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
+
1739
1792
  // src/lib/registry.ts
1740
1793
  var SANDBOX_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
1741
1794
  var DEFAULT_NAME_PREFIX = "agro-sbx-";
@@ -1792,10 +1845,12 @@ function nextDefaultName(run = spawnRunner) {
1792
1845
  if (!taken.has(candidate)) return candidate;
1793
1846
  }
1794
1847
  }
1795
- function materialize(root, opts = {}) {
1796
- const entry = resolve5(root);
1848
+ function openshellFiles(entry) {
1849
+ return [{ dest: join4(entry, "openshell-policy.yaml"), body: openshell_policy_default, mode: 420 }];
1850
+ }
1851
+ function dockerFiles(entry, opts) {
1797
1852
  const scripts = join4(resolveProjectLayout(entry).controlDir, "scripts");
1798
- const files = [
1853
+ return [
1799
1854
  {
1800
1855
  dest: join4(entry, ".devcontainer", "docker-compose.yml"),
1801
1856
  body: (opts.checkout ?? opts.repo) === void 0 ? docker_compose_image_only_default : docker_compose_default,
@@ -1807,6 +1862,10 @@ function materialize(root, opts = {}) {
1807
1862
  { dest: join4(scripts, "paths.sh"), body: paths_default, mode: 420 },
1808
1863
  { dest: join4(scripts, "check-host-port.sh"), body: check_host_port_default, mode: 493 }
1809
1864
  ];
1865
+ }
1866
+ function materialize(root, opts = {}) {
1867
+ const entry = resolve5(root);
1868
+ const files = entryRuntime(entry) === "openshell" ? openshellFiles(entry) : dockerFiles(entry, opts);
1810
1869
  for (const file of files) {
1811
1870
  const dest = resolve5(file.dest);
1812
1871
  if (!dest.startsWith(entry + sep2)) {
@@ -1965,11 +2024,27 @@ function stripQuotes2(s) {
1965
2024
  // src/commands/lifecycle.ts
1966
2025
  import { existsSync as existsSync10, mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
1967
2026
  import { tmpdir } from "node:os";
1968
- import { basename as basename4, join as join5, sep as sep3 } from "node:path";
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
+ }
1969
2043
 
1970
2044
  // src/lib/execution/detect.ts
1971
2045
  import { existsSync as existsSync9 } from "node:fs";
1972
2046
  var SANDBOX_MARKER_FILE = "/.dockerenv";
2047
+ var EXECUTION_TARGET_ENV = "AGRO_EXECUTION_TARGET";
1973
2048
  function runningInsideSandbox(env = process.env, fileExists = existsSync9) {
1974
2049
  const override = agroEnvValue(env, "EXECUTION_TARGET");
1975
2050
  if (override === "local") return true;
@@ -2195,6 +2270,243 @@ function defaultIdentity() {
2195
2270
  return { name: info2.username, uid: info2.uid };
2196
2271
  }
2197
2272
 
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
+
2198
2510
  // src/lib/execution/status.ts
2199
2511
  var RUNTIME_ABSENT_STATUS = "no container runtime";
2200
2512
  async function resolveTargetStatus(target) {
@@ -2218,19 +2530,23 @@ function resolveExecutionTarget(opts) {
2218
2530
  ...opts.env ? { env: opts.env } : {}
2219
2531
  });
2220
2532
  }
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
+ }
2221
2543
  return new DockerComposeExecutionTarget(opts);
2222
2544
  }
2223
-
2224
- // src/lib/version.ts
2225
- var AGRO_VERSION = true ? "0.16.0" : "0.0.0-dev";
2226
- var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2227
- var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2228
- function parseReleaseVersion(value) {
2229
- const bare = value.startsWith("v") ? value.slice(1) : value;
2230
- return RELEASE_VERSION.test(bare) ? bare : void 0;
2545
+ function openshellImage(config) {
2546
+ return nonEmpty(config.image?.ref) ?? officialImageRef(AGRO_VERSION);
2231
2547
  }
2232
- function officialImageRef(version) {
2233
- return `${OFFICIAL_IMAGE}:${RELEASE_VERSION.test(version) ? version : "latest"}`;
2548
+ function nonEmpty(value) {
2549
+ return value === void 0 || value === "" ? void 0 : value;
2234
2550
  }
2235
2551
 
2236
2552
  // src/lib/config-render.ts
@@ -2296,13 +2612,103 @@ function renderComposeEnv(config) {
2296
2612
  `;
2297
2613
  }
2298
2614
 
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
+
2299
2705
  // src/commands/lifecycle.ts
2300
2706
  var COMPOSE_ENV_DIR_PREFIX = "oh-compose-env-";
2301
2707
  function composeEnvDir() {
2302
- return mkdtempSync(join5(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
2708
+ return mkdtempSync(join6(tmpdir(), COMPOSE_ENV_DIR_PREFIX));
2303
2709
  }
2304
2710
  function writeComposeEnv(root, dir) {
2305
- const file = join5(dir, "compose.env");
2711
+ const file = join6(dir, "compose.env");
2306
2712
  writeFileSync5(file, renderComposeEnv(readAgroConfig(agroConfigPath(root))), {
2307
2713
  mode: 384,
2308
2714
  encoding: "utf8"
@@ -2360,7 +2766,7 @@ async function maybePromptDockerSocket(root, io, bin) {
2360
2766
  if (dockerSocketConfigured(root)) return;
2361
2767
  const interactive = process.stdin.isTTY === true || io.ask !== void 0;
2362
2768
  if (!interactive) return;
2363
- const envDir = join5(root, ".devcontainer");
2769
+ const envDir = join6(root, ".devcontainer");
2364
2770
  if (!existsSync10(envDir)) return;
2365
2771
  const askFn = io.ask ?? ask;
2366
2772
  const answer = (await askFn(
@@ -2441,25 +2847,27 @@ async function runSandbox(opts, io) {
2441
2847
  function configuredContainerName(root) {
2442
2848
  return fromProcessEnv("SANDBOX_NAME") ?? configuredString(root, "name");
2443
2849
  }
2850
+ function sandboxName(root) {
2851
+ return entrySandboxName(root) ?? configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2852
+ }
2444
2853
  function runShell(opts, io) {
2445
2854
  const run = opts.run ?? spawnRunner;
2446
2855
  const root = sandboxRoot(opts);
2447
- const name = configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
2856
+ const name = sandboxName(root);
2857
+ const { runtime } = routeVerb(root, "shell", name);
2448
2858
  const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
2449
2859
  let code;
2450
2860
  try {
2451
2861
  code = target.attach({ argv: ["zsh"], user: "sandbox" });
2452
2862
  } catch (err) {
2453
2863
  if (err instanceof ExecutionSpawnError && err.code === "ENOENT") {
2454
- throw new Error(`docker is required for \`${opts.bin} shell\` but was not found on PATH`);
2864
+ throw new Error(`${runtime} is required for \`${opts.bin} shell\` but was not found on PATH`);
2455
2865
  }
2456
2866
  throw err;
2457
2867
  }
2458
2868
  if (code !== 0) {
2459
- io.stderr(
2460
- `container \`${name}\` not running? start it with \`${opts.bin} sandbox install docker\`
2461
- `
2462
- );
2869
+ io.stderr(`${shellFailureHint(runtime, opts.bin, name)}
2870
+ `);
2463
2871
  }
2464
2872
  return code;
2465
2873
  }
@@ -2476,6 +2884,7 @@ function composeVerbs() {
2476
2884
  function runComposeVerb(verb, opts, extra = []) {
2477
2885
  const run = opts.run ?? spawnRunner;
2478
2886
  const root = sandboxRoot(opts);
2887
+ routeVerb(root, verb, sandboxName(root));
2479
2888
  const script = requireLifecycleScript(root, "docker-compose.sh");
2480
2889
  return withComposeEnvFile(root, (extraArgs) => {
2481
2890
  const r = run("bash", [script, ...extraArgs, ...COMPOSE_VERBS[verb], ...extra], {
@@ -2488,6 +2897,7 @@ function runComposeVerb(verb, opts, extra = []) {
2488
2897
  function runComposeConfig(opts, extra = []) {
2489
2898
  const run = opts.run ?? spawnRunner;
2490
2899
  const root = sandboxRoot(opts);
2900
+ routeVerb(root, "config", sandboxName(root));
2491
2901
  const script = requireLifecycleScript(root, "docker-compose.sh");
2492
2902
  return withComposeEnvFile(root, (extraArgs) => {
2493
2903
  const r = run("bash", [script, ...extraArgs, "config", ...extra], { stdio: "inherit" });
@@ -2498,7 +2908,7 @@ function runComposeConfig(opts, extra = []) {
2498
2908
  function namedVolumes(root) {
2499
2909
  let text;
2500
2910
  try {
2501
- text = readFileSync7(join5(root, ".devcontainer", "docker-compose.yml"), "utf8");
2911
+ text = readFileSync7(join6(root, ".devcontainer", "docker-compose.yml"), "utf8");
2502
2912
  } catch {
2503
2913
  return [];
2504
2914
  }
@@ -2514,11 +2924,23 @@ function namedVolumes(root) {
2514
2924
  return names;
2515
2925
  }
2516
2926
  function destroyConfirmationPhrase(root) {
2517
- return configuredContainerName(root) ?? DEFAULT_CONTAINER_NAME;
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
+ }
2518
2939
  }
2519
2940
  async function runDestroy(opts, io) {
2520
2941
  const root = sandboxRoot(opts);
2521
2942
  const name = destroyConfirmationPhrase(root);
2943
+ const { via } = routeVerb(root, "destroy", name);
2522
2944
  if (opts.yes !== true) {
2523
2945
  const interactive = process.stdin.isTTY === true || io.ask !== void 0;
2524
2946
  if (!interactive) {
@@ -2533,7 +2955,14 @@ async function runDestroy(opts, io) {
2533
2955
  ${bold(`${opts.bin} destroy \u2014 ${name}`)}
2534
2956
 
2535
2957
  `);
2536
- if (homePath !== void 0 && homePath !== "") {
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 !== "") {
2537
2966
  io.stdout("`docker compose down -v` removes the containers.\n");
2538
2967
  io.stdout(
2539
2968
  `This sandbox keeps its home on a host bind at ${homePath}, which \`down -v\`
@@ -2564,7 +2993,7 @@ stay there. Delete that directory yourself if you also want them gone.
2564
2993
  return 1;
2565
2994
  }
2566
2995
  }
2567
- const code = runComposeVerb("destroy", { ...opts, name: basename4(root) });
2996
+ const code = via === "target" ? await destroyThroughTarget(root, name, opts.run ?? spawnRunner) : runComposeVerb("destroy", { ...opts, name: basename6(root) });
2568
2997
  if (code === 0 && root.startsWith(registryRoot() + sep3)) {
2569
2998
  rmSync(root, { recursive: true, force: true });
2570
2999
  io.stdout(`removed the sandbox entry ${root}
@@ -2853,13 +3282,13 @@ function sameRemote(url, owner, name) {
2853
3282
 
2854
3283
  // src/commands/secret.ts
2855
3284
  import { existsSync as existsSync12 } from "node:fs";
2856
- import { join as join7 } from "node:path";
3285
+ import { join as join8 } from "node:path";
2857
3286
 
2858
3287
  // src/lib/gitignore.ts
2859
3288
  import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
2860
- import { dirname as dirname5, join as join6 } from "node:path";
3289
+ import { dirname as dirname5, join as join7 } from "node:path";
2861
3290
  function appendGitignoreLines(root, lines) {
2862
- const target = join6(root, ".gitignore");
3291
+ const target = join7(root, ".gitignore");
2863
3292
  const existing = existsSync11(target) ? readFileSync8(target, "utf8") : "";
2864
3293
  const present = new Set(existing.split("\n").map((line) => line.trimEnd()));
2865
3294
  const added = [];
@@ -2884,7 +3313,7 @@ function secretsRoot(opts) {
2884
3313
  return opts.sandbox === void 0 ? resolveProjectRoot(opts.cwd) : resolveSandboxRoot({ name: opts.sandbox });
2885
3314
  }
2886
3315
  function ignoreSecretsFile(root) {
2887
- if (!existsSync12(join7(root, ".git"))) return;
3316
+ if (!existsSync12(join8(root, ".git"))) return;
2888
3317
  appendGitignoreLines(root, [".env"]);
2889
3318
  }
2890
3319
  function secretKeyList() {
@@ -2942,10 +3371,10 @@ async function runSecretList(opts, io) {
2942
3371
  // src/commands/langfuse.ts
2943
3372
  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";
2944
3373
  import { homedir as homedir2, tmpdir as tmpdir2 } from "node:os";
2945
- import { dirname as dirname7, join as join10, relative } from "node:path";
3374
+ import { dirname as dirname7, join as join11, relative } from "node:path";
2946
3375
 
2947
3376
  // src/lib/tracing/harness-writers.ts
2948
- import { join as join8 } from "node:path";
3377
+ import { join as join9 } from "node:path";
2949
3378
 
2950
3379
  // src/lib/tracing/writer.ts
2951
3380
  import { chmodSync, existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
@@ -2990,13 +3419,13 @@ function isPlainObject(value) {
2990
3419
  // src/lib/tracing/harness-writers.ts
2991
3420
  var CODEX_TRACING_TAGS = ["codex"];
2992
3421
  function claudeCodeSettingsPath(home) {
2993
- return join8(home, ".claude", "settings.json");
3422
+ return join9(home, ".claude", "settings.json");
2994
3423
  }
2995
3424
  function piTracingConfigPath(home) {
2996
- return join8(home, ".pi", "agent", "langfuse.json");
3425
+ return join9(home, ".pi", "agent", "langfuse.json");
2997
3426
  }
2998
3427
  function codexTracingConfigPath(home) {
2999
- return join8(home, ".codex", "langfuse.json");
3428
+ return join9(home, ".codex", "langfuse.json");
3000
3429
  }
3001
3430
  var claudeCodeTracingWriter = (home, settings) => {
3002
3431
  const path4 = claudeCodeSettingsPath(home);
@@ -3206,7 +3635,7 @@ function harnessBinPath(prefix) {
3206
3635
  }
3207
3636
 
3208
3637
  // src/lib/tracing/providers/langfuse.ts
3209
- import { join as join9 } from "node:path";
3638
+ import { join as join10 } from "node:path";
3210
3639
  var LANGFUSE_FRAGMENT_KEYS = [
3211
3640
  "LANGFUSE_PUBLIC_KEY",
3212
3641
  "LANGFUSE_SECRET_KEY",
@@ -3214,7 +3643,7 @@ var LANGFUSE_FRAGMENT_KEYS = [
3214
3643
  "LANGFUSE_TRACING_ENVIRONMENT"
3215
3644
  ];
3216
3645
  function langfuseFragmentPath(home) {
3217
- return join9(home, ".config", "agro", "langfuse.env");
3646
+ return join10(home, ".config", "agro", "langfuse.env");
3218
3647
  }
3219
3648
  function loadLangfuseCredentials(root) {
3220
3649
  const publicKey = readSecret(root, "LANGFUSE_PUBLIC_KEY");
@@ -3378,7 +3807,7 @@ function displayPath(home, path4) {
3378
3807
  return rel.startsWith("..") ? path4 : `~/${rel}`;
3379
3808
  }
3380
3809
  function withTempHome(fn) {
3381
- const dir = mkdtempSync2(join10(tmpdir2(), "agro-langfuse-"));
3810
+ const dir = mkdtempSync2(join11(tmpdir2(), "agro-langfuse-"));
3382
3811
  try {
3383
3812
  return fn(dir);
3384
3813
  } finally {
@@ -3387,10 +3816,10 @@ function withTempHome(fn) {
3387
3816
  }
3388
3817
  function probeGeneratedFile(home, render) {
3389
3818
  const rel = withTempHome((scratch) => relative(scratch, render(scratch).path));
3390
- const path4 = join10(home, rel);
3819
+ const path4 = join11(home, rel);
3391
3820
  if (!existsSync14(path4)) return { path: path4, state: "missing" };
3392
3821
  const outcome = withTempHome((scratch) => {
3393
- const copy = join10(scratch, rel);
3822
+ const copy = join11(scratch, rel);
3394
3823
  mkdirSync6(dirname7(copy), { recursive: true });
3395
3824
  copyFileSync2(path4, copy);
3396
3825
  return render(scratch).outcome;
@@ -3453,7 +3882,7 @@ async function runLangfuseApply(opts, io) {
3453
3882
  return renderAll(ctx, resolved, io, opts.bin, "apply");
3454
3883
  }
3455
3884
  function zshenvWarning(home) {
3456
- const zshenv = join10(home, ".zshenv");
3885
+ const zshenv = join11(home, ".zshenv");
3457
3886
  if (!existsSync14(zshenv)) return void 0;
3458
3887
  if (readFileSync10(zshenv, "utf8").includes("langfuse.env")) return void 0;
3459
3888
  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
@@ -3734,45 +4163,18 @@ async function runLangfuseSetup(opts, io) {
3734
4163
  // src/commands/sandbox.ts
3735
4164
  import { existsSync as existsSync16, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
3736
4165
  import { tmpdir as tmpdir3 } from "node:os";
3737
- import { join as join12, resolve as resolve7 } from "node:path";
3738
-
3739
- // src/lib/runtimes/catalog.ts
3740
- var RUNTIME_CATALOG = Object.freeze([
3741
- Object.freeze({
3742
- id: "docker",
3743
- title: "Docker container",
3744
- tier: "container",
3745
- state: "active",
3746
- provisionable: true,
3747
- docsPath: "docs/runtimes/docker.md"
3748
- }),
3749
- Object.freeze({
3750
- id: "microsandbox",
3751
- title: "MicroSandbox",
3752
- tier: "microvm",
3753
- state: "planned",
3754
- 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\`.`,
3756
- docsPath: "docs/runtimes/microsandbox.md"
3757
- })
3758
- ]);
3759
- function findRuntime(id) {
3760
- return RUNTIME_CATALOG.find((r) => r.id === id);
3761
- }
3762
- function runtimeIds() {
3763
- return RUNTIME_CATALOG.map((r) => r.id);
3764
- }
4166
+ import { join as join13, resolve as resolve9 } from "node:path";
3765
4167
 
3766
4168
  // src/lib/star-prompt.ts
3767
4169
  import { existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "node:fs";
3768
- import { join as join11 } from "node:path";
4170
+ import { join as join12 } from "node:path";
3769
4171
  var STAR_PROMPT_LINE = "\u2B50 If AGRO helps, star https://github.com/mifunedev/agro";
3770
4172
  var MARKER = "star-prompt-shown";
3771
4173
  function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTTY === true, stateHome = resolveUserStateHome(env)) {
3772
4174
  if (env.AGRO_NO_STAR_PROMPT === "1") return;
3773
4175
  if (env.CI !== void 0 && env.CI !== "") return;
3774
4176
  if (!isTTY) return;
3775
- const marker = join11(stateHome, MARKER);
4177
+ const marker = join12(stateHome, MARKER);
3776
4178
  if (existsSync15(marker)) return;
3777
4179
  io.stdout(`${STAR_PROMPT_LINE}
3778
4180
  `);
@@ -3787,7 +4189,7 @@ function maybePrintStarPrompt(io, env = process.env, isTTY = process.stdout.isTT
3787
4189
  // src/commands/sandbox-list.ts
3788
4190
  async function entryStatus(root, name, run) {
3789
4191
  try {
3790
- const target = resolveExecutionTarget({ projectRoot: root, container: name, run });
4192
+ const target = resolveExecutionTarget({ projectRoot: root, container: entrySandboxName(root) ?? name, run });
3791
4193
  return await target.status();
3792
4194
  } catch {
3793
4195
  return "unknown";
@@ -3849,10 +4251,10 @@ function gitIdentity(run, key) {
3849
4251
  return (result.stdout ?? "").trim();
3850
4252
  }
3851
4253
  function isBuildCapable(repo) {
3852
- return repo !== void 0 && existsSync16(join12(repo, ".devcontainer", "Dockerfile"));
4254
+ return repo !== void 0 && existsSync16(join13(repo, ".devcontainer", "Dockerfile"));
3853
4255
  }
3854
4256
  function prepareHomeMount(value) {
3855
- const path4 = resolve7(value);
4257
+ const path4 = resolve9(value);
3856
4258
  if (existsSync16(path4)) {
3857
4259
  if (!statSync4(path4).isDirectory()) {
3858
4260
  throw new Error(`--home-mount path exists and is not a directory: ${path4}`);
@@ -3896,18 +4298,18 @@ function mergeSettings(...sources) {
3896
4298
  for (const source of present) overlaySettings(merged, source);
3897
4299
  return merged;
3898
4300
  }
3899
- function nonEmpty(value) {
4301
+ function nonEmpty2(value) {
3900
4302
  return value === void 0 || value === "" ? void 0 : value;
3901
4303
  }
3902
- function seedConfig(name, checkout, seed, run) {
4304
+ function seedConfig(name, checkout, seed, run, runtime = "docker") {
3903
4305
  const config = overlaySettings(defaultAgroConfig(name), seed);
3904
4306
  config.name = name;
3905
- config.runtime = "docker";
4307
+ config.runtime = runtime;
3906
4308
  if (checkout !== void 0) config.checkout = checkout;
3907
- config.timezone = nonEmpty(seed?.timezone) ?? hostTimezone();
4309
+ config.timezone = nonEmpty2(seed?.timezone) ?? hostTimezone();
3908
4310
  config.git = {
3909
- userName: nonEmpty(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
3910
- userEmail: nonEmpty(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
4311
+ userName: nonEmpty2(seed?.git?.userName) ?? gitIdentity(run, "user.name"),
4312
+ userEmail: nonEmpty2(seed?.git?.userEmail) ?? gitIdentity(run, "user.email")
3911
4313
  };
3912
4314
  config.image = {
3913
4315
  ...config.image,
@@ -3948,7 +4350,7 @@ async function runWizard(config, io) {
3948
4350
  "Host path for /home/sandbox (blank keeps the Docker-managed volume)",
3949
4351
  config.storage?.homePath ?? ""
3950
4352
  );
3951
- if (homePath !== "") config.storage = { ...config.storage, homePath: resolve7(homePath) };
4353
+ if (homePath !== "") config.storage = { ...config.storage, homePath: resolve9(homePath) };
3952
4354
  }
3953
4355
  async function runSandboxInstall(opts, io) {
3954
4356
  const run = opts.run ?? spawnRunner;
@@ -3965,7 +4367,8 @@ async function runSandboxInstall(opts, io) {
3965
4367
  `);
3966
4368
  return 1;
3967
4369
  }
3968
- const checkout = opts.checkout === void 0 ? void 0 : resolve7(opts.checkout);
4370
+ if (runtime.id === "openshell") return await installOpenShell(opts, io, run);
4371
+ const checkout = opts.checkout === void 0 ? void 0 : resolve9(opts.checkout);
3969
4372
  if (checkout !== void 0 && !existsSync16(checkout)) {
3970
4373
  io.stderr(`${opts.bin} sandbox install: --checkout directory does not exist: ${checkout}
3971
4374
  `);
@@ -3983,9 +4386,9 @@ async function runSandboxInstall(opts, io) {
3983
4386
  const config = seedConfig(name, checkout, mergeSettings(readEntryConfig(name), repoSeed), run);
3984
4387
  const buildRequested = config.image?.mode === "build" && opts.noBuild !== true && opts.image !== true && opts.imageRef === void 0;
3985
4388
  if (buildRequested && !isBuildCapable(configCheckout(config))) {
3986
- const target = configCheckout(config) ?? resolve7(opts.checkout ?? ".");
4389
+ const target = configCheckout(config) ?? resolve9(opts.checkout ?? ".");
3987
4390
  io.stderr(
3988
- `${opts.bin} sandbox install: image.mode is "build" but ${join12(target, ".devcontainer", "Dockerfile")} does not exist \u2014 point --checkout <dir> at a harness checkout that has .devcontainer/Dockerfile, or set image.mode to "image" to run the prebuilt image
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
3989
4392
  `
3990
4393
  );
3991
4394
  return 1;
@@ -4029,7 +4432,7 @@ async function runSandboxInstall(opts, io) {
4029
4432
  ...useNoBuild ? { noBuild: true } : {}
4030
4433
  };
4031
4434
  if (opts.printArgv === true) {
4032
- const preview = mkdtempSync3(join12(tmpdir3(), "oh-sandbox-preview-"));
4435
+ const preview = mkdtempSync3(join13(tmpdir3(), "oh-sandbox-preview-"));
4033
4436
  try {
4034
4437
  writeAgroConfig(preview, config);
4035
4438
  materialize(preview, { ...configCheckout(config) !== void 0 ? { checkout: configCheckout(config) } : {} });
@@ -4050,16 +4453,77 @@ async function runSandboxInstall(opts, io) {
4050
4453
  }
4051
4454
  return code;
4052
4455
  }
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
+ }
4053
4517
 
4054
4518
  // src/services/sandbox-upgrade.ts
4055
4519
  import { closeSync as closeSync2, existsSync as existsSync17, openSync as openSync2, readFileSync as readFileSync11, unlinkSync as unlinkSync5, writeSync } from "node:fs";
4056
- import { join as join13 } from "node:path";
4520
+ import { join as join14 } from "node:path";
4057
4521
  function errorMessage(error) {
4058
4522
  return error instanceof Error ? error.message : String(error);
4059
4523
  }
4060
4524
  function conflictingDotenvImage(root, imageRef) {
4061
- const rootEnv = join13(root, ".env");
4062
- const fallbackEnv = join13(root, ".devcontainer", ".env");
4525
+ const rootEnv = join14(root, ".env");
4526
+ const fallbackEnv = join14(root, ".devcontainer", ".env");
4063
4527
  const file = existsSync17(rootEnv) ? rootEnv : fallbackEnv;
4064
4528
  if (!existsSync17(file)) return void 0;
4065
4529
  for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
@@ -4097,7 +4561,8 @@ async function runSandboxUpgrade(opts, io) {
4097
4561
  `);
4098
4562
  return 1;
4099
4563
  }
4100
- const lock = join13(root, ".sandbox-upgrade.lock");
4564
+ routeVerb(root, "upgrade", entrySandboxName(root) ?? opts.name);
4565
+ const lock = join14(root, ".sandbox-upgrade.lock");
4101
4566
  let fd;
4102
4567
  try {
4103
4568
  fd = openSync2(lock, "wx", 384);
@@ -4212,6 +4677,10 @@ identity, SSH (with its host port), the host Docker socket, and the host path
4212
4677
  for /home/sandbox. With --checkout it seeds those answers from that
4213
4678
  checkout's ${stateNames(bin).configFile}.
4214
4679
 
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
+
4215
4684
  Flags:
4216
4685
  --name <name> Registry entry name (default: the lowest free ${DEFAULT_NAME_PREFIX}<n>)
4217
4686
  --checkout <dir> Bind this checkout into the sandbox and seed the defaults
@@ -4234,7 +4703,7 @@ Flags:
4234
4703
  ${stateNames(bin).configFile} image.ref > ${officialImageRef(AGRO_VERSION)}
4235
4704
  --image=<ref> Run a custom image (implies --image); conflicts with --version
4236
4705
  --no-build Suppress the local build and reuse an existing image
4237
- --print-argv Print the docker compose argv that would run, then exit
4706
+ --print-argv Print the docker compose or openshell argv that would run, then exit
4238
4707
  without writing an entry
4239
4708
  --json Machine-readable output (list)
4240
4709
 
@@ -4427,7 +4896,7 @@ async function runSandboxCommand(rest, bin) {
4427
4896
  // src/commands/harness.ts
4428
4897
  import { existsSync as existsSync20 } from "node:fs";
4429
4898
  import { homedir as homedir3 } from "node:os";
4430
- import { delimiter as delimiter2, join as join16 } from "node:path";
4899
+ import { delimiter as delimiter2, join as join17 } from "node:path";
4431
4900
 
4432
4901
  // src/lib/docs.ts
4433
4902
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -4441,7 +4910,7 @@ function sourceDocsUrl(docsPath) {
4441
4910
 
4442
4911
  // src/lib/host-config.ts
4443
4912
  import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
4444
- import { basename as basename5, dirname as dirname8, isAbsolute, join as join14, resolve as resolve8 } from "node:path";
4913
+ import { basename as basename7, dirname as dirname8, isAbsolute, join as join15, resolve as resolve10 } from "node:path";
4445
4914
  var HOST_CONFIG_MODE = 420;
4446
4915
  var HOST_CONFIG_FILE = "config.json";
4447
4916
  var WORKSPACES_SUBDIR = "workspaces";
@@ -4453,7 +4922,7 @@ function hostStateHome(env, home) {
4453
4922
  return resolveUserStateHome(env, home);
4454
4923
  }
4455
4924
  function hostConfigPath(env, home) {
4456
- return join14(hostStateHome(env, home), HOST_CONFIG_FILE);
4925
+ return join15(hostStateHome(env, home), HOST_CONFIG_FILE);
4457
4926
  }
4458
4927
  function assertWorkspaceName(name) {
4459
4928
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -4463,14 +4932,14 @@ function assertWorkspaceName(name) {
4463
4932
  }
4464
4933
  }
4465
4934
  function workspacesRoot(env, home) {
4466
- return join14(hostStateHome(env, home), WORKSPACES_SUBDIR);
4935
+ return join15(hostStateHome(env, home), WORKSPACES_SUBDIR);
4467
4936
  }
4468
4937
  function workspaceRoot(name, env, home) {
4469
4938
  assertWorkspaceName(name);
4470
- return join14(workspacesRoot(env, home), name);
4939
+ return join15(workspacesRoot(env, home), name);
4471
4940
  }
4472
4941
  function defaultHarnessRoot(env, home) {
4473
- return join14(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4942
+ return join15(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
4474
4943
  }
4475
4944
  function validateHostConfig(value) {
4476
4945
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4547,7 +5016,7 @@ function readHostConfig(env, home) {
4547
5016
  try {
4548
5017
  parsed = JSON.parse(raw);
4549
5018
  } catch {
4550
- throw new Error(`${basename5(path4)} is not valid JSON: ${path4}`);
5019
+ throw new Error(`${basename7(path4)} is not valid JSON: ${path4}`);
4551
5020
  }
4552
5021
  return validateHostConfig(parsed);
4553
5022
  }
@@ -4570,15 +5039,15 @@ function writeHostConfig(config, env, home) {
4570
5039
  }
4571
5040
  }
4572
5041
  function resolveHarnessRoot(explicit, env, home) {
4573
- if (typeof explicit === "string" && explicit !== "") return resolve8(explicit);
5042
+ if (typeof explicit === "string" && explicit !== "") return resolve10(explicit);
4574
5043
  const configured = readHostConfig(env, home).harnessRoot;
4575
- if (typeof configured === "string" && configured !== "") return resolve8(configured);
5044
+ if (typeof configured === "string" && configured !== "") return resolve10(configured);
4576
5045
  return defaultHarnessRoot(env, home);
4577
5046
  }
4578
5047
  function recordHarnessRoot(root, env, home) {
4579
5048
  const config = readHostConfig(env, home);
4580
5049
  const recorded = config.harnessRoot;
4581
- if (typeof recorded === "string" && recorded !== "" && resolve8(recorded) === root) return;
5050
+ if (typeof recorded === "string" && recorded !== "" && resolve10(recorded) === root) return;
4582
5051
  writeHostConfig({ ...config, harnessRoot: root }, env, home);
4583
5052
  }
4584
5053
  function fieldError2(path4, requirement) {
@@ -4587,7 +5056,7 @@ function fieldError2(path4, requirement) {
4587
5056
 
4588
5057
  // src/lib/host-workspace.ts
4589
5058
  import { existsSync as existsSync19, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "node:fs";
4590
- import { dirname as dirname9, join as join15, resolve as resolve9 } from "node:path";
5059
+ import { dirname as dirname9, join as join16, resolve as resolve11 } from "node:path";
4591
5060
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
4592
5061
  var IGNORABLE_ENTRIES = [
4593
5062
  REGISTRY_SUBDIR,
@@ -4599,15 +5068,15 @@ function isDirectory3(path4) {
4599
5068
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
4600
5069
  }
4601
5070
  function stateHomes(home) {
4602
- return STATE_HOME_DIRS.map((name) => join15(home, name));
5071
+ return STATE_HOME_DIRS.map((name) => join16(home, name));
4603
5072
  }
4604
5073
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
4605
- const target = resolve9(root);
5074
+ const target = resolve11(root);
4606
5075
  const enclosing = stateHomes(home).find((state) => target === state);
4607
5076
  if (enclosing === void 0) return void 0;
4608
5077
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
4609
5078
  A workspace there collides with AGRO state and blocks every command run from ${home}.
4610
- Move it out \u2014 for example \`mv ${enclosing} ${join15(home, "agro")}\` \u2014 then re-run this command.
5079
+ Move it out \u2014 for example \`mv ${enclosing} ${join16(home, "agro")}\` \u2014 then re-run this command.
4611
5080
  `;
4612
5081
  }
4613
5082
  function isIgnorableEntry(name) {
@@ -4624,24 +5093,24 @@ function cloneInto(target, run, root, ref) {
4624
5093
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4625
5094
  }
4626
5095
  function cloneThroughStaging(root, run, ref) {
4627
- const staging = mkdtempSync4(join15(dirname9(root), "agro-clone-"));
5096
+ const staging = mkdtempSync4(join16(dirname9(root), "agro-clone-"));
4628
5097
  try {
4629
- const checkout = join15(staging, "agro");
5098
+ const checkout = join16(staging, "agro");
4630
5099
  cloneInto(checkout, run, root, ref);
4631
5100
  for (const name of readdirSync3(checkout)) {
4632
- const destination = join15(root, name);
5101
+ const destination = join16(root, name);
4633
5102
  if (existsSync19(destination)) {
4634
5103
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
4635
5104
  }
4636
- renameSync5(join15(checkout, name), destination);
5105
+ renameSync5(join16(checkout, name), destination);
4637
5106
  }
4638
5107
  } finally {
4639
5108
  rmSync4(staging, { recursive: true, force: true });
4640
5109
  }
4641
5110
  }
4642
5111
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4643
- const root = resolve9(path4);
4644
- if (existsSync19(join15(root, ".git"))) return { root, action: "reused" };
5112
+ const root = resolve11(path4);
5113
+ if (existsSync19(join16(root, ".git"))) return { root, action: "reused" };
4645
5114
  const stats = statSync5(root, { throwIfNoEntry: false });
4646
5115
  if (stats === void 0) {
4647
5116
  mkdirSync10(dirname9(root), { recursive: true });
@@ -4663,11 +5132,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4663
5132
  function listHostWorkspaces(env, home) {
4664
5133
  const root = workspacesRoot(env, home);
4665
5134
  if (!isDirectory3(root)) return [];
4666
- return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync19(join15(root, name, ".git"))).sort();
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();
4667
5136
  }
4668
5137
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
4669
- const root = resolve9(path4);
4670
- if (existsSync19(join15(root, ".git"))) return { ok: true, root };
5138
+ const root = resolve11(path4);
5139
+ if (existsSync19(join16(root, ".git"))) return { ok: true, root };
4671
5140
  const existing = listHostWorkspaces(env, home);
4672
5141
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
4673
5142
  `;
@@ -4697,7 +5166,7 @@ function targetFor(root, run, env) {
4697
5166
  });
4698
5167
  }
4699
5168
  function hostPrefix(home) {
4700
- return join16(home, ".local");
5169
+ return join17(home, ".local");
4701
5170
  }
4702
5171
  function pathEntries(env) {
4703
5172
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -4744,7 +5213,7 @@ async function sandboxMarkerExists(target, entry) {
4744
5213
  return r.exitCode === 0;
4745
5214
  }
4746
5215
  function hostBinaryUnderPrefix(entry, prefix) {
4747
- return existsSync20(join16(harnessBinPath(prefix), entry.binary));
5216
+ return existsSync20(join17(harnessBinPath(prefix), entry.binary));
4748
5217
  }
4749
5218
  function stateOf(entry, installed, location) {
4750
5219
  return {
@@ -4765,7 +5234,7 @@ function probeableHost(env, home) {
4765
5234
  return void 0;
4766
5235
  }
4767
5236
  const prefix = hostPrefix(home);
4768
- if (!existsSync20(join16(root, ".git")) && !existsSync20(prefix)) return void 0;
5237
+ if (!existsSync20(join17(root, ".git")) && !existsSync20(prefix)) return void 0;
4769
5238
  return { root, prefix };
4770
5239
  }
4771
5240
  async function collectStates(root, run, home, env, only) {
@@ -5232,7 +5701,7 @@ async function runHarnessInstall(name, opts, io) {
5232
5701
  // src/commands/workspace.ts
5233
5702
  import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
5234
5703
  import { homedir as homedir4 } from "node:os";
5235
- import { join as join17, resolve as resolve10 } from "node:path";
5704
+ import { join as join18, resolve as resolve12 } from "node:path";
5236
5705
  function homeOf2(opts) {
5237
5706
  return (opts.homedir ?? homedir4)();
5238
5707
  }
@@ -5241,7 +5710,7 @@ function messageOf2(error) {
5241
5710
  }
5242
5711
  function recordedRoot(env, home) {
5243
5712
  const configured = readHostConfig(env, home).harnessRoot;
5244
- return typeof configured === "string" && configured !== "" ? resolve10(configured) : void 0;
5713
+ return typeof configured === "string" && configured !== "" ? resolve12(configured) : void 0;
5245
5714
  }
5246
5715
  function renderTable2(rows, io) {
5247
5716
  const header2 = ["WORKSPACE", "DEFAULT", "PATH"];
@@ -5266,7 +5735,7 @@ async function runWorkspaceCreate(name, opts, io) {
5266
5735
  let root;
5267
5736
  const chosen = name ?? DEFAULT_WORKSPACE_NAME;
5268
5737
  try {
5269
- root = opts.path !== void 0 ? resolve10(opts.path) : workspaceRoot(chosen, env, home);
5738
+ root = opts.path !== void 0 ? resolve12(opts.path) : workspaceRoot(chosen, env, home);
5270
5739
  } catch (err) {
5271
5740
  io.stderr(`${bin} workspace: ${messageOf2(err)}
5272
5741
  `);
@@ -5280,7 +5749,7 @@ async function runWorkspaceCreate(name, opts, io) {
5280
5749
  let action;
5281
5750
  const preexisting = existsSync21(root);
5282
5751
  try {
5283
- if (!opts.json && !existsSync21(join17(root, ".git"))) {
5752
+ if (!opts.json && !existsSync21(join18(root, ".git"))) {
5284
5753
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
5285
5754
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
5286
5755
  `);
@@ -5325,7 +5794,7 @@ async function runWorkspaceList(opts, io) {
5325
5794
  const recorded = recordedRoot(env, home);
5326
5795
  const registry = workspacesRoot(env, home);
5327
5796
  rows = listHostWorkspaces(env, home).map((name) => {
5328
- const root = join17(registry, name);
5797
+ const root = join18(registry, name);
5329
5798
  return { name, root, default: recorded === root };
5330
5799
  });
5331
5800
  } catch (err) {
@@ -5352,7 +5821,7 @@ async function runWorkspaceList(opts, io) {
5352
5821
  // src/commands/tool.ts
5353
5822
  import { existsSync as existsSync22 } from "node:fs";
5354
5823
  import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
5355
- import { delimiter as delimiter3, join as join18 } from "node:path";
5824
+ import { delimiter as delimiter3, join as join19 } from "node:path";
5356
5825
 
5357
5826
  // src/lib/tools/catalog.ts
5358
5827
  var TOOLS_DOC = "docs/installation.md";
@@ -5783,7 +6252,7 @@ function targetFor2(root, run, env) {
5783
6252
  });
5784
6253
  }
5785
6254
  function hostPrefix2(home) {
5786
- return join18(home, ".local");
6255
+ return join19(home, ".local");
5787
6256
  }
5788
6257
  function sandboxMarkerPath2(entry) {
5789
6258
  return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
@@ -5797,7 +6266,7 @@ async function sandboxMarkerExists2(target, entry) {
5797
6266
  return r.exitCode === 0;
5798
6267
  }
5799
6268
  function hostBinaryUnderPrefix2(entry, prefix) {
5800
- return existsSync22(join18(harnessBinPath(prefix), entry.binary));
6269
+ return existsSync22(join19(harnessBinPath(prefix), entry.binary));
5801
6270
  }
5802
6271
  function pathEntries2(env) {
5803
6272
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -5877,7 +6346,7 @@ function probeableHost2(env, home) {
5877
6346
  return void 0;
5878
6347
  }
5879
6348
  const prefix = hostPrefix2(home);
5880
- if (!existsSync22(join18(root, ".git")) && !existsSync22(prefix)) return void 0;
6349
+ if (!existsSync22(join19(root, ".git")) && !existsSync22(prefix)) return void 0;
5881
6350
  return { root, prefix };
5882
6351
  }
5883
6352
  async function collectRows(root, run, home, env, only) {
@@ -6414,7 +6883,7 @@ function fetchRemoteSource(opts) {
6414
6883
  }
6415
6884
 
6416
6885
  // src/cli.ts
6417
- var DEFAULT_SOURCE_CONTROL_DIR = resolve11(
6886
+ var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
6418
6887
  dirname10(fileURLToPath(import.meta.url)),
6419
6888
  "../.."
6420
6889
  );
@@ -7262,18 +7731,18 @@ function parseGatewayArgs(rest) {
7262
7731
  return { ok: true, args: { help: false, passthrough: [...rest] } };
7263
7732
  }
7264
7733
  function bundledPayloadExists(bundled, exists = existsSync23) {
7265
- return exists(join19(bundled.sourceControlDir, "manifest.json"));
7734
+ return exists(join20(bundled.sourceControlDir, "manifest.json"));
7266
7735
  }
7267
7736
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
7268
7737
  const exists = bundled.exists ?? existsSync23;
7269
7738
  if (args.fromDir !== void 0) {
7270
- return { kind: "local", fromDir: resolve11(args.fromDir) };
7739
+ return { kind: "local", fromDir: resolve13(args.fromDir) };
7271
7740
  }
7272
7741
  if (args.fromRemote) {
7273
7742
  return { kind: "remote", ref: args.ref };
7274
7743
  }
7275
7744
  if (bundledPayloadExists(bundled, exists)) {
7276
- return { kind: "local", fromDir: resolve11(bundled.sourceControlDir, "..") };
7745
+ return { kind: "local", fromDir: resolve13(bundled.sourceControlDir, "..") };
7277
7746
  }
7278
7747
  return {
7279
7748
  kind: "remote",
@@ -7285,7 +7754,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
7285
7754
  function readPayloadVersion(checkoutDir) {
7286
7755
  try {
7287
7756
  const parsed = JSON.parse(
7288
- readFileSync13(join19(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7757
+ readFileSync13(join20(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
7289
7758
  );
7290
7759
  if (parsed && typeof parsed.version === "string") return parsed.version;
7291
7760
  } catch {
@@ -7294,7 +7763,7 @@ function readPayloadVersion(checkoutDir) {
7294
7763
  }
7295
7764
  async function runWithRemoteSource(hooks, fn) {
7296
7765
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
7297
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join19(tmpdir4(), "oh-remote-")));
7766
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join20(tmpdir4(), "oh-remote-")));
7298
7767
  const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
7299
7768
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
7300
7769
  const checkoutDir = mkdtemp();
@@ -7654,7 +8123,19 @@ function lifecycleIo() {
7654
8123
  stderr: (s) => process.stderr.write(s)
7655
8124
  };
7656
8125
  }
7657
- main(process.argv.slice(2)).then(
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(
7658
8139
  (code) => process.exit(code),
7659
8140
  (err) => {
7660
8141
  const msg = err instanceof Error ? err.message : String(err);
@@ -7699,6 +8180,7 @@ export {
7699
8180
  printVendorHelp,
7700
8181
  printWorkspaceHelp,
7701
8182
  resolveVendorSource,
8183
+ runCli,
7702
8184
  runWithRemoteSource,
7703
8185
  runtimeLines
7704
8186
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.16.0",
3
+ "version": "0.17.0-rc.1",
4
4
  "description": "AGRO CLI — create and manage sandboxes for coding agents.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/mifunedev/agro#readme",