Skip to content

Commit dabbbef

Browse files
crowlbotclaude
andcommitted
feat: emit JSON for env list, database list/query, and logs (NDJSON)
Third slice of the agent-ergonomics series. Wires `--json` (from #91) into the list/inspect commands that an agent would normally pipe into `jq`: - `env list --json` — array of `{ id, key, value, isSecret, contexts }`. Secret values are emitted as `null` rather than the `***` placeholder so agents can tell "secret" from "literally three asterisks". Contexts are resolved to names. - `database list --json` — array of database instances with `name`, `engine`, `createdAt`, `assignments` (app slugs), `connection` (the safe-to-display fields), and a nested `databases` array. - `database query --json` — `{ rows: [...] }` on success; `{ error: { code, message, ... } }` (via the existing error envelope) with errorCode `POSTGRES_ERROR` or `QUERY_ERROR` on failure. - `logs --json` — NDJSON: one log record per line on stdout, fields flattened to lowerCamelCase (`timestamp`, `traceId`, `severity`, `severityNumber`, `body`, `scope`, `revision`, `attributes`). Pipes cleanly into `jq -c .` or `grep -F`. The "connected, streaming logs" preamble is suppressed. Human output is unchanged when `--json` is not set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cfd9e77 commit dabbbef

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

deploy/database.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Command, ValidationError } from "@cliffy/command";
22
import { createTrpcClient } from "../auth.ts";
3-
import { error, renderTemporalTimestamp, tablePrinter } from "../util.ts";
3+
import {
4+
error,
5+
renderTemporalTimestamp,
6+
tablePrinter,
7+
writeJsonResult,
8+
} from "../util.ts";
49
import { green } from "@std/fmt/colors";
510
import type { GlobalContext } from "../main.ts";
611
import { parse as parseConnectionString } from "pg-connection-string";
@@ -244,6 +249,20 @@ const databasesQueryCommand = new Command<DatabaseContext>()
244249
array: false,
245250
});
246251

252+
if (options.json) {
253+
if (res.kind === "ok") {
254+
writeJsonResult({ rows: res.rows ?? [] });
255+
return;
256+
}
257+
if (res.kind === "postgres_error") {
258+
error(options, res.error, { errorCode: "POSTGRES_ERROR" });
259+
}
260+
if (res.error) {
261+
error(options, res.message, { errorCode: "QUERY_ERROR" });
262+
}
263+
return;
264+
}
265+
247266
if (res.kind === "ok") {
248267
if (
249268
Array.isArray(res.rows) && res.rows.length > 0 &&
@@ -318,6 +337,22 @@ const databasesListCommand = new Command<DatabaseContext>()
318337
} & ConnectionInfo
319338
>;
320339

340+
if (options.json) {
341+
writeJsonResult(list.map((database) => ({
342+
name: database.slug,
343+
engine: database.engine,
344+
createdAt: database.created_at,
345+
assignments: database.assignments.map((a) => a.app_slug),
346+
connection: database.safeConnectionConfig,
347+
databases: database.databases.map((db) => ({
348+
name: db.name,
349+
status: db.status,
350+
createdAt: db.created_at,
351+
})),
352+
})));
353+
return;
354+
}
355+
321356
tablePrinter(
322357
["NAME", "ENGINE", "ASSIGNMENTS", "CONNECTION DETAILS"],
323358
list,

deploy/env.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Command } from "@cliffy/command";
22
import { parse as dotEnvParse } from "@std/dotenv";
3-
import { error, isNonInteractive, tablePrinter } from "../util.ts";
3+
import {
4+
error,
5+
isNonInteractive,
6+
tablePrinter,
7+
writeJsonResult,
8+
} from "../util.ts";
49
import { green } from "@std/fmt/colors";
510
import { createTrpcClient } from "../auth.ts";
611
import type { GlobalContext } from "../main.ts";
@@ -42,6 +47,21 @@ const envListCommand = new Command<EnvCommandContext>()
4247
{ org },
4348
) as Context[];
4449

50+
if (options.json) {
51+
writeJsonResult(envVars.map((envVar) => ({
52+
id: envVar.id,
53+
key: envVar.key,
54+
value: envVar.is_secret ? null : envVar.value,
55+
isSecret: envVar.is_secret,
56+
contexts: envVar.context_ids
57+
? envVar.context_ids.map((id) =>
58+
contexts.find((c) => c.id === id)?.name ?? id
59+
)
60+
: null,
61+
})));
62+
return;
63+
}
64+
4565
if (envVars.length === 0) {
4666
console.log(
4767
"There are no environment variables set on this application.",

deploy/mod.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const logsCommand = new Command<GlobalContext>()
146146
const seenIds = new Set();
147147
let onceConnected = false;
148148

149+
const encoder = new TextEncoder();
149150
const sub = trpcClient.subscription(
150151
"apps.logs",
151152
{
@@ -160,7 +161,7 @@ const logsCommand = new Command<GlobalContext>()
160161
onData: (data: unknown) => {
161162
const typedData = data as "streaming" | null | LogEntry[];
162163
if (typedData === "streaming") {
163-
if (!onceConnected && !options.quiet) {
164+
if (!onceConnected && !options.quiet && !options.json) {
164165
console.log("connected, streaming logs...");
165166
}
166167
onceConnected = true;
@@ -174,6 +175,25 @@ const logsCommand = new Command<GlobalContext>()
174175
seenIds.add(id);
175176
}
176177

178+
if (options.json) {
179+
// NDJSON: one record per line on stdout, severity preserved as
180+
// a numeric field so agents can filter without re-parsing.
181+
Deno.stdout.writeSync(encoder.encode(
182+
JSON.stringify({
183+
timestamp: log.Timestamp,
184+
traceId: log.TraceId || null,
185+
spanId: log.SpanId || null,
186+
severity: log.SeverityText,
187+
severityNumber: log.SeverityNumber,
188+
body: log.Body,
189+
scope: log.ScopeName,
190+
revision: log.Revision,
191+
attributes: log.LogAttributes,
192+
}) + "\n",
193+
));
194+
continue;
195+
}
196+
177197
const prefix = `[${renderTemporalTimestamp(log.Timestamp)}${
178198
log.TraceId ? ` (${log.TraceId})` : ""
179199
}]`;

0 commit comments

Comments
 (0)