@mifune/agro 0.14.0 → 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 +261 -22
  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
@@ -2222,9 +2222,9 @@ function resolveExecutionTarget(opts) {
2222
2222
  }
2223
2223
 
2224
2224
  // src/lib/version.ts
2225
- var AGRO_VERSION = true ? "0.14.0" : "0.0.0-dev";
2225
+ var AGRO_VERSION = true ? "0.15.0" : "0.0.0-dev";
2226
2226
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2227
- var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+$/;
2227
+ var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2228
2228
  function parseReleaseVersion(value) {
2229
2229
  const bare = value.startsWith("v") ? value.slice(1) : value;
2230
2230
  return RELEASE_VERSION.test(bare) ? bare : void 0;
@@ -4210,14 +4210,15 @@ function assertCloned(result, root) {
4210
4210
  throw new Error(`could not clone ${AGRO_REPO_URL} into ${root} (exit status ${String(status)})`);
4211
4211
  }
4212
4212
  }
4213
- function cloneInto(target, run, root) {
4214
- 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);
4215
4216
  }
4216
- function cloneThroughStaging(root, run) {
4217
+ function cloneThroughStaging(root, run, ref) {
4217
4218
  const staging = mkdtempSync4(join13(dirname9(root), "agro-clone-"));
4218
4219
  try {
4219
4220
  const checkout = join13(staging, "agro");
4220
- cloneInto(checkout, run, root);
4221
+ cloneInto(checkout, run, root, ref);
4221
4222
  for (const name of readdirSync3(checkout)) {
4222
4223
  const destination = join13(root, name);
4223
4224
  if (existsSync17(destination)) {
@@ -4229,13 +4230,13 @@ function cloneThroughStaging(root, run) {
4229
4230
  rmSync4(staging, { recursive: true, force: true });
4230
4231
  }
4231
4232
  }
4232
- function ensureHostWorkspace(path4, run = spawnRunner) {
4233
+ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
4233
4234
  const root = resolve9(path4);
4234
4235
  if (existsSync17(join13(root, ".git"))) return { root, action: "reused" };
4235
4236
  const stats = statSync5(root, { throwIfNoEntry: false });
4236
4237
  if (stats === void 0) {
4237
4238
  mkdirSync9(dirname9(root), { recursive: true });
4238
- cloneInto(root, run, root);
4239
+ cloneInto(root, run, root, ref);
4239
4240
  return { root, action: "cloned" };
4240
4241
  }
4241
4242
  if (!stats.isDirectory()) {
@@ -4247,7 +4248,7 @@ function ensureHostWorkspace(path4, run = spawnRunner) {
4247
4248
  `${root} holds files but no git checkout (${blocking.sort().join(", ")}) \u2014 choose an empty directory or an existing AGRO checkout`
4248
4249
  );
4249
4250
  }
4250
- cloneThroughStaging(root, run);
4251
+ cloneThroughStaging(root, run, ref);
4251
4252
  return { root, action: "cloned" };
4252
4253
  }
4253
4254
  function listHostWorkspaces(env, home) {
@@ -4773,7 +4774,7 @@ async function runHarnessInstall(name, opts, io) {
4773
4774
  }
4774
4775
 
4775
4776
  // src/commands/workspace.ts
4776
- import { existsSync as existsSync19 } from "node:fs";
4777
+ import { existsSync as existsSync19, rmSync as rmSync5 } from "node:fs";
4777
4778
  import { homedir as homedir4 } from "node:os";
4778
4779
  import { join as join15, resolve as resolve10 } from "node:path";
4779
4780
  function homeOf2(opts) {
@@ -4821,15 +4822,25 @@ async function runWorkspaceCreate(name, opts, io) {
4821
4822
  return 1;
4822
4823
  }
4823
4824
  let action;
4825
+ const preexisting = existsSync19(root);
4824
4826
  try {
4825
4827
  if (!opts.json && !existsSync19(join15(root, ".git"))) {
4826
- 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
4827
4830
  `);
4828
4831
  }
4829
- const created = ensureHostWorkspace(root, run);
4832
+ const created = ensureHostWorkspace(root, run, opts.ref);
4830
4833
  root = created.root;
4831
4834
  action = created.action;
4832
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
+ }
4833
4844
  io.stderr(`${bin} workspace: ${messageOf2(err)}
4834
4845
  `);
4835
4846
  return 1;
@@ -4884,7 +4895,7 @@ async function runWorkspaceList(opts, io) {
4884
4895
 
4885
4896
  // src/commands/tool.ts
4886
4897
  import { existsSync as existsSync20 } from "node:fs";
4887
- import { homedir as homedir5 } from "node:os";
4898
+ import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
4888
4899
  import { delimiter as delimiter3, join as join16 } from "node:path";
4889
4900
 
4890
4901
  // src/lib/tools/catalog.ts
@@ -5106,6 +5117,178 @@ install -d -m 0700 "$HOME/.tailscale"`
5106
5117
  `${HARNESS_PREFIX_TOKEN}/bin/tailscaled`
5107
5118
  ]),
5108
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
5109
5292
  })
5110
5293
  ]);
5111
5294
  function findTool(id) {
@@ -5168,6 +5351,10 @@ function homeOf3(opts) {
5168
5351
  function platformOf(opts) {
5169
5352
  return opts.platform ?? process.platform;
5170
5353
  }
5354
+ function isRootUser(opts) {
5355
+ const identity = opts.identity ?? (() => ({ name: userInfo2().username, uid: userInfo2().uid }));
5356
+ return identity().uid === 0;
5357
+ }
5171
5358
  function isInteractive2(opts) {
5172
5359
  return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
5173
5360
  }
@@ -5206,6 +5393,7 @@ function rowOf(entry, installed, version, location) {
5206
5393
  version,
5207
5394
  installable: entry.installArgv !== void 0,
5208
5395
  hostCapable: entry.hostCapable,
5396
+ hostRoot: entry.hostInstallUser === "root",
5209
5397
  location,
5210
5398
  docs: sourceDocsUrl(entry.docsPath)
5211
5399
  };
@@ -5253,13 +5441,20 @@ function cell2(value, absent) {
5253
5441
  function renderTable3(collected, io, bin) {
5254
5442
  const rows = collected.rows;
5255
5443
  const header2 = ["TOOL", "KIND", "INSTALLED"];
5256
- 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
+ ]);
5257
5449
  const widths = header2.map(
5258
5450
  (h, i) => Math.max(h.length, ...body.map((b) => b[i].length))
5259
5451
  );
5260
5452
  const line = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd() + "\n";
5261
5453
  io.stdout(line(header2));
5262
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
+ }
5263
5458
  if (rows.some((r) => r.location === "host")) {
5264
5459
  io.stdout(
5265
5460
  `
@@ -5439,11 +5634,21 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5439
5634
  }
5440
5635
  return 0;
5441
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
+ }
5442
5646
  if (!await confirmDownload(entry, opts, io, true)) return 1;
5443
5647
  io.stdout(`installing ${entry.title} on the host\u2026
5444
5648
  `);
5649
+ const argv = resolveToolInstallArgv(entry, true);
5445
5650
  const r = await target.exec({
5446
- argv: resolveToolInstallArgv(entry, true),
5651
+ argv: sudo ? ["sudo", "-n", "--", ...argv] : argv,
5447
5652
  stdio: "inherit",
5448
5653
  env: installEnv
5449
5654
  });
@@ -5475,6 +5680,11 @@ Every tool installer in the catalog is Debian-specific. Install ${entry.id} with
5475
5680
  `);
5476
5681
  return 1;
5477
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
+ }
5478
5688
  io.stdout(`${entry.id}: installed at ${prefix} \u2014 see ${sourceDocsUrl(entry.docsPath)}
5479
5689
  `);
5480
5690
  if (!onPath2(prefix, env)) {
@@ -5490,7 +5700,8 @@ async function runToolInstall(name, opts, io) {
5490
5700
  const root = resolveProjectRoot(opts.cwd);
5491
5701
  const entry = findTool(name);
5492
5702
  if (!entry) return unknownTool(name, io, opts.bin);
5493
- if (entry.installArgv === void 0) {
5703
+ const hostOnly = entry.installArgv === void 0 && entry.hostInstallArgv !== void 0;
5704
+ const refuseContainer = () => {
5494
5705
  io.stderr(`${opts.bin} tool: ${entry.id} cannot be installed by this command.
5495
5706
 
5496
5707
  `);
@@ -5501,12 +5712,16 @@ async function runToolInstall(name, opts, io) {
5501
5712
  ${installableToolIds().map((t) => ` ${t}`).join("\n")}
5502
5713
  `);
5503
5714
  return 1;
5715
+ };
5716
+ if (entry.installArgv === void 0 && (!hostOnly || runningInsideSandbox(opts.env ?? process.env))) {
5717
+ return refuseContainer();
5504
5718
  }
5505
5719
  const target = targetFor2(root, run, opts.env);
5506
5720
  const status = await resolveTargetStatus(target);
5507
- if (!isReachable2(status)) {
5721
+ if (!isReachable2(status) || hostOnly && opts.host === true) {
5508
5722
  return await installOnHost2(entry, opts, io, run, status);
5509
5723
  }
5724
+ if (entry.installArgv === void 0) return refuseContainer();
5510
5725
  const already = await probeInstalled2(target, entry, "sandbox");
5511
5726
  if (already === true) {
5512
5727
  io.stdout(`${entry.id}: already installed (${entry.binary})
@@ -5914,7 +6129,8 @@ Flags:
5914
6129
  --yes Non-interactive: keep every default and ask nothing
5915
6130
  --version <X.Y.Z>
5916
6131
  Run the official release image ghcr.io/mifunedev/agro:<X.Y.Z>
5917
- (implies --image). A leading v is accepted
6132
+ (implies --image). A leading v and a -<channel>.<n>
6133
+ pre-release suffix are accepted
5918
6134
  --image Run the prebuilt image instead of building (implies
5919
6135
  --no-build). The ref resolves first-match: --version or
5920
6136
  --image=<ref> > ${stateNames(bin).envPrefix}SANDBOX_IMAGE >
@@ -5980,7 +6196,8 @@ function printWorkspaceHelp(bin = AGRO_PRODUCT.bin) {
5980
6196
  process.stdout.write(`${bin} workspace \u2014 Create and list host AGRO workspaces
5981
6197
 
5982
6198
  Usage:
5983
- ${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
5984
6201
  ${bin} workspace list List every host workspace
5985
6202
 
5986
6203
  A host workspace is an AGRO checkout under \`~/.agro/workspaces/<name>\`. \`create\`
@@ -5995,6 +6212,9 @@ root as \`harnessRoot\`. \`list\` marks the recorded root.
5995
6212
  Flags:
5996
6213
  --json Machine-readable output
5997
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.
5998
6218
  `);
5999
6219
  }
6000
6220
  function printGatewayHelp(bin = AGRO_PRODUCT.bin) {
@@ -6498,16 +6718,24 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6498
6718
  }
6499
6719
  const positionals = [];
6500
6720
  let expectPath = false;
6721
+ let expectRef = false;
6501
6722
  for (const token of rest) {
6502
6723
  if (expectPath) {
6503
6724
  expectPath = false;
6504
6725
  args.path = token;
6726
+ } else if (expectRef) {
6727
+ expectRef = false;
6728
+ args.ref = token;
6505
6729
  } else if (token === "--json") {
6506
6730
  args.json = true;
6507
6731
  } else if (token === "--path") {
6508
6732
  expectPath = true;
6509
6733
  } else if (token.startsWith("--path=")) {
6510
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);
6511
6739
  } else if (token.startsWith("-")) {
6512
6740
  return { ok: false, error: `${bin} workspace: unknown flag "${token}"` };
6513
6741
  } else {
@@ -6517,6 +6745,9 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6517
6745
  if (expectPath || args.path === "") {
6518
6746
  return { ok: false, error: `${bin} workspace: --path requires a directory` };
6519
6747
  }
6748
+ if (expectRef || args.ref === "") {
6749
+ return { ok: false, error: `${bin} workspace: --ref requires a branch or tag` };
6750
+ }
6520
6751
  const [sub, name, ...extra] = positionals;
6521
6752
  if (sub !== "create" && sub !== "list") {
6522
6753
  return {
@@ -6534,6 +6765,9 @@ function parseWorkspaceArgs(rest, bin = AGRO_PRODUCT.bin) {
6534
6765
  if (sub === "list" && args.path !== void 0) {
6535
6766
  return { ok: false, error: `${bin} workspace list: --path applies to create only` };
6536
6767
  }
6768
+ if (sub === "list" && args.ref !== void 0) {
6769
+ return { ok: false, error: `${bin} workspace list: --ref applies to create only` };
6770
+ }
6537
6771
  if (args.path !== void 0 && name !== void 0) {
6538
6772
  return {
6539
6773
  ok: false,
@@ -6724,7 +6958,7 @@ function readPayloadVersion(checkoutDir) {
6724
6958
  async function runWithRemoteSource(hooks, fn) {
6725
6959
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
6726
6960
  const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join17(tmpdir4(), "oh-remote-")));
6727
- const rm = hooks.rm ?? ((dir) => rmSync5(dir, { recursive: true, force: true }));
6961
+ const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
6728
6962
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
6729
6963
  const checkoutDir = mkdtemp();
6730
6964
  try {
@@ -6986,7 +7220,12 @@ async function main(argv) {
6986
7220
  }
6987
7221
  return await runWorkspaceCreate(
6988
7222
  a.name,
6989
- { 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
+ },
6990
7229
  io
6991
7230
  );
6992
7231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.14.0",
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",