@mifune/agro 0.23.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 +481 -87
  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.23.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,36 +5155,103 @@ 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;
5163
5237
  var NODE_FAST_POLL_INTERVAL_MS = 1e3;
5164
5238
  var NODE_FAST_POLL_WINDOW_MS = 30 * 1e3;
5165
5239
  var NODE_POLL_INTERVAL_MS = 5e3;
5166
5240
  var FREE_TIER_SPEC = "n4";
5241
+ var LOCAL_SSH_KEY_MISSING = "no --ssh-key given and ~/.ssh/id_ed25519.pub not found";
5167
5242
  var FAILURE_STATUSES = /* @__PURE__ */ new Set(["failed", "destroy_requested", "destroying", "destroyed", "retired"]);
5168
- function resolveConsoleConfig(env, readSecret2) {
5169
- const lookup = (key) => {
5170
- const fromEnv = env[key]?.trim();
5171
- if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
5172
- const fromSecret = readSecret2(key)?.trim();
5173
- 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;
5174
5247
  };
5175
- const token = lookup("AGRO_CONSOLE_TOKEN");
5176
- if (token === void 0) return void 0;
5177
- const url = lookup("AGRO_CONSOLE_URL") ?? DEFAULT_CONSOLE_URL;
5178
- return { url: url.replace(/\/+$/, ""), token };
5179
- }
5180
- function readProjectSecret(key) {
5181
- try {
5182
- return readSecret(resolveProjectRoot(), key);
5183
- } catch {
5184
- return void 0;
5185
- }
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" };
5186
5255
  }
5187
5256
  function nodePollDelayMs(elapsedMs) {
5188
5257
  return elapsedMs < NODE_FAST_POLL_WINDOW_MS ? NODE_FAST_POLL_INTERVAL_MS : NODE_POLL_INTERVAL_MS;
@@ -5191,7 +5260,16 @@ function defaultDeps2() {
5191
5260
  return {
5192
5261
  fetch: (input, init) => globalThis.fetch(input, init),
5193
5262
  env: process.env,
5194
- 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
+ },
5195
5273
  sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
5196
5274
  now: () => Date.now(),
5197
5275
  pollDelayMs: nodePollDelayMs,
@@ -5203,15 +5281,16 @@ function nodeHelp(bin) {
5203
5281
  return `${bin} node \u2014 Create, list, and delete agro-console sandbox nodes (remote VMs)
5204
5282
 
5205
5283
  Usage:
5206
- ${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]
5207
5285
  ${bin} node list [--json]
5208
5286
  ${bin} node delete <id> [--yes]
5209
5287
 
5210
5288
  \`create\` waits up to 15 minutes for the node to reach \`running\`, then prints
5211
5289
  its SSH command. \`--free-tier\` requests the console free tier. The spec
5212
- defaults to n4. The verbs read AGRO_CONSOLE_URL and AGRO_CONSOLE_TOKEN from the
5213
- environment, then from the project .env (\`${bin} secret set <KEY>\`).
5214
- 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}.
5215
5294
  `;
5216
5295
  }
5217
5296
  function asRecord(value) {
@@ -5298,9 +5377,40 @@ function parseCreate(tokens) {
5298
5377
  }
5299
5378
  if (args.spec === void 0 && args.freeTier) args.spec = FREE_TIER_SPEC;
5300
5379
  if (args.spec === void 0) return { ok: false, error: "--spec is required" };
5301
- if (args.sshKeyIds.length === 0) return { ok: false, error: "at least one --ssh-key is required" };
5302
5380
  return { ok: true, args };
5303
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
+ }
5304
5414
  function printCreateResult(io, node, json) {
5305
5415
  const ssh = node.status === "running" ? sshCommand(node) : null;
5306
5416
  if (json) {
@@ -5321,7 +5431,17 @@ async function runCreate(tokens, io, deps, config, bin) {
5321
5431
  `);
5322
5432
  return 1;
5323
5433
  }
5324
- 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
+ }
5325
5445
  const requestedAt = deps.now();
5326
5446
  const created = await callApi(deps, config, "POST", "/api/nodes", {
5327
5447
  profile: "sandbox",
@@ -5463,9 +5583,9 @@ async function runNode(args, io, overrides = {}, bin = "agro") {
5463
5583
  return 0;
5464
5584
  }
5465
5585
  const deps = { ...defaultDeps2(), ...overrides };
5466
- const config = resolveConsoleConfig(deps.env, deps.readSecret);
5586
+ const config = resolveConsoleConfig(deps.env, deps.readCredentials);
5467
5587
  if (config === void 0) {
5468
- io.stderr(`${bin} node ${verb}: ${CONSOLE_CONFIG_MISSING}
5588
+ io.stderr(`${CONSOLE_CONFIG_MISSING}
5469
5589
  `);
5470
5590
  return 1;
5471
5591
  }
@@ -5474,10 +5594,250 @@ async function runNode(args, io, overrides = {}, bin = "agro") {
5474
5594
  return runCreate(rest, io, deps, config, bin);
5475
5595
  }
5476
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
+
5477
5837
  // src/commands/harness.ts
5478
5838
  import { existsSync as existsSync20 } from "node:fs";
5479
- import { homedir as homedir3 } from "node:os";
5480
- 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";
5481
5841
 
5482
5842
  // src/lib/docs.ts
5483
5843
  var SOURCE_DOCS_BASE = "https://github.com/mifunedev/agro/blob/main/";
@@ -5490,8 +5850,8 @@ function sourceDocsUrl(docsPath) {
5490
5850
  }
5491
5851
 
5492
5852
  // src/lib/host-config.ts
5493
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, renameSync as renameSync4, unlinkSync as unlinkSync6, writeFileSync as writeFileSync9 } from "node:fs";
5494
- 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";
5495
5855
  var HOST_CONFIG_MODE = 420;
5496
5856
  var HOST_CONFIG_FILE = "config.json";
5497
5857
  var WORKSPACES_SUBDIR = "workspaces";
@@ -5503,7 +5863,7 @@ function hostStateHome(env, home) {
5503
5863
  return resolveUserStateHome(env, home);
5504
5864
  }
5505
5865
  function hostConfigPath(env, home) {
5506
- return join15(hostStateHome(env, home), HOST_CONFIG_FILE);
5866
+ return join17(hostStateHome(env, home), HOST_CONFIG_FILE);
5507
5867
  }
5508
5868
  function assertWorkspaceName(name) {
5509
5869
  if (!SANDBOX_NAME_PATTERN.test(name)) {
@@ -5513,14 +5873,14 @@ function assertWorkspaceName(name) {
5513
5873
  }
5514
5874
  }
5515
5875
  function workspacesRoot(env, home) {
5516
- return join15(hostStateHome(env, home), WORKSPACES_SUBDIR);
5876
+ return join17(hostStateHome(env, home), WORKSPACES_SUBDIR);
5517
5877
  }
5518
5878
  function workspaceRoot(name, env, home) {
5519
5879
  assertWorkspaceName(name);
5520
- return join15(workspacesRoot(env, home), name);
5880
+ return join17(workspacesRoot(env, home), name);
5521
5881
  }
5522
5882
  function defaultHarnessRoot(env, home) {
5523
- return join15(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
5883
+ return join17(workspacesRoot(env, home), DEFAULT_WORKSPACE_NAME);
5524
5884
  }
5525
5885
  function validateHostConfig(value) {
5526
5886
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -5588,7 +5948,7 @@ function readHostConfig(env, home) {
5588
5948
  if (!existsSync18(path4)) return { version: 1 };
5589
5949
  let raw;
5590
5950
  try {
5591
- raw = readFileSync12(path4, "utf8");
5951
+ raw = readFileSync14(path4, "utf8");
5592
5952
  } catch (error) {
5593
5953
  const detail = error instanceof Error ? error.message : String(error);
5594
5954
  throw new Error(`could not read ${path4}: ${detail}`);
@@ -5607,9 +5967,9 @@ function writeHostConfig(config, env, home) {
5607
5967
  const body = `${JSON.stringify(validated, null, 2)}
5608
5968
  `;
5609
5969
  const tmp = `${path4}.tmp.${process.pid}`;
5610
- mkdirSync9(dirname8(path4), { recursive: true });
5970
+ mkdirSync10(dirname9(path4), { recursive: true });
5611
5971
  try {
5612
- writeFileSync9(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
5972
+ writeFileSync10(tmp, body, { mode: HOST_CONFIG_MODE, encoding: "utf8" });
5613
5973
  renameSync4(tmp, path4);
5614
5974
  } catch (error) {
5615
5975
  try {
@@ -5636,8 +5996,8 @@ function fieldError2(path4, requirement) {
5636
5996
  }
5637
5997
 
5638
5998
  // src/lib/host-workspace.ts
5639
- 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";
5640
- 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";
5641
6001
  var AGRO_REPO_URL = "https://github.com/mifunedev/agro.git";
5642
6002
  var IGNORABLE_ENTRIES = [
5643
6003
  REGISTRY_SUBDIR,
@@ -5649,7 +6009,7 @@ function isDirectory3(path4) {
5649
6009
  return statSync5(path4, { throwIfNoEntry: false })?.isDirectory() === true;
5650
6010
  }
5651
6011
  function stateHomes(home) {
5652
- return STATE_HOME_DIRS.map((name) => join16(home, name));
6012
+ return STATE_HOME_DIRS.map((name) => join18(home, name));
5653
6013
  }
5654
6014
  function stateHomeRootRefusal(bin, root, home, verb = "harness") {
5655
6015
  const target = resolve11(root);
@@ -5657,7 +6017,7 @@ function stateHomeRootRefusal(bin, root, home, verb = "harness") {
5657
6017
  if (enclosing === void 0) return void 0;
5658
6018
  return `${bin} ${verb}: the ${verb} root ${target} is the state home ${enclosing} itself.
5659
6019
  A workspace there collides with AGRO state and blocks every command run from ${home}.
5660
- 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.
5661
6021
  `;
5662
6022
  }
5663
6023
  function isIgnorableEntry(name) {
@@ -5674,27 +6034,27 @@ function cloneInto(target, run, root, ref) {
5674
6034
  assertCloned(run("git", ["clone", ...branch, AGRO_REPO_URL, target], { stdio: "inherit" }), root);
5675
6035
  }
5676
6036
  function cloneThroughStaging(root, run, ref) {
5677
- const staging = mkdtempSync4(join16(dirname9(root), "agro-clone-"));
6037
+ const staging = mkdtempSync4(join18(dirname10(root), "agro-clone-"));
5678
6038
  try {
5679
- const checkout = join16(staging, "agro");
6039
+ const checkout = join18(staging, "agro");
5680
6040
  cloneInto(checkout, run, root, ref);
5681
6041
  for (const name of readdirSync3(checkout)) {
5682
- const destination = join16(root, name);
6042
+ const destination = join18(root, name);
5683
6043
  if (existsSync19(destination)) {
5684
6044
  throw new Error(`could not move ${name} into ${root} \u2014 the path already exists`);
5685
6045
  }
5686
- renameSync5(join16(checkout, name), destination);
6046
+ renameSync5(join18(checkout, name), destination);
5687
6047
  }
5688
6048
  } finally {
5689
- rmSync4(staging, { recursive: true, force: true });
6049
+ rmSync5(staging, { recursive: true, force: true });
5690
6050
  }
5691
6051
  }
5692
6052
  function ensureHostWorkspace(path4, run = spawnRunner, ref) {
5693
6053
  const root = resolve11(path4);
5694
- if (existsSync19(join16(root, ".git"))) return { root, action: "reused" };
6054
+ if (existsSync19(join18(root, ".git"))) return { root, action: "reused" };
5695
6055
  const stats = statSync5(root, { throwIfNoEntry: false });
5696
6056
  if (stats === void 0) {
5697
- mkdirSync10(dirname9(root), { recursive: true });
6057
+ mkdirSync11(dirname10(root), { recursive: true });
5698
6058
  cloneInto(root, run, root, ref);
5699
6059
  return { root, action: "cloned" };
5700
6060
  }
@@ -5713,11 +6073,11 @@ function ensureHostWorkspace(path4, run = spawnRunner, ref) {
5713
6073
  function listHostWorkspaces(env, home) {
5714
6074
  const root = workspacesRoot(env, home);
5715
6075
  if (!isDirectory3(root)) return [];
5716
- 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();
5717
6077
  }
5718
6078
  function resolveExistingWorkspace(bin, verb, path4, env, home) {
5719
6079
  const root = resolve11(path4);
5720
- if (existsSync19(join16(root, ".git"))) return { ok: true, root };
6080
+ if (existsSync19(join18(root, ".git"))) return { ok: true, root };
5721
6081
  const existing = listHostWorkspaces(env, home);
5722
6082
  const inventory = existing.length === 0 ? "No host workspace exists yet.\n" : `Workspaces that exist: ${existing.join(", ")}
5723
6083
  `;
@@ -5754,7 +6114,7 @@ function targetFor(root, cwd, run, env) {
5754
6114
  });
5755
6115
  }
5756
6116
  function hostPrefix(home) {
5757
- return join17(home, ".local");
6117
+ return join19(home, ".local");
5758
6118
  }
5759
6119
  function pathEntries(env) {
5760
6120
  return (env.PATH ?? "").split(delimiter2).filter((part) => part !== "");
@@ -5801,7 +6161,7 @@ async function sandboxMarkerExists(target, entry) {
5801
6161
  return r.exitCode === 0;
5802
6162
  }
5803
6163
  function hostBinaryUnderPrefix(entry, prefix) {
5804
- return existsSync20(join17(harnessBinPath(prefix), entry.binary));
6164
+ return existsSync20(join19(harnessBinPath(prefix), entry.binary));
5805
6165
  }
5806
6166
  function stateOf(entry, installed, location) {
5807
6167
  return {
@@ -5822,7 +6182,7 @@ function probeableHost(env, home) {
5822
6182
  return void 0;
5823
6183
  }
5824
6184
  const prefix = hostPrefix(home);
5825
- if (!existsSync20(join17(root, ".git")) && !existsSync20(prefix)) return void 0;
6185
+ if (!existsSync20(join19(root, ".git")) && !existsSync20(prefix)) return void 0;
5826
6186
  return { root, prefix };
5827
6187
  }
5828
6188
  async function collectStates(root, cwd, run, home, env, only) {
@@ -6053,7 +6413,7 @@ Start it with \`${bin} sandbox\`, then re-run this command.
6053
6413
  `;
6054
6414
  }
6055
6415
  function homeOf(opts) {
6056
- return (opts.homedir ?? homedir3)();
6416
+ return (opts.homedir ?? homedir5)();
6057
6417
  }
6058
6418
  function isInteractive(opts) {
6059
6419
  return opts.interactive ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
@@ -6442,11 +6802,11 @@ async function runHarnessInstall(name, opts, io) {
6442
6802
  }
6443
6803
 
6444
6804
  // src/commands/workspace.ts
6445
- import { existsSync as existsSync21, rmSync as rmSync5 } from "node:fs";
6446
- import { homedir as homedir4 } from "node:os";
6447
- 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";
6448
6808
  function homeOf2(opts) {
6449
- return (opts.homedir ?? homedir4)();
6809
+ return (opts.homedir ?? homedir6)();
6450
6810
  }
6451
6811
  function messageOf2(error) {
6452
6812
  return error instanceof Error ? error.message : String(error);
@@ -6492,7 +6852,7 @@ async function runWorkspaceCreate(name, opts, io) {
6492
6852
  let action;
6493
6853
  const preexisting = existsSync21(root);
6494
6854
  try {
6495
- if (!opts.json && !existsSync21(join18(root, ".git"))) {
6855
+ if (!opts.json && !existsSync21(join20(root, ".git"))) {
6496
6856
  const at = opts.ref !== void 0 ? ` at ${opts.ref}` : "";
6497
6857
  io.stdout(`cloning ${AGRO_REPO_URL}${at} into ${root}\u2026
6498
6858
  `);
@@ -6502,7 +6862,7 @@ async function runWorkspaceCreate(name, opts, io) {
6502
6862
  action = created.action;
6503
6863
  } catch (err) {
6504
6864
  if (opts.ref !== void 0) {
6505
- if (!preexisting) rmSync5(root, { recursive: true, force: true });
6865
+ if (!preexisting) rmSync6(root, { recursive: true, force: true });
6506
6866
  io.stderr(
6507
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)})
6508
6868
  `
@@ -6537,7 +6897,7 @@ async function runWorkspaceList(opts, io) {
6537
6897
  const recorded = recordedRoot(env, home);
6538
6898
  const registry = workspacesRoot(env, home);
6539
6899
  rows = listHostWorkspaces(env, home).map((name) => {
6540
- const root = join18(registry, name);
6900
+ const root = join20(registry, name);
6541
6901
  return { name, root, default: recorded === root };
6542
6902
  });
6543
6903
  } catch (err) {
@@ -6563,8 +6923,8 @@ async function runWorkspaceList(opts, io) {
6563
6923
 
6564
6924
  // src/commands/tool.ts
6565
6925
  import { existsSync as existsSync22 } from "node:fs";
6566
- import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
6567
- 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";
6568
6928
 
6569
6929
  // src/lib/tools/catalog.ts
6570
6930
  var TOOLS_DOC = "docs/installation.md";
@@ -6607,22 +6967,47 @@ var TOOL_CATALOG = Object.freeze([
6607
6967
  ' browser="$resolved"',
6608
6968
  " break",
6609
6969
  "done",
6610
- 'if [ -z "$browser" ]; then',
6611
- ' echo "agent-browser is installed at $prefix/bin/agent-browser, but no Chromium-family browser was found on this host." >&2',
6612
- ' echo "AGRO does not install browser libraries on the host. Point agent-browser at an existing browser, then rerun:" >&2',
6613
- ' echo " export AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chrome" >&2',
6614
- ' 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",
6615
6996
  " exit 1",
6616
6997
  "fi",
6617
- 'echo "agent-browser will drive $browser"',
6618
- '"$prefix/bin/agent-browser" --version >/dev/null'
6998
+ 'echo "agent-browser launched headless Chrome"'
6619
6999
  ].join("\n")
6620
7000
  ]),
6621
7001
  installUser: "sandbox",
6622
7002
  downloadSize: "~1 GB",
7003
+ hostDownloadSize: "~400 MB",
6623
7004
  hostCapable: true,
6624
7005
  uninstallArgv: Object.freeze(["bash", "-lc", "pnpm remove -g agent-browser"]),
6625
- 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
+ ]),
6626
7011
  docsPath: TOOLS_DOC
6627
7012
  }),
6628
7013
  Object.freeze({
@@ -7043,7 +7428,7 @@ function targetFor2(root, run, env) {
7043
7428
  });
7044
7429
  }
7045
7430
  function hostPrefix2(home) {
7046
- return join19(home, ".local");
7431
+ return join21(home, ".local");
7047
7432
  }
7048
7433
  function sandboxMarkerPath2(entry) {
7049
7434
  return `${SANDBOX_HARNESS_PREFIX}/share/agro/tools/${entry.id}.installed`;
@@ -7057,7 +7442,7 @@ async function sandboxMarkerExists2(target, entry) {
7057
7442
  return r.exitCode === 0;
7058
7443
  }
7059
7444
  function hostBinaryUnderPrefix2(entry, prefix) {
7060
- return existsSync22(join19(harnessBinPath(prefix), entry.binary));
7445
+ return existsSync22(join21(harnessBinPath(prefix), entry.binary));
7061
7446
  }
7062
7447
  function pathEntries2(env) {
7063
7448
  return (env.PATH ?? "").split(delimiter3).filter((part) => part !== "");
@@ -7077,7 +7462,7 @@ function messageOf3(error) {
7077
7462
  return error instanceof Error ? error.message : String(error);
7078
7463
  }
7079
7464
  function homeOf3(opts) {
7080
- return (opts.homedir ?? homedir5)();
7465
+ return (opts.homedir ?? homedir7)();
7081
7466
  }
7082
7467
  function platformOf(opts) {
7083
7468
  return opts.platform ?? process.platform;
@@ -7137,7 +7522,7 @@ function probeableHost2(env, home) {
7137
7522
  return void 0;
7138
7523
  }
7139
7524
  const prefix = hostPrefix2(home);
7140
- if (!existsSync22(join19(root, ".git")) && !existsSync22(prefix)) return void 0;
7525
+ if (!existsSync22(join21(root, ".git")) && !existsSync22(prefix)) return void 0;
7141
7526
  return { root, prefix };
7142
7527
  }
7143
7528
  async function collectRows(root, run, home, env, only) {
@@ -7675,7 +8060,7 @@ function fetchRemoteSource(opts) {
7675
8060
 
7676
8061
  // src/cli.ts
7677
8062
  var DEFAULT_SOURCE_CONTROL_DIR = resolve13(
7678
- dirname10(fileURLToPath(import.meta.url)),
8063
+ dirname11(fileURLToPath(import.meta.url)),
7679
8064
  "../.."
7680
8065
  );
7681
8066
  var INTEGRATIONS = {
@@ -7720,6 +8105,7 @@ Usage:
7720
8105
  ${bin} egress <args...> Read the egress proxy log or reload its allowlist (log|reload)
7721
8106
  ${bin} workspace <args...> Create and list host AGRO workspaces (create|list)
7722
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)
7723
8109
  ${bin} harness <args...> Install and inspect agent CLI harnesses
7724
8110
  ${bin} tool <args...> Install and inspect sandbox tooling
7725
8111
  ${bin} gateway <args...> Manage a messaging client session (pi|hermes|openclaw)
@@ -8555,7 +8941,7 @@ function parseGatewayArgs(rest) {
8555
8941
  return { ok: true, args: { help: false, passthrough: [...rest] } };
8556
8942
  }
8557
8943
  function bundledPayloadExists(bundled, exists = existsSync23) {
8558
- return exists(join20(bundled.sourceControlDir, "manifest.json"));
8944
+ return exists(join22(bundled.sourceControlDir, "manifest.json"));
8559
8945
  }
8560
8946
  function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
8561
8947
  const exists = bundled.exists ?? existsSync23;
@@ -8578,7 +8964,7 @@ function resolveVendorSource(args, bundled, bin = AGRO_PRODUCT.bin) {
8578
8964
  function readPayloadVersion(checkoutDir) {
8579
8965
  try {
8580
8966
  const parsed = JSON.parse(
8581
- readFileSync13(join20(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
8967
+ readFileSync15(join22(resolveControlDir(checkoutDir), "cli", "package.json"), "utf8")
8582
8968
  );
8583
8969
  if (parsed && typeof parsed.version === "string") return parsed.version;
8584
8970
  } catch {
@@ -8587,8 +8973,8 @@ function readPayloadVersion(checkoutDir) {
8587
8973
  }
8588
8974
  async function runWithRemoteSource(hooks, fn) {
8589
8975
  const fetch2 = hooks.fetch ?? fetchRemoteSource;
8590
- const mkdtemp = hooks.mkdtemp ?? (() => mkdtempSync5(join20(tmpdir4(), "oh-remote-")));
8591
- 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 }));
8592
8978
  const out = hooks.stdout ?? ((s) => process.stdout.write(s));
8593
8979
  const checkoutDir = mkdtemp();
8594
8980
  try {
@@ -8817,6 +9203,14 @@ async function main(argv) {
8817
9203
  }
8818
9204
  return runEgress(a.verb, a.name === void 0 ? { bin } : { bin, name: a.name }, lifecycleIo());
8819
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
+ }
8820
9214
  if (first === "node") {
8821
9215
  return await runNode(
8822
9216
  argv.slice(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mifune/agro",
3
- "version": "0.23.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",