@mifune/agro 0.22.0 → 0.24.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 +488 -88
  2. package/package.json +1 -1
package/dist/agro.js CHANGED
@@ -7,8 +7,8 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/cli.ts
9
9
  import { fileURLToPath } from "node:url";
10
- import { dirname as dirname10, join as join20, resolve as resolve13 } from "node:path";
11
- import { existsSync as existsSync23, mkdtempSync as mkdtempSync5, readFileSync as readFileSync13, rmSync as rmSync6 } from "node:fs";
10
+ import { dirname as dirname11, join as join22, resolve as resolve13 } from "node:path";
11
+ import { existsSync as existsSync23, mkdtempSync as mkdtempSync5, readFileSync as readFileSync15, rmSync as rmSync7 } from "node:fs";
12
12
  import { tmpdir as tmpdir4 } from "node:os";
13
13
 
14
14
  // src/commands/update.ts
@@ -2040,9 +2040,7 @@ var SECRET_KEYS = [
2040
2040
  "PI_SLACK_BOT_TOKEN",
2041
2041
  "LANGFUSE_PUBLIC_KEY",
2042
2042
  "LANGFUSE_SECRET_KEY",
2043
- "TYPESAFE_API_KEY",
2044
- "AGRO_CONSOLE_URL",
2045
- "AGRO_CONSOLE_TOKEN"
2043
+ "TYPESAFE_API_KEY"
2046
2044
  ];
2047
2045
  var COMPOSE_SECRET_KEYS = [
2048
2046
  "GH_TOKEN",
@@ -2113,7 +2111,7 @@ import { basename as basename6, join as join6, sep as sep3 } from "node:path";
2113
2111
  import { basename as basename4, resolve as resolve7 } from "node:path";
2114
2112
 
2115
2113
  // src/lib/version.ts
2116
- var AGRO_VERSION = true ? "0.22.0" : "0.0.0-dev";
2114
+ var AGRO_VERSION = true ? "0.24.0" : "0.0.0-dev";
2117
2115
  var OFFICIAL_IMAGE = "ghcr.io/mifunedev/agro";
2118
2116
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z][a-z0-9]*\.[0-9]+)?$/;
2119
2117
  function parseReleaseVersion(value) {
@@ -5123,6 +5121,9 @@ function runEgress(verb, opts, io) {
5123
5121
  });
5124
5122
  }
5125
5123
 
5124
+ // src/commands/auth.ts
5125
+ import { hostname as hostname2 } from "node:os";
5126
+
5126
5127
  // src/command-table.ts
5127
5128
  var CONFIG_VERBS = ["show", "set", "repo"];
5128
5129
  var CONFIG_INTEGRATIONS = ["langfuse"];
@@ -5134,6 +5135,7 @@ var HARNESS_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
5134
5135
  var TOOL_SUBCOMMANDS = ["list", "install", "status", "uninstall"];
5135
5136
  var EGRESS_VERBS = ["log", "reload"];
5136
5137
  var NODE_VERBS = ["create", "list", "delete"];
5138
+ var AUTH_VERBS = ["login", "status", "logout", "token"];
5137
5139
  function isOneOf(values, value) {
5138
5140
  return value !== void 0 && values.includes(value);
5139
5141
  }
@@ -5153,43 +5155,124 @@ var AGRO_COMMANDS = Object.freeze({
5153
5155
  gateway: null,
5154
5156
  egress: EGRESS_VERBS,
5155
5157
  node: NODE_VERBS,
5158
+ auth: AUTH_VERBS,
5156
5159
  ...Object.fromEntries(composeVerbs().map((verb) => [verb, null]))
5157
5160
  });
5158
5161
 
5162
+ // src/lib/console-auth.ts
5163
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readFileSync as readFileSync12, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
5164
+ import { homedir as homedir3 } from "node:os";
5165
+ import { dirname as dirname8, join as join15 } from "node:path";
5166
+ var CREDENTIAL_FIELDS = ["url", "token", "tokenId", "tokenPrefix", "login"];
5167
+ function consoleCredentialsPath(env = process.env, homeDir = homedir3()) {
5168
+ const xdg = env.XDG_CONFIG_HOME?.trim();
5169
+ const configHome = xdg === void 0 || xdg === "" ? join15(homeDir, ".config") : xdg;
5170
+ return join15(configHome, "agro", "console.json");
5171
+ }
5172
+ function isMissing(error) {
5173
+ return error.code === "ENOENT";
5174
+ }
5175
+ function toCredentials(value) {
5176
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
5177
+ const record = value;
5178
+ if (!CREDENTIAL_FIELDS.every((field) => typeof record[field] === "string")) return void 0;
5179
+ return {
5180
+ url: record.url,
5181
+ token: record.token,
5182
+ tokenId: record.tokenId,
5183
+ tokenPrefix: record.tokenPrefix,
5184
+ login: record.login
5185
+ };
5186
+ }
5187
+ function readConsoleCredentials(env = process.env, homeDir = homedir3()) {
5188
+ let text;
5189
+ try {
5190
+ text = readFileSync12(consoleCredentialsPath(env, homeDir), "utf8");
5191
+ } catch (error) {
5192
+ if (isMissing(error)) return void 0;
5193
+ throw error;
5194
+ }
5195
+ try {
5196
+ return toCredentials(JSON.parse(text));
5197
+ } catch {
5198
+ return void 0;
5199
+ }
5200
+ }
5201
+ function writeConsoleCredentials(credentials, env = process.env, homeDir = homedir3()) {
5202
+ const path4 = consoleCredentialsPath(env, homeDir);
5203
+ const dir = dirname8(path4);
5204
+ mkdirSync9(dir, { recursive: true, mode: 448 });
5205
+ chmodSync2(dir, 448);
5206
+ const body = {
5207
+ url: credentials.url,
5208
+ token: credentials.token,
5209
+ tokenId: credentials.tokenId,
5210
+ tokenPrefix: credentials.tokenPrefix,
5211
+ login: credentials.login
5212
+ };
5213
+ rmSync4(path4, { force: true });
5214
+ writeFileSync9(path4, `${JSON.stringify(body, null, 2)}
5215
+ `, { mode: 384, flag: "wx" });
5216
+ chmodSync2(path4, 384);
5217
+ return path4;
5218
+ }
5219
+ function deleteConsoleCredentials(env = process.env, homeDir = homedir3()) {
5220
+ const path4 = consoleCredentialsPath(env, homeDir);
5221
+ try {
5222
+ rmSync4(path4);
5223
+ return true;
5224
+ } catch (error) {
5225
+ if (isMissing(error)) return false;
5226
+ throw error;
5227
+ }
5228
+ }
5229
+
5159
5230
  // src/commands/node.ts
5231
+ import { readFileSync as readFileSync13 } from "node:fs";
5232
+ import { homedir as homedir4, hostname } from "node:os";
5233
+ import { join as join16 } from "node:path";
5160
5234
  var DEFAULT_CONSOLE_URL = "https://console.mifune.dev";
5161
- var CONSOLE_CONFIG_MISSING = "set AGRO_CONSOLE_TOKEN, or run agro secret set AGRO_CONSOLE_TOKEN";
5235
+ var CONSOLE_CONFIG_MISSING = "not logged in to agro-console: run agro auth login";
5162
5236
  var NODE_READY_TIMEOUT_MS = 15 * 60 * 1e3;
5237
+ var NODE_FAST_POLL_INTERVAL_MS = 1e3;
5238
+ var NODE_FAST_POLL_WINDOW_MS = 30 * 1e3;
5163
5239
  var NODE_POLL_INTERVAL_MS = 5e3;
5164
5240
  var FREE_TIER_SPEC = "n4";
5241
+ var LOCAL_SSH_KEY_MISSING = "no --ssh-key given and ~/.ssh/id_ed25519.pub not found";
5165
5242
  var FAILURE_STATUSES = /* @__PURE__ */ new Set(["failed", "destroy_requested", "destroying", "destroyed", "retired"]);
5166
- function resolveConsoleConfig(env, readSecret2) {
5167
- const lookup = (key) => {
5168
- const fromEnv = env[key]?.trim();
5169
- if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
5170
- const fromSecret = readSecret2(key)?.trim();
5171
- return fromSecret === void 0 || fromSecret === "" ? void 0 : fromSecret;
5243
+ function resolveConsoleConfig(env, readCredentials) {
5244
+ const present = (value) => {
5245
+ const trimmed = value?.trim();
5246
+ return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
5172
5247
  };
5173
- const token = lookup("AGRO_CONSOLE_TOKEN");
5174
- if (token === void 0) return void 0;
5175
- const url = lookup("AGRO_CONSOLE_URL") ?? DEFAULT_CONSOLE_URL;
5176
- return { url: url.replace(/\/+$/, ""), token };
5248
+ const trimUrl2 = (url) => (url ?? DEFAULT_CONSOLE_URL).replace(/\/+$/, "");
5249
+ const envToken = present(env.AGRO_CONSOLE_TOKEN);
5250
+ if (envToken !== void 0) return { url: trimUrl2(present(env.AGRO_CONSOLE_URL)), token: envToken, source: "env" };
5251
+ const file = readCredentials();
5252
+ const fileToken = present(file?.token);
5253
+ if (fileToken === void 0) return void 0;
5254
+ return { url: trimUrl2(present(file?.url)), token: fileToken, source: "file" };
5177
5255
  }
5178
- function readProjectSecret(key) {
5179
- try {
5180
- return readSecret(resolveProjectRoot(), key);
5181
- } catch {
5182
- return void 0;
5183
- }
5256
+ function nodePollDelayMs(elapsedMs) {
5257
+ return elapsedMs < NODE_FAST_POLL_WINDOW_MS ? NODE_FAST_POLL_INTERVAL_MS : NODE_POLL_INTERVAL_MS;
5184
5258
  }
5185
5259
  function defaultDeps2() {
5186
5260
  return {
5187
5261
  fetch: (input, init) => globalThis.fetch(input, init),
5188
5262
  env: process.env,
5189
- readSecret: readProjectSecret,
5263
+ readCredentials: () => readConsoleCredentials(),
5264
+ homeDir: () => homedir4(),
5265
+ hostname: () => hostname(),
5266
+ readFile: (path4) => {
5267
+ try {
5268
+ return readFileSync13(path4, "utf8");
5269
+ } catch {
5270
+ return void 0;
5271
+ }
5272
+ },
5190
5273
  sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
5191
5274
  now: () => Date.now(),
5192
- pollIntervalMs: NODE_POLL_INTERVAL_MS,
5275
+ pollDelayMs: nodePollDelayMs,
5193
5276
  timeoutMs: NODE_READY_TIMEOUT_MS,
5194
5277
  confirm: (question) => confirm(question, false)
5195
5278
  };
@@ -5198,15 +5281,16 @@ function nodeHelp(bin) {
5198
5281
  return `${bin} node \u2014 Create, list, and delete agro-console sandbox nodes (remote VMs)
5199
5282
 
5200
5283
  Usage:
5201
- ${bin} node create --spec <spec> --ssh-key <uuid> [--ssh-key <uuid>] [--name <name>] [--free-tier] [--json]
5284
+ ${bin} node create --spec <spec> [--ssh-key <uuid>]... [--name <name>] [--free-tier] [--json]
5202
5285
  ${bin} node list [--json]
5203
5286
  ${bin} node delete <id> [--yes]
5204
5287
 
5205
5288
  \`create\` waits up to 15 minutes for the node to reach \`running\`, then prints
5206
5289
  its SSH command. \`--free-tier\` requests the console free tier. The spec
5207
- defaults to n4. The verbs read AGRO_CONSOLE_URL and AGRO_CONSOLE_TOKEN from the
5208
- environment, then from the project .env (\`${bin} secret set <KEY>\`).
5209
- AGRO_CONSOLE_URL defaults to ${DEFAULT_CONSOLE_URL}.
5290
+ defaults to n4. When AGRO_CONSOLE_TOKEN is set, the verbs use it with
5291
+ AGRO_CONSOLE_URL. Otherwise the verbs use the token and the URL from the
5292
+ credentials file that \`${bin} auth login\` writes. The URL defaults to
5293
+ ${DEFAULT_CONSOLE_URL}.
5210
5294
  `;
5211
5295
  }
5212
5296
  function asRecord(value) {
@@ -5293,9 +5377,40 @@ function parseCreate(tokens) {
5293
5377
  }
5294
5378
  if (args.spec === void 0 && args.freeTier) args.spec = FREE_TIER_SPEC;
5295
5379
  if (args.spec === void 0) return { ok: false, error: "--spec is required" };
5296
- if (args.sshKeyIds.length === 0) return { ok: false, error: "at least one --ssh-key is required" };
5297
5380
  return { ok: true, args };
5298
5381
  }
5382
+ function keyIdentity(publicKey) {
5383
+ return publicKey.trim().split(/\s+/).slice(0, 2).join(" ");
5384
+ }
5385
+ function toKeyList(body) {
5386
+ const record = asRecord(body);
5387
+ const items = Array.isArray(body) ? body : record?.sshKeys;
5388
+ if (!Array.isArray(items)) return [];
5389
+ return items.flatMap((item) => {
5390
+ const key = asRecord(item);
5391
+ return key === void 0 ? [] : [key];
5392
+ });
5393
+ }
5394
+ async function resolveSshKeyId(deps, config) {
5395
+ const publicKey = deps.readFile(join16(deps.homeDir(), ".ssh", "id_ed25519.pub"))?.trim();
5396
+ if (publicKey === void 0 || publicKey === "") return { ok: false, error: LOCAL_SSH_KEY_MISSING };
5397
+ const listed = await callApi(deps, config, "GET", "/api/ssh-keys");
5398
+ if (!listed.ok) return { ok: false, error: listed.message };
5399
+ const identity = keyIdentity(publicKey);
5400
+ const match = toKeyList(listed.body).find(
5401
+ (key) => typeof key.publicKey === "string" && keyIdentity(key.publicKey) === identity
5402
+ );
5403
+ const matchId = asText(match?.id);
5404
+ if (matchId !== null) return { ok: true, id: matchId };
5405
+ const registered = await callApi(deps, config, "POST", "/api/ssh-keys", {
5406
+ name: `agro-cli@${deps.hostname()}`,
5407
+ publicKey
5408
+ });
5409
+ if (!registered.ok) return { ok: false, error: registered.message };
5410
+ const outer = asRecord(registered.body);
5411
+ const id = asText((asRecord(outer?.sshKey) ?? outer)?.id);
5412
+ return id === null ? { ok: false, error: "the console response has no SSH key id" } : { ok: true, id };
5413
+ }
5299
5414
  function printCreateResult(io, node, json) {
5300
5415
  const ssh = node.status === "running" ? sshCommand(node) : null;
5301
5416
  if (json) {
@@ -5316,7 +5431,18 @@ async function runCreate(tokens, io, deps, config, bin) {
5316
5431
  `);
5317
5432
  return 1;
5318
5433
  }
5319
- const { spec, sshKeyIds, name, freeTier, json } = parsed.args;
5434
+ const { spec, name, freeTier, json } = parsed.args;
5435
+ let sshKeyIds = parsed.args.sshKeyIds;
5436
+ if (sshKeyIds.length === 0) {
5437
+ const resolved = await resolveSshKeyId(deps, config);
5438
+ if (!resolved.ok) {
5439
+ io.stderr(`${prefix}: ${resolved.error}
5440
+ `);
5441
+ return 1;
5442
+ }
5443
+ sshKeyIds = [resolved.id];
5444
+ }
5445
+ const requestedAt = deps.now();
5320
5446
  const created = await callApi(deps, config, "POST", "/api/nodes", {
5321
5447
  profile: "sandbox",
5322
5448
  spec,
@@ -5346,7 +5472,7 @@ async function runCreate(tokens, io, deps, config, bin) {
5346
5472
  `);
5347
5473
  return 1;
5348
5474
  }
5349
- await deps.sleep(Math.min(deps.pollIntervalMs, remaining));
5475
+ await deps.sleep(Math.min(deps.pollDelayMs(deps.now() - requestedAt), remaining));
5350
5476
  const polled = await callApi(deps, config, "GET", `/api/nodes/${encodeURIComponent(node.id)}`);
5351
5477
  if (!polled.ok) {
5352
5478
  io.stderr(`${prefix}: node ${node.id}: ${polled.message}
@@ -5457,9 +5583,9 @@ async function runNode(args, io, overrides = {}, bin = "agro") {
5457
5583
  return 0;
5458
5584
  }
5459
5585
  const deps = { ...defaultDeps2(), ...overrides };
5460
- const config = resolveConsoleConfig(deps.env, deps.readSecret);
5586
+ const config = resolveConsoleConfig(deps.env, deps.readCredentials);
5461
5587
  if (config === void 0) {
5462
- io.stderr(`${bin} node ${verb}: ${CONSOLE_CONFIG_MISSING}
5588
+ io.stderr(`${CONSOLE_CONFIG_MISSING}
5463
5589
  `);
5464
5590
  return 1;
5465
5591
  }
@@ -5468,10 +5594,250 @@ async function runNode(args, io, overrides = {}, bin = "agro") {
5468
5594
  return runCreate(rest, io, deps, config, bin);
5469
5595
  }
5470
5596
 
5597
+ // src/commands/auth.ts
5598
+ var ENV_TOKEN_LOGOUT = "token comes from AGRO_CONSOLE_TOKEN; unset it to log out";
5599
+ var ALREADY_REVOKED = "token was already revoked; removed local credentials";
5600
+ var SLOW_DOWN_STEP_S = 5;
5601
+ function defaultDeps3() {
5602
+ return {
5603
+ fetch: (input, init) => globalThis.fetch(input, init),
5604
+ env: process.env,
5605
+ hostname: () => hostname2(),
5606
+ sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
5607
+ now: () => Date.now(),
5608
+ readCredentials: () => readConsoleCredentials(),
5609
+ writeCredentials: (credentials) => writeConsoleCredentials(credentials),
5610
+ deleteCredentials: () => deleteConsoleCredentials()
5611
+ };
5612
+ }
5613
+ function authHelp(bin) {
5614
+ return `${bin} auth \u2014 Sign in to agro-console
5615
+
5616
+ Usage:
5617
+ ${bin} auth login [--url <url>]
5618
+ ${bin} auth status
5619
+ ${bin} auth logout
5620
+ ${bin} auth token
5621
+
5622
+ \`login\` starts a device authorization, prints a URL and a code, and waits
5623
+ for approval in the browser. It stores the token in the user credentials file.
5624
+ Without --url it uses AGRO_CONSOLE_URL, then ${DEFAULT_CONSOLE_URL}.
5625
+ \`status\` shows the signed-in identity. \`logout\` revokes the token and deletes
5626
+ the credentials file. \`token\` prints the token. AGRO_CONSOLE_TOKEN in the
5627
+ environment overrides the credentials file.
5628
+ `;
5629
+ }
5630
+ function asRecord2(value) {
5631
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
5632
+ }
5633
+ function asText2(value) {
5634
+ if (typeof value === "string" && value !== "") return value;
5635
+ if (typeof value === "number") return String(value);
5636
+ return void 0;
5637
+ }
5638
+ async function callApi2(deps, url, method, path4, options = {}) {
5639
+ let response;
5640
+ try {
5641
+ response = await deps.fetch(`${url}${path4}`, {
5642
+ method,
5643
+ headers: {
5644
+ Accept: "application/json",
5645
+ ...options.token === void 0 ? {} : { Authorization: `Bearer ${options.token}` },
5646
+ ...options.payload === void 0 ? {} : { "Content-Type": "application/json" }
5647
+ },
5648
+ ...options.payload === void 0 ? {} : { body: JSON.stringify(options.payload) }
5649
+ });
5650
+ } catch (error) {
5651
+ return { ok: false, message: `request failed: ${error instanceof Error ? error.message : String(error)}` };
5652
+ }
5653
+ const text = await response.text();
5654
+ let body = null;
5655
+ try {
5656
+ body = text === "" ? null : JSON.parse(text);
5657
+ } catch {
5658
+ body = null;
5659
+ }
5660
+ const record = asRecord2(body);
5661
+ if (response.status >= 400) {
5662
+ const nested = asRecord2(record.error);
5663
+ const code = asText2(record.code) ?? asText2(nested.code);
5664
+ return { ok: false, status: response.status, code, message: `HTTP ${response.status} ${code ?? "no code"}` };
5665
+ }
5666
+ return { ok: true, status: response.status, body: record };
5667
+ }
5668
+ function trimUrl(url) {
5669
+ return url.trim().replace(/\/+$/, "");
5670
+ }
5671
+ function parseLogin(tokens) {
5672
+ let url;
5673
+ for (let i = 0; i < tokens.length; i += 1) {
5674
+ const token = tokens[i];
5675
+ const [flag, inline] = token.startsWith("--") && token.includes("=") ? [token.slice(0, token.indexOf("=")), token.slice(token.indexOf("=") + 1)] : [token, void 0];
5676
+ if (flag !== "--url") {
5677
+ return { ok: false, error: token.startsWith("-") ? `unknown flag "${token}"` : `unexpected argument "${token}"` };
5678
+ }
5679
+ const value = inline ?? tokens[++i];
5680
+ if (value === void 0 || value.trim() === "") return { ok: false, error: "--url requires a value" };
5681
+ url = value;
5682
+ }
5683
+ return { ok: true, url };
5684
+ }
5685
+ async function runLogin(tokens, io, deps, bin) {
5686
+ const prefix = `${bin} auth login`;
5687
+ const parsed = parseLogin(tokens);
5688
+ if (!parsed.ok) {
5689
+ io.stderr(`${prefix}: ${parsed.error}
5690
+ `);
5691
+ return 1;
5692
+ }
5693
+ const envUrl = deps.env.AGRO_CONSOLE_URL?.trim();
5694
+ const url = trimUrl(parsed.url ?? (envUrl === void 0 || envUrl === "" ? DEFAULT_CONSOLE_URL : envUrl));
5695
+ const started = await callApi2(deps, url, "POST", "/api/cli/device", { payload: { clientName: deps.hostname() } });
5696
+ if (!started.ok) {
5697
+ io.stderr(`${prefix}: ${started.message}
5698
+ `);
5699
+ return 1;
5700
+ }
5701
+ const deviceCode = asText2(started.body.device_code);
5702
+ const userCode = asText2(started.body.user_code);
5703
+ const verificationUri = asText2(started.body.verification_uri);
5704
+ if (deviceCode === void 0 || userCode === void 0 || verificationUri === void 0) {
5705
+ io.stderr(`${prefix}: unexpected device authorization response
5706
+ `);
5707
+ return 1;
5708
+ }
5709
+ let intervalS = Number(started.body.interval) > 0 ? Number(started.body.interval) : 5;
5710
+ const expiresInS = Number(started.body.expires_in) > 0 ? Number(started.body.expires_in) : 900;
5711
+ const deadline = deps.now() + expiresInS * 1e3;
5712
+ io.stdout(`Open ${verificationUri} and enter the code ${userCode}
5713
+ Waiting for approval...
5714
+ `);
5715
+ while (deps.now() + intervalS * 1e3 <= deadline) {
5716
+ await deps.sleep(intervalS * 1e3);
5717
+ const polled = await callApi2(deps, url, "POST", "/api/cli/device/token", { payload: { device_code: deviceCode } });
5718
+ if (polled.ok) {
5719
+ const credentials = {
5720
+ url,
5721
+ token: asText2(polled.body.token),
5722
+ tokenId: asText2(polled.body.tokenId),
5723
+ tokenPrefix: asText2(polled.body.tokenPrefix),
5724
+ login: asText2(polled.body.login)
5725
+ };
5726
+ if (Object.values(credentials).some((value) => value === void 0)) {
5727
+ io.stderr(`${prefix}: unexpected token response
5728
+ `);
5729
+ return 1;
5730
+ }
5731
+ deps.writeCredentials(credentials);
5732
+ io.stdout(`logged in to ${url} as ${credentials.login}
5733
+ `);
5734
+ return 0;
5735
+ }
5736
+ if (polled.code === "authorization_pending") continue;
5737
+ if (polled.code === "slow_down") {
5738
+ intervalS += SLOW_DOWN_STEP_S;
5739
+ continue;
5740
+ }
5741
+ if (polled.code === "access_denied") {
5742
+ io.stderr(`${prefix}: the authorization was denied
5743
+ `);
5744
+ return 1;
5745
+ }
5746
+ if (polled.code === "expired_token") {
5747
+ io.stderr(`${prefix}: the code expired; run ${bin} auth login again
5748
+ `);
5749
+ return 1;
5750
+ }
5751
+ io.stderr(`${prefix}: ${polled.message}
5752
+ `);
5753
+ return 1;
5754
+ }
5755
+ io.stderr(`${prefix}: the code expired; run ${bin} auth login again
5756
+ `);
5757
+ return 1;
5758
+ }
5759
+ async function runStatus(io, deps, config, bin) {
5760
+ const me = await callApi2(deps, config.url, "GET", "/api/me", { token: config.token });
5761
+ if (!me.ok) {
5762
+ const hint = me.status === 401 ? `: the token is invalid or revoked; run ${bin} auth login` : "";
5763
+ io.stderr(`${bin} auth status: ${me.message}${hint}
5764
+ `);
5765
+ return 1;
5766
+ }
5767
+ const field = (value) => asText2(value) ?? "-";
5768
+ io.stdout(
5769
+ `url: ${config.url}
5770
+ login: ${field(me.body.login)}
5771
+ role: ${field(me.body.role)}
5772
+ org: ${field(me.body.orgId)}
5773
+ source: ${config.source}
5774
+ token prefix: ${field(me.body.tokenPrefix)}
5775
+ `
5776
+ );
5777
+ return 0;
5778
+ }
5779
+ async function runLogout(io, deps, config, bin) {
5780
+ if (config.source === "env") {
5781
+ io.stderr(`${ENV_TOKEN_LOGOUT}
5782
+ `);
5783
+ return 1;
5784
+ }
5785
+ const revoked = await callApi2(deps, config.url, "DELETE", "/api/me/token", { token: config.token });
5786
+ if (!revoked.ok && revoked.status === 401) {
5787
+ deps.deleteCredentials();
5788
+ io.stdout(`${ALREADY_REVOKED}
5789
+ `);
5790
+ return 0;
5791
+ }
5792
+ if (!revoked.ok) {
5793
+ io.stderr(`${bin} auth logout: ${revoked.message}
5794
+ `);
5795
+ return 1;
5796
+ }
5797
+ deps.deleteCredentials();
5798
+ io.stdout(`logged out of ${config.url}
5799
+ `);
5800
+ return 0;
5801
+ }
5802
+ async function runAuth(args, io, overrides = {}, bin = "agro") {
5803
+ const [verb, ...rest] = args;
5804
+ if (verb === void 0 || verb === "--help" || verb === "-h" || verb === "help") {
5805
+ io.stdout(authHelp(bin));
5806
+ return verb === void 0 ? 1 : 0;
5807
+ }
5808
+ if (!isOneOf(AUTH_VERBS, verb)) {
5809
+ io.stderr(`${bin} auth: unknown subcommand "${verb}" \u2014 expected ${AUTH_VERBS.join(", ")}
5810
+ `);
5811
+ return 1;
5812
+ }
5813
+ if (rest.some((token) => token === "--help" || token === "-h")) {
5814
+ io.stdout(authHelp(bin));
5815
+ return 0;
5816
+ }
5817
+ const deps = { ...defaultDeps3(), ...overrides };
5818
+ if (verb === "login") return runLogin(rest, io, deps, bin);
5819
+ if (rest.length > 0) {
5820
+ io.stderr(`${bin} auth ${verb}: unexpected argument "${rest[0]}"
5821
+ `);
5822
+ return 1;
5823
+ }
5824
+ const config = resolveConsoleConfig(deps.env, deps.readCredentials);
5825
+ if (config === void 0) {
5826
+ io.stderr(`${CONSOLE_CONFIG_MISSING}
5827
+ `);
5828
+ return 1;
5829
+ }
5830
+ if (verb === "status") return runStatus(io, deps, config, bin);
5831
+ if (verb === "logout") return runLogout(io, deps, config, bin);
5832
+ io.stdout(`${config.token}
5833
+ `);
5834
+ return 0;
5835
+ }
5836
+
5471
5837
  // src/commands/harness.ts
5472
5838
  import { existsSync as existsSync20 } from "node:fs";
5473
- import { homedir as homedir3 } from "node:os";
5474
- import { delimiter as delimiter2, join as join17 } from "node:path";
5839
+ import { homedir as homedir5 } from "node:os";
5840
+ import { delimiter as delimiter2, join as join19 } from "node:path";
5475
5841
 
5476
5842
  // src/lib/docs.ts
5477
5843
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -5484,8 +5850,8 @@ function sourceDocsUrl(docsPath) {
5484
5850
  }
5485
5851
 
5486
5852
  // src/lib/host-config.ts
5487
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
5488
- import { basename as basename8, dirname as dirname8, isAbsolute, join as join15, resolve as resolve10 } from "node:path";
5853
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync14, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync10 } from "node:fs";
5854
+ import { basename as basename8, dirname as dirname9, isAbsolute, join as join17, resolve as resolve10 } from "node:path";
5489
5855
  var HOST_CONFIG_MODE = 420;
5490
5856
  var HOST_CONFIG_FILE = "config.json";
5491
5857
  var WORKSPACES_SUBDIR = "workspaces";
@@ -5497,7 +5863,7 @@ function hostStateHome(env, home) {
5497
5863
  return resolveUserStateHome(env, home);
5498
5864
  }
5499
5865
  function hostConfigPath(env, home) {
5500
- return join15(hostStateHome(env, home), HOST_CONFIG_FILE);
5866
+ return join17(hostStateHome(env, home), HOST_CONFIG_FILE);
5501
5867
  }
5502
5868
  function assertWorkspaceName(name) {
5503
5869
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -5507,14 +5873,14 @@ function assertWorkspaceName(name) {
5507
5873
  }
5508
5874
  }
5509
5875
  function workspacesRoot(env, home) {
5510
- return join15(hostStateHome(env, home), WORKSPACES_SUBDIR);
5876
+ return join17(hostStateHome(env, home), WORKSPACES_SUBDIR);
5511
5877
  }
5512
5878
  function workspaceRoot(name, env, home) {
5513
5879
  assertWorkspaceName(name);
5514
- return join15(workspacesRoot(env, home), name);
5880
+ return join17(workspacesRoot(env, home), name);
5515
5881
  }
5516
5882
  function defaultHarnessRoot(env, home) {
5517
- return join15(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
5883
+ return join17(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
5518
5884
  }
5519
5885
  function validateHostConfig(value) {
5520
5886
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -5582,7 +5948,7 @@ function readHostConfig(env, home) {
5582
5948
  if (!existsSync18(path4)) return { version: 1 };
5583
5949
  let raw;
5584
5950
  try {
5585
- raw = readFileSync12(path4, "utf8");
5951
+ raw = readFileSync14(path4, "utf8");
5586
5952
  } catch (error) {
5587
5953
  const detail = error instanceof Error ? error.message : String(error);
5588
5954
  throw new Error(`could not read ${path4}: ${detail}`);
@@ -5601,9 +5967,9 @@ function writeHostConfig(config, env, home) {
5601
5967
  const body = `${JSON.stringify(validated, null, 2)}
5602
5968
  `;
5603
5969
  const tmp = `${path4}.tmp.${process.pid}`;
5604
- mkdirSync9(dirname8(path4), { recursive: true });
5970
+ mkdirSync10(dirname9(path4), { recursive: true });
5605
5971
  try {
5606
- writeFileSync9(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
5972
+ writeFileSync10(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
5607
5973
  renameSync4(tmp, path4);
5608
5974
  } catch (error) {
5609
5975
  try {
@@ -5630,8 +5996,8 @@ function fieldError2(path4, requirement) {
5630
5996
  }
5631
5997
 
5632
5998
  // src/lib/host-workspace.ts
5633
- 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";
5634
- import { dirname as dirname9, join as join16, resolve as resolve11 } from "node:path";
5999
+ import { existsSync as existsSync19, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, readdirSync as readdirSync3, renameSync as renameSync5, rmSync as rmSync5, statSync as statSync5 } from "node:fs";
6000
+ import { dirname as dirname10, join as join18, resolve as resolve11 } from "node:path";
5635
6001
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
5636
6002
  var IGNORABLE_ENTRIES = [
5637
6003
  REGISTRY_SUBDIR,
@@ -5643,7 +6009,7 @@ function isDirectory3(path4) {
5643
6009
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
5644
6010
  }
5645
6011
  function stateHomes(home) {
5646
- return STATE_HOME_DIRS.map((name) => join16(home, name));
6012
+ return STATE_HOME_DIRS.map((name) => join18(home, name));
5647
6013
  }
5648
6014
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
5649
6015
  const target = resolve11(root);
@@ -5651,7 +6017,7 @@ function stateHomeRootRefusal(bin, root, home, verb = "harness") {
5651
6017
  if (enclosing === void 0) return void 0;
5652
6018
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
5653
6019
  A workspace there collides with AGRO state and blocks every command run from ${home}.
5654
- Move it out \u2014 for example \`mv ${enclosing} ${join16(home, "agro")}\` \u2014 then re-run this command.
6020
+ Move it out \u2014 for example \`mv ${enclosing} ${join18(home, "agro")}\` \u2014 then re-run this command.
5655
6021
  `;
5656
6022
  }
5657
6023
  function isIgnorableEntry(name) {
@@ -5668,27 +6034,27 @@ function cloneInto(target, run, root, ref) {
5668
6034
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
5669
6035
  }
5670
6036
  function cloneThroughStaging(root, run, ref) {
5671
- const staging = mkdtempSync4(join16(dirname9(root), "agro-clone-"));
6037
+ const staging = mkdtempSync4(join18(dirname10(root), "agro-clone-"));
5672
6038
  try {
5673
- const checkout = join16(staging, "agro");
6039
+ const checkout = join18(staging, "agro");
5674
6040
  cloneInto(checkout, run, root, ref);
5675
6041
  for (const name of readdirSync3(checkout)) {
5676
- const destination = join16(root, name);
6042
+ const destination = join18(root, name);
5677
6043
  if (existsSync19(destination)) {
5678
6044
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
5679
6045
  }
5680
- renameSync5(join16(checkout, name), destination);
6046
+ renameSync5(join18(checkout, name), destination);
5681
6047
  }
5682
6048
  } finally {
5683
- rmSync4(staging, { recursive: true, force: true });
6049
+ rmSync5(staging, { recursive: true, force: true });
5684
6050
  }
5685
6051
  }
5686
6052
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
5687
6053
  const root = resolve11(path4);
5688
- if (existsSync19(join16(root, ".git"))) return { root, action: "reused" };
6054
+ if (existsSync19(join18(root, ".git"))) return { root, action: "reused" };
5689
6055
  const stats = statSync5(root, { throwIfNoEntry: false });
5690
6056
  if (stats === void 0) {
5691
- mkdirSync10(dirname9(root), { recursive: true });
6057
+ mkdirSync11(dirname10(root), { recursive: true });
5692
6058
  cloneInto(root, run, root, ref);
5693
6059
  return { root, action: "cloned" };
5694
6060
  }
@@ -5707,11 +6073,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
5707
6073
  function listHostWorkspaces(env, home) {
5708
6074
  const root = workspacesRoot(env, home);
5709
6075
  if (!isDirectory3(root)) return [];
5710
- 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();
6076
+ return readdirSync3(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => SANDBOX_NAME_PATTERN.test(name)).filter((name) => existsSync19(join18(root, name, ".git"))).sort();
5711
6077
  }
5712
6078
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
5713
6079
  const root = resolve11(path4);
5714
- if (existsSync19(join16(root, ".git"))) return { ok: true, root };
6080
+ if (existsSync19(join18(root, ".git"))) return { ok: true, root };
5715
6081
  const existing = listHostWorkspaces(env, home);
5716
6082
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
5717
6083
  `;
@@ -5748,7 +6114,7 @@ function targetFor(root, cwd, run, env) {
5748
6114
  });
5749
6115
  }
5750
6116
  function hostPrefix(home) {
5751
- return join17(home, ".local");
6117
+ return join19(home, ".local");
5752
6118
  }
5753
6119
  function pathEntries(env) {
5754
6120
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -5795,7 +6161,7 @@ async function sandboxMarkerExists(target, entry) {
5795
6161
  return r.exitCode === 0;
5796
6162
  }
5797
6163
  function hostBinaryUnderPrefix(entry, prefix) {
5798
- return existsSync20(join17(harnessBinPath(prefix), entry.binary));
6164
+ return existsSync20(join19(harnessBinPath(prefix), entry.binary));
5799
6165
  }
5800
6166
  function stateOf(entry, installed, location) {
5801
6167
  return {
@@ -5816,7 +6182,7 @@ function probeableHost(env, home) {
5816
6182
  return void 0;
5817
6183
  }
5818
6184
  const prefix = hostPrefix(home);
5819
- if (!existsSync20(join17(root, ".git")) && !existsSync20(prefix)) return void 0;
6185
+ if (!existsSync20(join19(root, ".git")) && !existsSync20(prefix)) return void 0;
5820
6186
  return { root, prefix };
5821
6187
  }
5822
6188
  async function collectStates(root, cwd, run, home, env, only) {
@@ -6047,7 +6413,7 @@ Start it with \`${bin} sandbox\`, then re-run this command.
6047
6413
  `;
6048
6414
  }
6049
6415
  function homeOf(opts) {
6050
- return (opts.homedir ?? homedir3)();
6416
+ return (opts.homedir ?? homedir5)();
6051
6417
  }
6052
6418
  function isInteractive(opts) {
6053
6419
  return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
@@ -6436,11 +6802,11 @@ async function runHarnessInstall(name, opts, io) {
6436
6802
  }
6437
6803
 
6438
6804
  // src/commands/workspace.ts
6439
- import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
6440
- import { homedir as homedir4 } from "node:os";
6441
- import { join as join18, resolve as resolve12 } from "node:path";
6805
+ import { existsSync as existsSync21, rmSync as rmSync6 } from "node:fs";
6806
+ import { homedir as homedir6 } from "node:os";
6807
+ import { join as join20, resolve as resolve12 } from "node:path";
6442
6808
  function homeOf2(opts) {
6443
- return (opts.homedir ?? homedir4)();
6809
+ return (opts.homedir ?? homedir6)();
6444
6810
  }
6445
6811
  function messageOf2(error) {
6446
6812
  return error instanceof Error ? error.message : String(error);
@@ -6486,7 +6852,7 @@ async function runWorkspaceCreate(name, opts, io) {
6486
6852
  let action;
6487
6853
  const preexisting = existsSync21(root);
6488
6854
  try {
6489
- if (!opts.json && !existsSync21(join18(root, ".git"))) {
6855
+ if (!opts.json && !existsSync21(join20(root, ".git"))) {
6490
6856
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
6491
6857
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
6492
6858
  `);
@@ -6496,7 +6862,7 @@ async function runWorkspaceCreate(name, opts, io) {
6496
6862
  action = created.action;
6497
6863
  } catch (err) {
6498
6864
  if (opts.ref !== void 0) {
6499
- if (!preexisting) rmSync5(root, { recursive: true, force: true });
6865
+ if (!preexisting) rmSync6(root, { recursive: true, force: true });
6500
6866
  io.stderr(
6501
6867
  `${bin} workspace: could not clone ref "${opts.ref}" \u2014 the ref does not exist in ${AGRO_REPO_URL} or the clone failed (${messageOf2(err)})
6502
6868
  `
@@ -6531,7 +6897,7 @@ async function runWorkspaceList(opts, io) {
6531
6897
  const recorded = recordedRoot(env, home);
6532
6898
  const registry = workspacesRoot(env, home);
6533
6899
  rows = listHostWorkspaces(env, home).map((name) => {
6534
- const root = join18(registry, name);
6900
+ const root = join20(registry, name);
6535
6901
  return { name, root, default: recorded === root };
6536
6902
  });
6537
6903
  } catch (err) {
@@ -6557,8 +6923,8 @@ async function runWorkspaceList(opts, io) {
6557
6923
 
6558
6924
  // src/commands/tool.ts
6559
6925
  import { existsSync as existsSync22 } from "node:fs";
6560
- import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
6561
- import { delimiter as delimiter3, join as join19 } from "node:path";
6926
+ import { homedir as homedir7, userInfo as userInfo2 } from "node:os";
6927
+ import { delimiter as delimiter3, join as join21 } from "node:path";
6562
6928
 
6563
6929
  // src/lib/tools/catalog.ts
6564
6930
  var TOOLS_DOC = "docs/installation.md";
@@ -6601,22 +6967,47 @@ var TOOL_CATALOG = Object.freeze([
6601
6967
  ' browser="$resolved"',
6602
6968
  " break",
6603
6969
  "done",
6604
- 'if [ -z "$browser" ]; then',
6605
- ' echo "agent-browser is installed at $prefix/bin/agent-browser, but no Chromium-family browser was found on this host." >&2',
6606
- ' echo "AGRO does not install browser libraries on the host. Point agent-browser at an existing browser, then rerun:" >&2',
6607
- ' echo " export AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chrome" >&2',
6608
- ' echo "Or install one with your own package manager. Inside the sandbox, agro tool install agent-browser downloads Chrome for you." >&2',
6970
+ 'deps=""',
6971
+ 'if [ -n "$browser" ]; then',
6972
+ ' echo "agent-browser will drive $browser"',
6973
+ 'elif [ "$(id -u)" = 0 ] || sudo -n true 2>/dev/null; then',
6974
+ " if ! command -v sudo >/dev/null 2>&1; then",
6975
+ ` printf '#!/bin/sh\\nexec "$@"\\n' > "$tmp/sudo"`,
6976
+ ' chmod +x "$tmp/sudo"',
6977
+ ' PATH="$tmp:$PATH"',
6978
+ " fi",
6979
+ ' "$prefix/bin/agent-browser" install --with-deps',
6980
+ "else",
6981
+ ' "$prefix/bin/agent-browser" install',
6982
+ " deps=missing",
6983
+ "fi",
6984
+ 'export AGENT_BROWSER_SOCKET_DIR="$tmp"',
6985
+ "session=agro-install-check",
6986
+ "launched=yes",
6987
+ '"$prefix/bin/agent-browser" --session "$session" open about:blank >"$tmp/launch.log" 2>&1 || launched=no',
6988
+ '"$prefix/bin/agent-browser" --session "$session" close >/dev/null 2>&1 || true',
6989
+ 'if [ "$launched" = no ]; then',
6990
+ ' cat "$tmp/launch.log" >&2',
6991
+ ' echo "agent-browser is installed at $prefix/bin/agent-browser, but headless Chrome did not launch." >&2',
6992
+ ' if [ "$deps" = missing ]; then',
6993
+ ' echo "Chrome needs system libraries, and this user has no passwordless sudo. Run this command, then rerun agro tool install agent-browser:" >&2',
6994
+ ' echo " sudo $prefix/bin/agent-browser install --with-deps" >&2',
6995
+ " fi",
6609
6996
  " exit 1",
6610
6997
  "fi",
6611
- 'echo "agent-browser will drive $browser"',
6612
- '"$prefix/bin/agent-browser" --version >/dev/null'
6998
+ 'echo "agent-browser launched headless Chrome"'
6613
6999
  ].join("\n")
6614
7000
  ]),
6615
7001
  installUser: "sandbox",
6616
7002
  downloadSize: "~1 GB",
7003
+ hostDownloadSize: "~400 MB",
6617
7004
  hostCapable: true,
6618
7005
  uninstallArgv: Object.freeze(["bash", "-lc", "pnpm remove -g agent-browser"]),
6619
- hostUninstallArgv: Object.freeze(["rm", "-rf", `${HARNESS_PREFIX_TOKEN}/bin/agent-browser`]),
7006
+ hostUninstallArgv: Object.freeze([
7007
+ "bash",
7008
+ "-c",
7009
+ `rm -rf "${HARNESS_PREFIX_TOKEN}/bin/agent-browser" "$HOME/.agent-browser/browsers"`
7010
+ ]),
6620
7011
  docsPath: TOOLS_DOC
6621
7012
  }),
6622
7013
  Object.freeze({
@@ -7037,7 +7428,7 @@ function targetFor2(root, run, env) {
7037
7428
  });
7038
7429
  }
7039
7430
  function hostPrefix2(home) {
7040
- return join19(home, ".local");
7431
+ return join21(home, ".local");
7041
7432
  }
7042
7433
  function sandboxMarkerPath2(entry) {
7043
7434
  return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
@@ -7051,7 +7442,7 @@ async function sandboxMarkerExists2(target, entry) {
7051
7442
  return r.exitCode === 0;
7052
7443
  }
7053
7444
  function hostBinaryUnderPrefix2(entry, prefix) {
7054
- return existsSync22(join19(harnessBinPath(prefix), entry.binary));
7445
+ return existsSync22(join21(harnessBinPath(prefix), entry.binary));
7055
7446
  }
7056
7447
  function pathEntries2(env) {
7057
7448
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -7071,7 +7462,7 @@ function messageOf3(error) {
7071
7462
  return error instanceof Error ? error.message : String(error);
7072
7463
  }
7073
7464
  function homeOf3(opts) {
7074
- return (opts.homedir ?? homedir5)();
7465
+ return (opts.homedir ?? homedir7)();
7075
7466
  }
7076
7467
  function platformOf(opts) {
7077
7468
  return opts.platform ?? process.platform;
@@ -7131,7 +7522,7 @@ function probeableHost2(env, home) {
7131
7522
  return void 0;
7132
7523
  }
7133
7524
  const prefix = hostPrefix2(home);
7134
- if (!existsSync22(join19(root, ".git")) && !existsSync22(prefix)) return void 0;
7525
+ if (!existsSync22(join21(root, ".git")) && !existsSync22(prefix)) return void 0;
7135
7526
  return { root, prefix };
7136
7527
  }
7137
7528
  async function collectRows(root, run, home, env, only) {
@@ -7669,7 +8060,7 @@ function fetchRemoteSource(opts) {
7669
8060
 
7670
8061
  // src/cli.ts
7671
8062
  var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
7672
- dirname10(fileURLToPath(import.meta.url)),
8063
+ dirname11(fileURLToPath(import.meta.url)),
7673
8064
  "../.."
7674
8065
  );
7675
8066
  var INTEGRATIONS = {
@@ -7714,6 +8105,7 @@ Usage:
7714
8105
  ${bin} egress <args...> Read the egress proxy log or reload its allowlist (log|reload)
7715
8106
  ${bin} workspace <args...> Create and list host AGRO workspaces (create|list)
7716
8107
  ${bin} node <args...> Manage remote agro-console sandbox nodes (create|list|delete)
8108
+ ${bin} auth <args...> Sign in to agro-console (login|status|logout|token)
7717
8109
  ${bin} harness <args...> Install and inspect agent CLI harnesses
7718
8110
  ${bin} tool <args...> Install and inspect sandbox tooling
7719
8111
  ${bin} gateway <args...> Manage a messaging client session (pi|hermes|openclaw)
@@ -8549,7 +8941,7 @@ function parseGatewayArgs(rest) {
8549
8941
  return { ok: true, args: { help: false, passthrough: [...rest] } };
8550
8942
  }
8551
8943
  function bundledPayloadExists(bundled, exists = existsSync23) {
8552
- return exists(join20(bundled.sourceControlDir, "manifest.json"));
8944
+ return exists(join22(bundled.sourceControlDir, "manifest.json"));
8553
8945
  }
8554
8946
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
8555
8947
  const exists = bundled.exists ?? existsSync23;
@@ -8572,7 +8964,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
8572
8964
  function readPayloadVersion(checkoutDir) {
8573
8965
  try {
8574
8966
  const parsed = JSON.parse(
8575
- readFileSync13(join20(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
8967
+ readFileSync15(join22(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
8576
8968
  );
8577
8969
  if (parsed && typeof parsed.version === "string") return parsed.version;
8578
8970
  } catch {
@@ -8581,8 +8973,8 @@ function readPayloadVersion(checkoutDir) {
8581
8973
  }
8582
8974
  async function runWithRemoteSource(hooks, fn) {
8583
8975
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
8584
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join20(tmpdir4(), "oh-remote-")));
8585
- const rm = hooks.rm ?? ((dir) => rmSync6(dir, { recursive: true, force: true }));
8976
+ const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join22(tmpdir4(), "oh-remote-")));
8977
+ const rm = hooks.rm ?? ((dir) => rmSync7(dir, { recursive: true, force: true }));
8586
8978
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
8587
8979
  const checkoutDir = mkdtemp();
8588
8980
  try {
@@ -8811,6 +9203,14 @@ async function main(argv) {
8811
9203
  }
8812
9204
  return runEgress(a.verb, a.name === void 0 ? { bin } : { bin, name: a.name }, lifecycleIo());
8813
9205
  }
9206
+ if (first === "auth") {
9207
+ return await runAuth(
9208
+ argv.slice(1),
9209
+ { stdout: (s) => process.stdout.write(s), stderr: (s) => process.stderr.write(s) },
9210
+ {},
9211
+ bin
9212
+ );
9213
+ }
8814
9214
  if (first === "node") {
8815
9215
  return await runNode(
8816
9216
  argv.slice(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.22.0",
3
+ "version": "0.24.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",