Skip to content

Commit 3b79e96

Browse files
crowlbotclaude
andcommitted
feat: add apps list, orgs list, deployments list subcommands
Fourth slice of the agent-ergonomics series. Adds the three list/inspect commands that an agent typically needs to navigate the resource hierarchy without having seen the dashboard: - `deno deploy apps list [--limit N] [--cursor C]` — paginated list of applications in the current organization. JSON mode emits `{ items: [{ id, slug, createdAt, updatedAt, layers }], nextCursor, org }`. Uses the existing `apps.listByPage` tRPC procedure. - `deno deploy orgs list` — organizations the current token can access. JSON mode emits `[{ id, slug, name, plan }]`. Uses `orgs.list`. - `deno deploy deployments list [--app A] [--limit N] [--cursor C] [--status S]` — paginated list of revisions for an app. `--status` accepts `skipped|queued|building|succeeded|failed` and is forwarded to the backend's existing filter (any other value comes back as a backend USAGE error through the global envelope). JSON mode emits `{ items: [{ id, status, prod, createdAt, updatedAt, lastStep }], nextCursor, org, app }`. All three honor the global `--json` flag (NDJSON-style human tables otherwise), and the global error envelope from #91 maps any NOT_AUTHENTICATED/NOT_FOUND/NETWORK responses without per-command code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dabbbef commit 3b79e96

2 files changed

Lines changed: 226 additions & 0 deletions

File tree

deploy/list-commands.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { Command } from "@cliffy/command";
2+
import { createTrpcClient } from "../auth.ts";
3+
import { actionHandler, getApp, getOrg } from "../config.ts";
4+
import type { GlobalContext } from "../main.ts";
5+
import {
6+
renderTemporalTimestamp,
7+
tablePrinter,
8+
writeJsonResult,
9+
} from "../util.ts";
10+
11+
interface AppItem {
12+
id: string;
13+
slug: string;
14+
created_at: Date;
15+
updated_at: Date;
16+
layers: Array<{ slug: string }>;
17+
}
18+
19+
interface OrgItem {
20+
id: string;
21+
name: string;
22+
slug: string;
23+
plan: string | null;
24+
}
25+
26+
interface RevisionItem {
27+
id: string;
28+
status: string;
29+
created_at: Date;
30+
updated_at: Date;
31+
prod: boolean;
32+
steps: Array<{ step: string }>;
33+
}
34+
35+
const appsListCommand = new Command<GlobalContext>()
36+
.description("List applications in an organization")
37+
.option("--org <name:string>", "The name of the organization")
38+
.option("--limit <n:number>", "Maximum number of apps to return (default 20)")
39+
.option("--cursor <c:string>", "Pagination cursor from a previous --json run")
40+
.action(actionHandler(async (config, options) => {
41+
config.noCreate();
42+
const org = await getOrg(options, config, options.org);
43+
const trpcClient = createTrpcClient(options);
44+
45+
const res = await trpcClient.query("apps.listByPage", {
46+
cursor: options.cursor,
47+
limit: options.limit ?? 20,
48+
}) as { items: AppItem[]; nextCursor: string | null };
49+
50+
if (options.json) {
51+
writeJsonResult({
52+
items: res.items.map((app) => ({
53+
id: app.id,
54+
slug: app.slug,
55+
createdAt: app.created_at,
56+
updatedAt: app.updated_at,
57+
layers: app.layers.map((l) => l.slug),
58+
})),
59+
nextCursor: res.nextCursor,
60+
org,
61+
});
62+
return;
63+
}
64+
65+
if (res.items.length === 0) {
66+
console.log("No applications in this organization.");
67+
return;
68+
}
69+
70+
tablePrinter(
71+
["SLUG", "CREATED", "UPDATED", "LAYERS"],
72+
res.items,
73+
(app) => [
74+
app.slug,
75+
renderTemporalTimestamp(app.created_at.toISOString()),
76+
renderTemporalTimestamp(app.updated_at.toISOString()),
77+
app.layers.map((l) => l.slug).join(", ") || "—",
78+
],
79+
);
80+
81+
if (res.nextCursor) {
82+
console.log(`\nMore results available; pass --cursor ${res.nextCursor}`);
83+
}
84+
}));
85+
86+
export const appsCommand = new Command<GlobalContext>()
87+
.description("Manage applications")
88+
.action(() => {
89+
appsCommand.showHelp();
90+
})
91+
.command("list", appsListCommand)
92+
.alias("ls");
93+
94+
const orgsListCommand = new Command<GlobalContext>()
95+
.description("List organizations the current token can access")
96+
.action(actionHandler(async (config, options) => {
97+
config.noCreate();
98+
const trpcClient = createTrpcClient(options);
99+
100+
const orgs = await trpcClient.query("orgs.list") as OrgItem[];
101+
102+
if (options.json) {
103+
writeJsonResult(orgs.map((org) => ({
104+
id: org.id,
105+
slug: org.slug,
106+
name: org.name,
107+
plan: org.plan,
108+
})));
109+
return;
110+
}
111+
112+
if (orgs.length === 0) {
113+
console.log("No organizations accessible with this token.");
114+
return;
115+
}
116+
117+
tablePrinter(
118+
["SLUG", "NAME", "PLAN"],
119+
orgs,
120+
(org) => [org.slug, org.name, org.plan ?? "—"],
121+
);
122+
}));
123+
124+
export const orgsCommand = new Command<GlobalContext>()
125+
.description("List organizations")
126+
.action(() => {
127+
orgsCommand.showHelp();
128+
})
129+
.command("list", orgsListCommand)
130+
.alias("ls");
131+
132+
const deploymentStatuses = [
133+
"skipped",
134+
"queued",
135+
"building",
136+
"succeeded",
137+
"failed",
138+
] as const;
139+
type DeploymentStatus = typeof deploymentStatuses[number];
140+
141+
const deploymentsListCommand = new Command<GlobalContext>()
142+
.description("List deployments (revisions) for an application")
143+
.option("--org <name:string>", "The name of the organization")
144+
.option("--app <name:string>", "The name of the application")
145+
.option(
146+
"--limit <n:number>",
147+
"Maximum number of deployments to return (default 20)",
148+
)
149+
.option("--cursor <c:string>", "Pagination cursor from a previous --json run")
150+
.option(
151+
"--status <status:string>",
152+
`Filter by status: one of ${deploymentStatuses.join(", ")}`,
153+
)
154+
.action(actionHandler(async (config, options) => {
155+
config.noCreate();
156+
const org = await getOrg(options, config, options.org);
157+
const { app } = await getApp(options, config, false, org, options.app);
158+
const trpcClient = createTrpcClient(options);
159+
160+
// Cliffy widens the option through its option-builder generics; the
161+
// backend zod-validates and returns a USAGE error if it's not one of
162+
// the enum values, which the global error envelope surfaces fine.
163+
const status = options.status as unknown as DeploymentStatus | undefined;
164+
165+
const res = await trpcClient.query("revisions.listByPage", {
166+
org,
167+
app,
168+
cursor: options.cursor,
169+
limit: options.limit ?? 20,
170+
status,
171+
}) as { items: RevisionItem[]; nextCursor: string | null };
172+
173+
if (options.json) {
174+
writeJsonResult({
175+
items: res.items.map((r) => ({
176+
id: r.id,
177+
status: r.status,
178+
prod: r.prod,
179+
createdAt: r.created_at,
180+
updatedAt: r.updated_at,
181+
lastStep: r.steps.at(-1)?.step ?? null,
182+
})),
183+
nextCursor: res.nextCursor,
184+
org,
185+
app,
186+
});
187+
return;
188+
}
189+
190+
if (res.items.length === 0) {
191+
console.log("No deployments for this application.");
192+
return;
193+
}
194+
195+
tablePrinter(
196+
["REVISION", "STATUS", "PROD", "CREATED", "LAST STEP"],
197+
res.items,
198+
(r) => [
199+
r.id,
200+
r.status,
201+
r.prod ? "yes" : "no",
202+
renderTemporalTimestamp(r.created_at.toISOString()),
203+
r.steps.at(-1)?.step ?? "—",
204+
],
205+
);
206+
207+
if (res.nextCursor) {
208+
console.log(`\nMore results available; pass --cursor ${res.nextCursor}`);
209+
}
210+
}));
211+
212+
export const deploymentsCommand = new Command<GlobalContext>()
213+
.description("Manage deployments (revisions)")
214+
.action(() => {
215+
deploymentsCommand.showHelp();
216+
})
217+
.command("list", deploymentsListCommand)
218+
.alias("ls");

deploy/mod.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import { createTrpcClient, getAuth, tokenStorage } from "../auth.ts";
1010
import { databasesCommand } from "./database.ts";
1111
import { envCommand } from "./env.ts";
1212
import { createCommand } from "./create/mod.ts";
13+
import {
14+
appsCommand,
15+
deploymentsCommand,
16+
orgsCommand,
17+
} from "./list-commands.ts";
1318

1419
const setupAWSCommand = new Command<GlobalContext>()
1520
.description("Setup cloud connections for AWS")
@@ -330,6 +335,9 @@ deploy your local directory to the specified application.`)
330335
.command("create", createCommand)
331336
.command("env", envCommand)
332337
.command("database", databasesCommand)
338+
.command("apps", appsCommand)
339+
.command("orgs", orgsCommand)
340+
.command("deployments", deploymentsCommand)
333341
.command("logs", logsCommand)
334342
.command("setup-aws", setupAWSCommand)
335343
.command("setup-gcp", setupGCPCommand)

0 commit comments

Comments
 (0)