Skip to content

Commit 83f452e

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 an agent typically needs to navigate the resource hierarchy: - `deno deploy apps list [--limit N] [--cursor C]` — paginated apps in the current org. JSON: `{ items, nextCursor, org }`. Uses `apps.listByPage`. - `deno deploy orgs list` — orgs reachable by the token. JSON: `[{ id, slug, name, plan }]`. Uses `orgs.list`. - `deno deploy deployments list [--app A] [--limit N] [--cursor C] [--status S]` — paginated revisions. JSON: `{ items, nextCursor, org, app }`. Uses `revisions.listByPage`. All three honor the global `--json` flag and the global error envelope from the foundation PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3594eb9 commit 83f452e

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
@@ -11,6 +11,11 @@ import { createTrpcClient, getAuth, tokenStorage } from "../auth.ts";
1111
import { databasesCommand } from "./database.ts";
1212
import { envCommand } from "./env.ts";
1313
import { createCommand } from "./create/mod.ts";
14+
import {
15+
appsCommand,
16+
deploymentsCommand,
17+
orgsCommand,
18+
} from "./list-commands.ts";
1419

1520
const setupAWSCommand = new Command<GlobalContext>()
1621
.description("Setup cloud connections for AWS")
@@ -334,6 +339,9 @@ deploy your local directory to the specified application.`)
334339
.command("create", createCommand as Command<any>)
335340
.command("env", envCommand as Command<any>)
336341
.command("database", databasesCommand as Command<any>)
342+
.command("apps", appsCommand as Command<any>)
343+
.command("orgs", orgsCommand as Command<any>)
344+
.command("deployments", deploymentsCommand as Command<any>)
337345
.command("logs", logsCommand as Command<any>)
338346
.command("setup-aws", setupAWSCommand as Command<any>)
339347
.command("setup-gcp", setupGCPCommand as Command<any>)

0 commit comments

Comments
 (0)