@mifune/agro 0.15.0-minimal.1 → 0.15.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 (2) hide show
  1. package/dist/agro.js +289 -21
  2. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // src/cli.ts
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { dirname as dirname10, join as join17, resolve as resolve11 } from "node:path";
11
- import { existsSync as existsSync21, mkdtempSync as mkdtempSync5, readFileSync as readFileSync12, rmSync as rmSync5 } from "node:fs";
11
+ import { existsSync as existsSync21, mkdtempSync as mkdtempSync5, readFileSync as readFileSync12, rmSync as rmSync6 } from "node:fs";
12
12
  import { tmpdir as tmpdir4 } from "node:os";
13
13
 
14
14
  // src/commands/update.ts
@@ -1479,6 +1479,11 @@ fi
1479
1479
  # shellcheck source=paths.sh
1480
1480
  . "$PATHS_SH"
1481
1481
 
1482
+ MIGRATOR="$SCRIPT_DIR/migrate-harness-yaml.sh"
1483
+ if [ -f "$REPO_DIR/harness.yaml" ] && [ -f "$MIGRATOR" ]; then
1484
+ sh "$MIGRATOR" "$REPO_DIR" >&2 || true
1485
+ fi
1486
+
1482
1487
  ENV_FILE="$REPO_DIR/.env"
1483
1488
  [ -f "$ENV_FILE" ] || ENV_FILE="$REPO_DIR/.devcontainer/.env"
1484
1489
 
@@ -2217,7 +2222,7 @@ function resolveExecutionTarget(opts) {
2217
2222
  }
2218
2223
 
2219
2224
  // src/lib/version.ts
2220
- var AGRO_VERSION = true ? "0.15.0-minimal.1" : "0.0.0-dev";
2225
+ var AGRO_VERSION = true ? "0.15.0" : "0.0.0-dev";
2221
2226
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2222
2227
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2223
2228
  function parseReleaseVersion(value) {
@@ -4205,14 +4210,15 @@ function assertCloned(result, root) {
4205
4210
  throw new Error(`could not clone ${AGRO_REPO_URL} into ${root} (exit status ${String(status)})`);
4206
4211
  }
4207
4212
  }
4208
- function cloneInto(target, run, root) {
4209
- assertCloned(run("git", ["clone", AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4213
+ function cloneInto(target, run, root, ref) {
4214
+ const branch = ref !== void 0 ? ["--branch", ref] : [];
4215
+ assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
4210
4216
  }
4211
- function cloneThroughStaging(root, run) {
4217
+ function cloneThroughStaging(root, run, ref) {
4212
4218
  const staging = mkdtempSync4(join13(dirname9(root), "agro-clone-"));
4213
4219
  try {
4214
4220
  const checkout = join13(staging, "agro");
4215
- cloneInto(checkout, run, root);
4221
+ cloneInto(checkout, run, root, ref);
4216
4222
  for (const name of readdirSync3(checkout)) {
4217
4223
  const destination = join13(root, name);
4218
4224
  if (existsSync17(destination)) {
@@ -4224,13 +4230,13 @@ function cloneThroughStaging(root, run) {
4224
4230
  rmSync4(staging, { recursive: true, force: true });
4225
4231
  }
4226
4232
  }
4227
- function ensureHostWorkspace(path4, run = spawnRunner) {
4233
+ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4228
4234
  const root = resolve9(path4);
4229
4235
  if (existsSync17(join13(root, ".git"))) return { root, action: "reused" };
4230
4236
  const stats = statSync5(root, { throwIfNoEntry: false });
4231
4237
  if (stats === void 0) {
4232
4238
  mkdirSync9(dirname9(root), { recursive: true });
4233
- cloneInto(root, run, root);
4239
+ cloneInto(root, run, root, ref);
4234
4240
  return { root, action: "cloned" };
4235
4241
  }
4236
4242
  if (!stats.isDirectory()) {
@@ -4242,7 +4248,7 @@ function ensureHostWorkspace(path4, run = spawnRunner) {
4242
4248
  `${root} holds files but no git checkout (${blocking.sort().join(", ")}) \u2014 choose an empty directory or an existing AGRO checkout`
4243
4249
  );
4244
4250
  }
4245
- cloneThroughStaging(root, run);
4251
+ cloneThroughStaging(root, run, ref);
4246
4252
  return { root, action: "cloned" };
4247
4253
  }
4248
4254
  function listHostWorkspaces(env, home) {
@@ -4431,6 +4437,19 @@ async function runHarnessStatus(name, opts, io) {
4431
4437
  function hermesTargetRoot(target) {
4432
4438
  return target.kind === "docker-compose" ? "/home/sandbox/harness" : target.workspace.targetRoot;
4433
4439
  }
4440
+ async function reconcileHermes(target, io, bin, user, root = hermesTargetRoot(target)) {
4441
+ const result = await target.exec({
4442
+ argv: remoteControlDirScript(root, "scripts/link-providers.sh", ["--init", "--hermes-only"]),
4443
+ env: agroEnvPair("PROJECT_ROOT", root),
4444
+ ...user ? { user } : {},
4445
+ stdio: "inherit"
4446
+ });
4447
+ if (result.exitCode !== 0) {
4448
+ io.stderr(`${bin} harness: Hermes integration failed (exit ${result.exitCode}); no installation success reported.
4449
+ `);
4450
+ }
4451
+ return result.exitCode;
4452
+ }
4434
4453
  function unreachableReason(status) {
4435
4454
  return runtimeIsAbsent(status) ? "No container runtime is on PATH." : `The sandbox is not running (${status}).`;
4436
4455
  }
@@ -4513,7 +4532,7 @@ async function installOnHost(entry, opts, io, run, status) {
4513
4532
  });
4514
4533
  if (linked.exitCode !== 0) {
4515
4534
  io.stderr(
4516
- `${bin} harness: could not link provider hooks in ${root} (exit ${linked.exitCode}); nothing was installed.
4535
+ `${bin} harness: could not link provider skills in ${root} (exit ${linked.exitCode}); nothing was installed.
4517
4536
  Run: bash ${root}/.agro/scripts/link-providers.sh --init
4518
4537
  `
4519
4538
  );
@@ -4524,6 +4543,10 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4524
4543
  ...agroEnvPair("PROJECT_ROOT", root),
4525
4544
  HERMES_HOME: `${root}/.hermes`
4526
4545
  } : void 0;
4546
+ if (hermes) {
4547
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4548
+ if (code !== 0) return code;
4549
+ }
4527
4550
  if (await probeInstalled(target, entry, prefix, void 0, installEnv) === true) {
4528
4551
  io.stdout(`${entry.id}: already installed (${entry.binary})
4529
4552
  `);
@@ -4554,6 +4577,8 @@ Run: bash ${root}/.agro/scripts/link-providers.sh --init
4554
4577
  `);
4555
4578
  return 1;
4556
4579
  }
4580
+ const code = await reconcileHermes(target, io, bin, void 0, root);
4581
+ if (code !== 0) return code;
4557
4582
  }
4558
4583
  const receipt = {
4559
4584
  prefix,
@@ -4707,6 +4732,10 @@ async function runHarnessInstall(name, opts, io) {
4707
4732
  ...agroEnvPair("PROJECT_ROOT", hermesTargetRoot(target)),
4708
4733
  HERMES_HOME: `${hermesTargetRoot(target)}/.hermes`
4709
4734
  } : void 0;
4735
+ if (hermes) {
4736
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4737
+ if (code !== 0) return code;
4738
+ }
4710
4739
  const already = await probeInstalled(target, entry, SANDBOX_HARNESS_PREFIX, "sandbox", installEnv);
4711
4740
  if (already === true) {
4712
4741
  io.stdout(`${entry.id}: already installed (${entry.binary})
@@ -4736,6 +4765,8 @@ async function runHarnessInstall(name, opts, io) {
4736
4765
  `);
4737
4766
  return 1;
4738
4767
  }
4768
+ const code = await reconcileHermes(target, io, opts.bin, "sandbox");
4769
+ if (code !== 0) return code;
4739
4770
  }
4740
4771
  io.stdout(`${entry.id}: installed \u2014 see ${sourceDocsUrl(entry.docsPath)} for authentication
4741
4772
  `);
@@ -4743,7 +4774,7 @@ async function runHarnessInstall(name, opts, io) {
4743
4774
  }
4744
4775
 
4745
4776
  // src/commands/workspace.ts
4746
- import { existsSync as existsSync19 } from "node:fs";
4777
+ import { existsSync as existsSync19, rmSync as rmSync5 } from "node:fs";
4747
4778
  import { homedir as homedir4 } from "node:os";
4748
4779
  import { join as join15, resolve as resolve10 } from "node:path";
4749
4780
  function homeOf2(opts) {
@@ -4791,15 +4822,25 @@ async function runWorkspaceCreate(name, opts, io) {
4791
4822
  return 1;
4792
4823
  }
4793
4824
  let action;
4825
+ const preexisting = existsSync19(root);
4794
4826
  try {
4795
4827
  if (!opts.json && !existsSync19(join15(root, ".git"))) {
4796
- io.stdout(`cloning ${AGRO_REPO_URL} into ${root}\u2026
4828
+ const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
4829
+ io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
4797
4830
  `);
4798
4831
  }
4799
- const created = ensureHostWorkspace(root, run);
4832
+ const created = ensureHostWorkspace(root, run, opts.ref);
4800
4833
  root = created.root;
4801
4834
  action = created.action;
4802
4835
  } catch (err) {
4836
+ if (opts.ref !== void 0) {
4837
+ if (!preexisting) rmSync5(root, { recursive: true, force: true });
4838
+ io.stderr(
4839
+ `${bin} workspace: could not clone ref "${opts.ref}" \u2014 the ref does not exist in ${AGRO_REPO_URL} or the clone failed (${messageOf2(err)})
4840
+ `
4841
+ );
4842
+ return 1;
4843
+ }
4803
4844
  io.stderr(`${bin} workspace: ${messageOf2(err)}
4804
4845
  `);
4805
4846
  return 1;
@@ -4854,7 +4895,7 @@ async function runWorkspaceList(opts, io) {
4854
4895
 
4855
4896
  // src/commands/tool.ts
4856
4897
  import { existsSync as existsSync20 } from "node:fs";
4857
- import { homedir as homedir5 } from "node:os";
4898
+ import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
4858
4899
  import { delimiter as delimiter3, join as join16 } from "node:path";
4859
4900
 
4860
4901
  // src/lib/tools/catalog.ts
@@ -5076,6 +5117,178 @@ install -d -m 0700 "$HOME/.tailscale"`
5076
5117
  `${HARNESS_PREFIX_TOKEN}/bin/tailscaled`
5077
5118
  ]),
5078
5119
  docsPath: TOOLS_DOC
5120
+ }),
5121
+ Object.freeze({
5122
+ id: "code-server",
5123
+ title: "code-server",
5124
+ kind: "installable",
5125
+ binary: "code-server",
5126
+ verifyArgv: Object.freeze(["bash", "-lc", "command -v code-server >/dev/null"]),
5127
+ versionArgv: Object.freeze(["code-server", "--version"]),
5128
+ installArgv: Object.freeze([
5129
+ "bash",
5130
+ "-lc",
5131
+ [
5132
+ "set -e",
5133
+ "version=4.129.0",
5134
+ 'arch="$(dpkg --print-architecture)"',
5135
+ 'case "$arch" in',
5136
+ " amd64) sha=889b09ff3a167a293f53cb68a5a7f38dbab6bd2b50d7a5951c757e56ba51a2b0 ;;",
5137
+ " arm64) sha=62f7886018923a18cc16112ccfbcd51aee80f8e0c1bb7abc48773d1fd32a7617 ;;",
5138
+ ' *) echo "no pinned code-server build for $arch" >&2; exit 1 ;;',
5139
+ "esac",
5140
+ 'prefix="${NPM_USER_PREFIX:-$HOME/.local}"',
5141
+ 'dest="$prefix/lib/code-server-$version"',
5142
+ 'tmp="$(mktemp -d)"',
5143
+ `trap 'rm -rf "$tmp"' EXIT`,
5144
+ 'curl -fsSL "https://github.com/coder/code-server/releases/download/v$version/code-server-$version-linux-$arch.tar.gz" -o "$tmp/code-server.tar.gz"',
5145
+ 'echo "$sha $tmp/code-server.tar.gz" | sha256sum -c -',
5146
+ 'install -d "$tmp/release" "$prefix/lib" "$prefix/bin"',
5147
+ 'tar -xzf "$tmp/code-server.tar.gz" -C "$tmp/release" --strip-components=1',
5148
+ 'rm -rf "$dest"',
5149
+ 'mv "$tmp/release" "$dest"',
5150
+ 'ln -sfn "$dest/bin/code-server" "$prefix/bin/code-server"',
5151
+ 'if ! "$prefix/bin/code-server" --version | grep -q "^$version "; then',
5152
+ ' echo "$prefix/bin/code-server does not report version $version" >&2',
5153
+ " exit 1",
5154
+ "fi"
5155
+ ].join("\n")
5156
+ ]),
5157
+ installUser: "sandbox",
5158
+ hostCapable: true,
5159
+ uninstallArgv: Object.freeze([
5160
+ "rm",
5161
+ "-rf",
5162
+ `${HARNESS_PREFIX_TOKEN}/bin/code-server`,
5163
+ `${HARNESS_PREFIX_TOKEN}/lib/code-server-4.129.0`
5164
+ ]),
5165
+ docsPath: TOOLS_DOC
5166
+ }),
5167
+ Object.freeze({
5168
+ id: "docker-engine",
5169
+ title: "Docker Engine + Compose",
5170
+ kind: "installable",
5171
+ binary: "docker",
5172
+ verifyArgv: Object.freeze([
5173
+ "bash",
5174
+ "-lc",
5175
+ 'command -v docker >/dev/null && docker compose version >/dev/null && systemctl is-enabled --quiet docker && id -nG "${SUDO_USER:-$(id -un)}" | tr " " "\\n" | grep -qx docker'
5176
+ ]),
5177
+ versionArgv: Object.freeze(["docker", "--version"]),
5178
+ hostInstallArgv: Object.freeze([
5179
+ "bash",
5180
+ "-lc",
5181
+ [
5182
+ "set -e",
5183
+ ". /etc/os-release",
5184
+ 'if [ "${ID:-}" != ubuntu ]; then',
5185
+ ' echo "docker-engine installs from Docker\'s Ubuntu repository; this host is ${ID:-unknown}" >&2',
5186
+ " exit 1",
5187
+ "fi",
5188
+ "fingerprint=9DC858229FC7DD38854AE2D88D81803C0EBFCD88",
5189
+ 'user="${SUDO_USER:-$(id -un)}"',
5190
+ "export DEBIAN_FRONTEND=noninteractive",
5191
+ 'tmp="$(mktemp -d)"',
5192
+ `trap 'rm -rf "$tmp"' EXIT`,
5193
+ "apt-get update",
5194
+ "apt-get install -y ca-certificates curl gnupg",
5195
+ "install -m 0755 -d /etc/apt/keyrings",
5196
+ 'curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o "$tmp/docker.asc"',
5197
+ `actual="$(gpg --homedir "$tmp" --show-keys --with-colons "$tmp/docker.asc" | awk -F: '$1 == "fpr" { print $10; exit }')"`,
5198
+ 'if [ "$actual" != "$fingerprint" ]; then',
5199
+ ' echo "Docker\'s repository key has fingerprint ${actual:-none}, expected $fingerprint" >&2',
5200
+ " exit 1",
5201
+ "fi",
5202
+ 'install -m 0644 "$tmp/docker.asc" /etc/apt/keyrings/docker.asc',
5203
+ 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${UBUNTU_CODENAME:-$VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list',
5204
+ "apt-get update",
5205
+ "apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
5206
+ 'usermod -aG docker "$user"',
5207
+ "systemctl enable --now docker",
5208
+ 'echo "$user is in the docker group. Log out and back in before you run docker without sudo."'
5209
+ ].join("\n")
5210
+ ]),
5211
+ hostInstallUser: "root",
5212
+ notInstallableReason: (bin) => `Docker Engine installs on the host only, with \`${bin} tool install docker-engine --host\`. To use Docker inside the sandbox, set \`access.dockerSocket\` to true in agro.json on the host and recreate the sandbox. That mounts the host Docker socket, which is effectively host root.`,
5213
+ hostCapable: true,
5214
+ uninstallArgv: null,
5215
+ docsPath: TOOLS_DOC
5216
+ }),
5217
+ Object.freeze({
5218
+ id: "desktop",
5219
+ title: "XFCE desktop over XRDP",
5220
+ kind: "installable",
5221
+ binary: "xrdp",
5222
+ verifyArgv: Object.freeze([
5223
+ "bash",
5224
+ "-lc",
5225
+ 'command -v xrdp >/dev/null && systemctl is-enabled --quiet xrdp && systemctl is-enabled --quiet tailscaled && test -f /etc/systemd/system/xrdp.service.d/agro-tailscale-only.conf && grep -qx xfce4-session "$HOME/.xsession"'
5226
+ ]),
5227
+ hostInstallArgv: Object.freeze([
5228
+ "bash",
5229
+ "-lc",
5230
+ [
5231
+ "set -e",
5232
+ ". /etc/os-release",
5233
+ 'if [ "${ID:-}" != ubuntu ]; then',
5234
+ ' echo "desktop installs Ubuntu packages; this host is ${ID:-unknown}" >&2',
5235
+ " exit 1",
5236
+ "fi",
5237
+ 'user="${SUDO_USER:-$(id -un)}"',
5238
+ 'home="$(getent passwd "$user" | cut -d: -f6)"',
5239
+ 'codename="${UBUNTU_CODENAME:-$VERSION_CODENAME}"',
5240
+ "export DEBIAN_FRONTEND=noninteractive",
5241
+ 'tmp="$(mktemp -d)"',
5242
+ `trap 'rm -rf "$tmp"' EXIT`,
5243
+ "apt-get update",
5244
+ "apt-get install -y ca-certificates curl gnupg nftables",
5245
+ "if ! systemctl cat tailscaled.service >/dev/null 2>&1; then",
5246
+ " fingerprint=2596A99EAAB33821893C0A79458CA832957F5868",
5247
+ ' curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/$codename.noarmor.gpg" -o "$tmp/tailscale.gpg"',
5248
+ ` actual="$(gpg --homedir "$tmp" --show-keys --with-colons "$tmp/tailscale.gpg" | awk -F: '$1 == "fpr" { print $10; exit }')"`,
5249
+ ' if [ "$actual" != "$fingerprint" ]; then',
5250
+ ' echo "Tailscale\'s repository key has fingerprint ${actual:-none}, expected $fingerprint" >&2',
5251
+ " exit 1",
5252
+ " fi",
5253
+ ' install -m 0644 "$tmp/tailscale.gpg" /usr/share/keyrings/tailscale-archive-keyring.gpg',
5254
+ ' echo "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu $codename main" > /etc/apt/sources.list.d/tailscale.list',
5255
+ " apt-get update",
5256
+ " apt-get install -y tailscale",
5257
+ "fi",
5258
+ "systemctl enable --now tailscaled",
5259
+ "install -d -m 0755 /etc/xrdp /etc/systemd/system/xrdp.service.d",
5260
+ "cat > /etc/xrdp/agro-tailscale-only.nft <<'EOF'",
5261
+ "table inet agro_xrdp",
5262
+ "delete table inet agro_xrdp",
5263
+ "table inet agro_xrdp {",
5264
+ " chain input {",
5265
+ " type filter hook input priority filter; policy accept;",
5266
+ ' iifname { "lo", "tailscale0" } accept',
5267
+ " tcp dport 3389 reject with tcp reset",
5268
+ " }",
5269
+ "}",
5270
+ "EOF",
5271
+ "nft -f /etc/xrdp/agro-tailscale-only.nft",
5272
+ `printf '[Service]\\nExecStartPre=%s -f /etc/xrdp/agro-tailscale-only.nft\\n' "$(command -v nft)" > /etc/systemd/system/xrdp.service.d/agro-tailscale-only.conf`,
5273
+ "systemctl daemon-reload",
5274
+ "apt-get install -y xfce4 xfce4-goodies xrdp xorgxrdp",
5275
+ `printf '%s\\n' xfce4-session > "$home/.xsession"`,
5276
+ 'chown "$user:$(id -gn "$user")" "$home/.xsession"',
5277
+ "adduser xrdp ssl-cert",
5278
+ "systemctl daemon-reload",
5279
+ "systemctl enable xrdp",
5280
+ "systemctl restart xrdp",
5281
+ "systemctl is-active --quiet xrdp",
5282
+ 'echo "The desktop is installed. Two steps remain:"',
5283
+ 'echo " sudo tailscale up"',
5284
+ 'echo " sudo passwd $user"'
5285
+ ].join("\n")
5286
+ ]),
5287
+ hostInstallUser: "root",
5288
+ notInstallableReason: (bin) => `The desktop installs on the host only, with \`${bin} tool install desktop --host\`. It installs Tailscale and serves XFCE over XRDP on TCP 3389 through Tailscale only.`,
5289
+ hostCapable: true,
5290
+ uninstallArgv: null,
5291
+ docsPath: TOOLS_DOC
5079
5292
  })
5080
5293
  ]);
5081
5294
  function findTool(id) {
@@ -5138,6 +5351,10 @@ function homeOf3(opts) {
5138
5351
  function platformOf(opts) {
5139
5352
  return opts.platform ?? process.platform;
5140
5353
  }
5354
+ function isRootUser(opts) {
5355
+ const identity = opts.identity ?? (() => ({ name: userInfo2().username, uid: userInfo2().uid }));
5356
+ return identity().uid === 0;
5357
+ }
5141
5358
  function isInteractive2(opts) {
5142
5359
  return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
5143
5360
  }
@@ -5176,6 +5393,7 @@ function rowOf(entry, installed, version, location) {
5176
5393
  version,
5177
5394
  installable: entry.installArgv !== void 0,
5178
5395
  hostCapable: entry.hostCapable,
5396
+ hostRoot: entry.hostInstallUser === "root",
5179
5397
  location,
5180
5398
  docs: sourceDocsUrl(entry.docsPath)
5181
5399
  };
@@ -5223,13 +5441,20 @@ function cell2(value, absent) {
5223
5441
  function renderTable3(collected, io, bin) {
5224
5442
  const rows = collected.rows;
5225
5443
  const header2 = ["TOOL", "KIND", "INSTALLED"];
5226
- const body = rows.map((r) => [r.id, r.kind, cell2(r.installed, "?")]);
5444
+ const body = rows.map((r) => [
5445
+ r.id,
5446
+ r.hostRoot ? `${r.kind} (root)` : r.kind,
5447
+ cell2(r.installed, "?")
5448
+ ]);
5227
5449
  const widths = header2.map(
5228
5450
  (h, i) => Math.max(h.length, ...body.map((b) => b[i].length))
5229
5451
  );
5230
5452
  const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
5231
5453
  io.stdout(line(header2));
5232
5454
  for (const row of body) io.stdout(line(row));
5455
+ if (rows.some((r) => r.hostRoot)) {
5456
+ io.stdout("\n(root) installs on the host as root through `sudo -n`.\n");
5457
+ }
5233
5458
  if (rows.some((r) => r.location === "host")) {
5234
5459
  io.stdout(
5235
5460
  `
@@ -5409,11 +5634,21 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5409
5634
  }
5410
5635
  return 0;
5411
5636
  }
5637
+ const sudo = entry.hostInstallUser === "root" && !isRootUser(opts);
5638
+ if (sudo && (await tryExec(target, ["sudo", "-n", "true"], void 0))?.exitCode !== 0) {
5639
+ io.stderr(
5640
+ `${bin} tool: ${entry.id} installs as root on the host and needs passwordless \`sudo\`.
5641
+ \`sudo -n true\` failed. Configure passwordless sudo for this user, or run as root.
5642
+ `
5643
+ );
5644
+ return 1;
5645
+ }
5412
5646
  if (!await confirmDownload(entry, opts, io, true)) return 1;
5413
5647
  io.stdout(`installing ${entry.title} on the host\u2026
5414
5648
  `);
5649
+ const argv = resolveToolInstallArgv(entry, true);
5415
5650
  const r = await target.exec({
5416
- argv: resolveToolInstallArgv(entry, true),
5651
+ argv: sudo ? ["sudo", "-n", "--", ...argv] : argv,
5417
5652
  stdio: "inherit",
5418
5653
  env: installEnv
5419
5654
  });
@@ -5445,6 +5680,11 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5445
5680
  `);
5446
5681
  return 1;
5447
5682
  }
5683
+ if (entry.hostInstallUser === "root") {
5684
+ io.stdout(`${entry.id} installed on the host \u2014 see ${sourceDocsUrl(entry.docsPath)}
5685
+ `);
5686
+ return 0;
5687
+ }
5448
5688
  io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)}
5449
5689
  `);
5450
5690
  if (!onPath2(prefix, env)) {
@@ -5460,7 +5700,8 @@ async function runToolInstall(name, opts, io) {
5460
5700
  const root = resolveProjectRoot(opts.cwd);
5461
5701
  const entry = findTool(name);
5462
5702
  if (!entry) return unknownTool(name, io, opts.bin);
5463
- if (entry.installArgv === void 0) {
5703
+ const hostOnly = entry.installArgv === void 0 && entry.hostInstallArgv !== void 0;
5704
+ const refuseContainer = () => {
5464
5705
  io.stderr(`${opts.bin} tool: ${entry.id} cannot be installed by this command.
5465
5706
 
5466
5707
  `);
@@ -5471,12 +5712,16 @@ async function runToolInstall(name, opts, io) {
5471
5712
  ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5472
5713
  `);
5473
5714
  return 1;
5715
+ };
5716
+ if (entry.installArgv === void 0 && (!hostOnly || runningInsideSandbox(opts.env ?? process.env))) {
5717
+ return refuseContainer();
5474
5718
  }
5475
5719
  const target = targetFor2(root, run, opts.env);
5476
5720
  const status = await resolveTargetStatus(target);
5477
- if (!isReachable2(status)) {
5721
+ if (!isReachable2(status) || hostOnly && opts.host === true) {
5478
5722
  return await installOnHost2(entry, opts, io, run, status);
5479
5723
  }
5724
+ if (entry.installArgv === void 0) return refuseContainer();
5480
5725
  const already = await probeInstalled2(target, entry, "sandbox");
5481
5726
  if (already === true) {
5482
5727
  io.stdout(`${entry.id}: already installed (${entry.binary})
@@ -5951,7 +6196,8 @@ function printWorkspaceHelp(bin = AGRO_PRODUCT.bin) {
5951
6196
  process.stdout.write(`${bin} workspace \u2014 Create and list host AGRO workspaces
5952
6197
 
5953
6198
  Usage:
5954
- ${bin} workspace create [<name>] Clone the AGRO repository into a registry entry
6199
+ ${bin} workspace create [<name>] [--ref <ref>]
6200
+ Clone the AGRO repository into a registry entry
5955
6201
  ${bin} workspace list List every host workspace
5956
6202
 
5957
6203
  A host workspace is an AGRO checkout under \`~/.agro/workspaces/<name>\`. \`create\`
@@ -5966,6 +6212,9 @@ root as \`harnessRoot\`. \`list\` marks the recorded root.
5966
6212
  Flags:
5967
6213
  --json Machine-readable output
5968
6214
  --path <dir> Create the workspace at a directory outside the registry
6215
+ --ref <ref> Clone this branch or tag instead of the default branch (create
6216
+ only). A missing ref exits 1 and leaves no target directory.
6217
+ An existing checkout is reused as is.
5969
6218
  `);
5970
6219
  }
5971
6220
  function printGatewayHelp(bin = AGRO_PRODUCT.bin) {
@@ -6469,16 +6718,24 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6469
6718
  }
6470
6719
  const positionals = [];
6471
6720
  let expectPath = false;
6721
+ let expectRef = false;
6472
6722
  for (const token of rest) {
6473
6723
  if (expectPath) {
6474
6724
  expectPath = false;
6475
6725
  args.path = token;
6726
+ } else if (expectRef) {
6727
+ expectRef = false;
6728
+ args.ref = token;
6476
6729
  } else if (token === "--json") {
6477
6730
  args.json = true;
6478
6731
  } else if (token === "--path") {
6479
6732
  expectPath = true;
6480
6733
  } else if (token.startsWith("--path=")) {
6481
6734
  args.path = token.slice("--path=".length);
6735
+ } else if (token === "--ref") {
6736
+ expectRef = true;
6737
+ } else if (token.startsWith("--ref=")) {
6738
+ args.ref = token.slice("--ref=".length);
6482
6739
  } else if (token.startsWith("-")) {
6483
6740
  return { ok: false, error: `${bin} workspace: unknown flag "${token}"` };
6484
6741
  } else {
@@ -6488,6 +6745,9 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6488
6745
  if (expectPath || args.path === "") {
6489
6746
  return { ok: false, error: `${bin} workspace: --path requires a directory` };
6490
6747
  }
6748
+ if (expectRef || args.ref === "") {
6749
+ return { ok: false, error: `${bin} workspace: --ref requires a branch or tag` };
6750
+ }
6491
6751
  const [sub, name, ...extra] = positionals;
6492
6752
  if (sub !== "create" && sub !== "list") {
6493
6753
  return {
@@ -6505,6 +6765,9 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6505
6765
  if (sub === "list" && args.path !== void 0) {
6506
6766
  return { ok: false, error: `${bin} workspace list: --path applies to create only` };
6507
6767
  }
6768
+ if (sub === "list" && args.ref !== void 0) {
6769
+ return { ok: false, error: `${bin} workspace list: --ref applies to create only` };
6770
+ }
6508
6771
  if (args.path !== void 0 && name !== void 0) {
6509
6772
  return {
6510
6773
  ok: false,
@@ -6695,7 +6958,7 @@ function readPayloadVersion(checkoutDir) {
6695
6958
  async function runWithRemoteSource(hooks, fn) {
6696
6959
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
6697
6960
  const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join17(tmpdir4(), "oh-remote-")));
6698
- const rm = hooks.rm ?? ((dir) => rmSync5(dir, { recursive: true, force: true }));
6961
+ const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
6699
6962
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
6700
6963
  const checkoutDir = mkdtemp();
6701
6964
  try {
@@ -6957,7 +7220,12 @@ async function main(argv) {
6957
7220
  }
6958
7221
  return await runWorkspaceCreate(
6959
7222
  a.name,
6960
- { bin, json: a.json, ...a.path !== void 0 ? { path: a.path } : {} },
7223
+ {
7224
+ bin,
7225
+ json: a.json,
7226
+ ...a.path !== void 0 ? { path: a.path } : {},
7227
+ ...a.ref !== void 0 ? { ref: a.ref } : {}
7228
+ },
6961
7229
  io
6962
7230
  );
6963
7231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.15.0-minimal.1",
3
+ "version": "0.15.0",
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",