From 340c61f0e0c1856e3620db253db80577d26c4a02 Mon Sep 17 00:00:00 2001 From: crowlbot <280062030+crowlbot@users.noreply.github.com> Date: Wed, 13 May 2026 13:20:40 +0000 Subject: [PATCH 1/2] feat: add --json, --non-interactive flags and exit-code taxonomy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the CLI usable by AI agents and CI bots: - Global `--json` flag emits a single JSON object/array on stdout and a structured `{ error: { code, message, hint, traceId } }` envelope on stderr. Suppresses spinners, progress bars, and ANSI color so output pipes cleanly into `jq`. Wired into `publish` (final result with revisionId, URL, status, timelines) and `create` (including `--dry-run`, which now emits the resolved build config as JSON). - Global `-y, --non-interactive, --yes` flag makes `requireInteractive()` fail fast with a clear `NON_INTERACTIVE_REQUIRED` error instead of hanging on stdin, even on a TTY. Same flag on both `deploy` and `sandbox` roots. - `ExitCode` enum (OK=0, GENERIC=1, USAGE=2, AUTH=3, NOT_FOUND=4, CONFLICT=5, NETWORK=6). `error()` accepts `{ code, errorCode, hint, response }` and exits with the matching numeric code. tRPC errors are mapped via `mapTrpcError()`: 401/403/NOT_AUTHENTICATED/TOKEN_EXPIRED → AUTH, 404 → NOT_FOUND, 409 → CONFLICT, 5xx → NETWORK. Invalid-token path emits `AUTH_INVALID_TOKEN` with an explicit hint pointing at `DENO_DEPLOY_TOKEN` rather than retrying through a browser. - Move the keychain-unavailable warning from stdout to stderr so it doesn't pollute machine-readable output. Adds `tests/agent.test.ts` covering JSON dry-run, non-interactive short-circuit, `AUTH_INVALID_TOKEN` envelope, and the `-y` alias. Co-Authored-By: Claude Opus 4.7 (1M context) From 4a56bfe5d9f5cdad1c81d0cd2b566146f0cadc03 Mon Sep 17 00:00:00 2001 From: crowlbot <280062030+crowlbot@users.noreply.github.com> Date: Wed, 13 May 2026 13:57:23 +0000 Subject: [PATCH 2/2] feat: add deno deploy whoami + conflict hint in tRPC error mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `deno deploy whoami` lets an agent verify the current token without side effects: calls `orgs.list`, prints a slug/name/plan table in human mode, and emits `{ authenticated, user, orgs }` in `--json` mode. `user` is currently `null` because the deployng tRPC router does not expose user identity; when an `account.me` procedure lands, the field will gain `{ id, name, email, ... }` and existing consumers reading `authenticated` / `orgs[]` keep working. A bad token surfaces the global `AUTH_INVALID_TOKEN` envelope on stderr with exit 3 — no browser is opened. Also a small idempotency upgrade in `mapTrpcError()`: when the backend returns `SLUG_ALREADY_IN_USE`, the global error envelope now carries a hint pointing at the recovery path. Exit code stays `CONFLICT=5`. Co-Authored-By: Claude Opus 4.7 (1M context) --- auth.ts | 10 ++++++- deploy/mod.ts | 65 +++++++++++++++++++++++++++++++++++++++++++-- tests/agent.test.ts | 14 ++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/auth.ts b/auth.ts index 7921813..8a286e2 100644 --- a/auth.ts +++ b/auth.ts @@ -41,7 +41,15 @@ function mapTrpcError( return { code: ExitCode.NOT_FOUND, errorCode: backendCode ?? "NOT_FOUND" }; } if (httpStatus === 409) { - return { code: ExitCode.CONFLICT, errorCode: backendCode ?? "CONFLICT" }; + const hint = backendCode === "SLUG_ALREADY_IN_USE" + ? "A resource with that name already exists. Use a different name, " + + "or run the corresponding update/publish command against the existing one." + : undefined; + return { + code: ExitCode.CONFLICT, + errorCode: backendCode ?? "CONFLICT", + hint, + }; } if (httpStatus !== undefined && httpStatus >= 500) { return { code: ExitCode.NETWORK, errorCode: backendCode ?? "BACKEND" }; diff --git a/deploy/mod.ts b/deploy/mod.ts index 954a5ff..a409722 100644 --- a/deploy/mod.ts +++ b/deploy/mod.ts @@ -1,6 +1,11 @@ import { Command, ValidationError } from "@cliffy/command"; import { green, red, setColorEnabled, yellow } from "@std/fmt/colors"; -import { error, renderTemporalTimestamp } from "../util.ts"; +import { + error, + renderTemporalTimestamp, + tablePrinter, + writeJsonResult, +} from "../util.ts"; import { createSwitchCommand, type GlobalContext } from "../main.ts"; import { VERSION } from "../version.ts"; import { actionHandler, getApp, getOrg } from "../config.ts"; @@ -234,6 +239,61 @@ const logoutCommand = new Command() console.log(`${green("✔")} Successfully logged out`); }); +interface WhoamiOrg { + id: string; + name: string; + slug: string; + plan: string | null; +} + +const whoamiCommand = new Command() + .description( + "Verify the current Deno Deploy token and list reachable organizations", + ) + .example( + "Check that DENO_DEPLOY_TOKEN works", + "whoami --json", + ) + .action(actionHandler(async (config, options) => { + config.noCreate(); + // Touch tokenStorage via the tRPC client; this will surface a clean + // AUTH_INVALID_TOKEN envelope from the errorLink if the token is bad, + // without ever calling `requireInteractive()` or opening a browser. + const trpcClient = createTrpcClient(options); + const orgs = await trpcClient.query("orgs.list") as WhoamiOrg[]; + + if (options.json) { + writeJsonResult({ + authenticated: true, + // The deployng tRPC router does not currently expose user identity, + // so we surface what we can (orgs the token can reach). When that + // procedure lands, this output will gain a `user` field; existing + // consumers reading `authenticated` / `orgs[]` keep working. + user: null, + orgs: orgs.map((org) => ({ + id: org.id, + slug: org.slug, + name: org.name, + plan: org.plan, + })), + }); + return; + } + + console.log( + `${green("✔")} Authenticated. ${orgs.length} reachable organization${ + orgs.length === 1 ? "" : "s" + }:`, + ); + if (orgs.length > 0) { + tablePrinter( + ["SLUG", "NAME", "PLAN"], + orgs, + (org) => [org.slug, org.name, org.plan ?? "—"], + ); + } + })); + export const deployCommand = new Command() .name("deno deploy") .version(VERSION) @@ -341,4 +401,5 @@ deploy your local directory to the specified application.`) .command("setup-gcp", setupGCPCommand) .command("tunnel-login", tunnelLoginCommand) .command("switch", createSwitchCommand(true)) - .command("logout", logoutCommand); + .command("logout", logoutCommand) + .command("whoami", whoamiCommand); diff --git a/tests/agent.test.ts b/tests/agent.test.ts index d83a0f2..15b9c24 100644 --- a/tests/agent.test.ts +++ b/tests/agent.test.ts @@ -119,6 +119,20 @@ Deno.test("setup-aws --non-interactive without --policies surfaces MISSING_FLAG" ); }); +Deno.test("whoami --json with bad token emits AUTH envelope (exit 3, no browser)", async () => { + const res = await deployRaw( + "--json", + "--token", + "obviously-invalid-token", + "--endpoint", + "http://127.0.0.1:1", + "whoami", + ); + assertEquals(res.code, 3, `stderr: ${res.stderr}`); + const envelope = JSON.parse(res.stderr.trim().split("\n").pop()!); + assertEquals(envelope.error.code, "AUTH_INVALID_TOKEN"); +}); + Deno.test("non-zero exit code matches taxonomy for invalid flag (USAGE=2)", async () => { // Cliffy's ValidationError handler exits with code 1 by default; // verify the agent can pattern-match on stderr text either way.