Skip to content

Commit 73b65b9

Browse files
authored
feat: add apps list, orgs list, deployments list subcommands (#94)
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.
1 parent 7acdfc3 commit 73b65b9

4 files changed

Lines changed: 238 additions & 0 deletions

File tree

deploy/apps.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Command } from "@cliffy/command";
2+
import { createTrpcClient } from "../auth.ts";
3+
import { actionHandler, 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+
const appsListCommand = new Command<GlobalContext>()
20+
.description("List applications in an organization")
21+
.option("--org <name:string>", "The name of the organization")
22+
.option("--limit <n:number>", "Maximum number of apps to return (default 20)")
23+
.option("--cursor <c:string>", "Pagination cursor from a previous --json run")
24+
.action(actionHandler(async (config, options) => {
25+
config.noCreate();
26+
const org = await getOrg(options, config, options.org);
27+
const trpcClient = createTrpcClient(options);
28+
29+
const res = await trpcClient.query("apps.listByPage", {
30+
cursor: options.cursor,
31+
limit: options.limit ?? 20,
32+
}) as { items: AppItem[]; nextCursor: string | null };
33+
34+
if (options.json) {
35+
writeJsonResult({
36+
items: res.items.map((app) => ({
37+
id: app.id,
38+
slug: app.slug,
39+
createdAt: app.created_at,
40+
updatedAt: app.updated_at,
41+
layers: app.layers.map((l) => l.slug),
42+
})),
43+
nextCursor: res.nextCursor,
44+
org,
45+
});
46+
return;
47+
}
48+
49+
if (res.items.length === 0) {
50+
console.log("No applications in this organization.");
51+
return;
52+
}
53+
54+
tablePrinter(
55+
["SLUG", "CREATED", "UPDATED", "LAYERS"],
56+
res.items,
57+
(app) => [
58+
app.slug,
59+
renderTemporalTimestamp(app.created_at.toISOString()),
60+
renderTemporalTimestamp(app.updated_at.toISOString()),
61+
app.layers.map((l) => l.slug).join(", ") || "—",
62+
],
63+
);
64+
65+
if (res.nextCursor) {
66+
console.log(`\nMore results available; pass --cursor ${res.nextCursor}`);
67+
}
68+
}));
69+
70+
export const appsCommand = new Command<GlobalContext>()
71+
.description("Manage applications")
72+
.action(() => {
73+
appsCommand.showHelp();
74+
})
75+
.command("list", appsListCommand)
76+
.alias("ls");

deploy/deployments.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 RevisionItem {
12+
id: string;
13+
status: string;
14+
created_at: Date;
15+
updated_at: Date;
16+
prod: boolean;
17+
steps: Array<{ step: string }>;
18+
}
19+
20+
const deploymentStatuses = [
21+
"skipped",
22+
"queued",
23+
"building",
24+
"succeeded",
25+
"failed",
26+
] as const;
27+
type DeploymentStatus = typeof deploymentStatuses[number];
28+
29+
const deploymentsListCommand = new Command<GlobalContext>()
30+
.description("List deployments (revisions) for an application")
31+
.option("--org <name:string>", "The name of the organization")
32+
.option("--app <name:string>", "The name of the application")
33+
.option(
34+
"--limit <n:number>",
35+
"Maximum number of deployments to return (default 20)",
36+
)
37+
.option("--cursor <c:string>", "Pagination cursor from a previous --json run")
38+
.option(
39+
"--status <status:string>",
40+
`Filter by status: one of ${deploymentStatuses.join(", ")}`,
41+
)
42+
.action(actionHandler(async (config, options) => {
43+
config.noCreate();
44+
const org = await getOrg(options, config, options.org);
45+
const { app } = await getApp(options, config, false, org, options.app);
46+
const trpcClient = createTrpcClient(options);
47+
48+
// Cliffy widens the option through its option-builder generics; the
49+
// backend zod-validates and returns a USAGE error if it's not one of
50+
// the enum values, which the global error envelope surfaces fine.
51+
const status = options.status as unknown as DeploymentStatus | undefined;
52+
53+
const res = await trpcClient.query("revisions.listByPage", {
54+
org,
55+
app,
56+
cursor: options.cursor,
57+
limit: options.limit ?? 20,
58+
status,
59+
}) as { items: RevisionItem[]; nextCursor: string | null };
60+
61+
if (options.json) {
62+
writeJsonResult({
63+
items: res.items.map((r) => ({
64+
id: r.id,
65+
status: r.status,
66+
prod: r.prod,
67+
createdAt: r.created_at,
68+
updatedAt: r.updated_at,
69+
lastStep: r.steps.at(-1)?.step ?? null,
70+
})),
71+
nextCursor: res.nextCursor,
72+
org,
73+
app,
74+
});
75+
return;
76+
}
77+
78+
if (res.items.length === 0) {
79+
console.log("No deployments for this application.");
80+
return;
81+
}
82+
83+
tablePrinter(
84+
["REVISION", "STATUS", "PROD", "CREATED", "LAST STEP"],
85+
res.items,
86+
(r) => [
87+
r.id,
88+
r.status,
89+
r.prod ? "yes" : "no",
90+
renderTemporalTimestamp(r.created_at.toISOString()),
91+
r.steps.at(-1)?.step ?? "—",
92+
],
93+
);
94+
95+
if (res.nextCursor) {
96+
console.log(`\nMore results available; pass --cursor ${res.nextCursor}`);
97+
}
98+
}));
99+
100+
export const deploymentsCommand = new Command<GlobalContext>()
101+
.description("Manage deployments (revisions)")
102+
.action(() => {
103+
deploymentsCommand.showHelp();
104+
})
105+
.command("list", deploymentsListCommand)
106+
.alias("ls");

deploy/mod.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ 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 { appsCommand } from "./apps.ts";
14+
import { orgsCommand } from "./orgs.ts";
15+
import { deploymentsCommand } from "./deployments.ts";
1316

1417
const setupAWSCommand = new Command<GlobalContext>()
1518
.description("Setup cloud connections for AWS")
@@ -330,6 +333,9 @@ deploy your local directory to the specified application.`)
330333
.command("create", createCommand)
331334
.command("env", envCommand)
332335
.command("database", databasesCommand)
336+
.command("apps", appsCommand)
337+
.command("orgs", orgsCommand)
338+
.command("deployments", deploymentsCommand)
333339
.command("logs", logsCommand)
334340
.command("setup-aws", setupAWSCommand)
335341
.command("setup-gcp", setupGCPCommand)

deploy/orgs.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Command } from "@cliffy/command";
2+
import { createTrpcClient } from "../auth.ts";
3+
import { actionHandler } from "../config.ts";
4+
import type { GlobalContext } from "../main.ts";
5+
import { tablePrinter, writeJsonResult } from "../util.ts";
6+
7+
interface OrgItem {
8+
id: string;
9+
name: string;
10+
slug: string;
11+
plan: string | null;
12+
}
13+
14+
const orgsListCommand = new Command<GlobalContext>()
15+
.description("List organizations the current token can access")
16+
.action(actionHandler(async (config, options) => {
17+
config.noCreate();
18+
const trpcClient = createTrpcClient(options);
19+
20+
const orgs = await trpcClient.query("orgs.list") as OrgItem[];
21+
22+
if (options.json) {
23+
writeJsonResult(orgs.map((org) => ({
24+
id: org.id,
25+
slug: org.slug,
26+
name: org.name,
27+
plan: org.plan,
28+
})));
29+
return;
30+
}
31+
32+
if (orgs.length === 0) {
33+
console.log("No organizations accessible with this token.");
34+
return;
35+
}
36+
37+
tablePrinter(
38+
["SLUG", "NAME", "PLAN"],
39+
orgs,
40+
(org) => [org.slug, org.name, org.plan ?? "—"],
41+
);
42+
}));
43+
44+
export const orgsCommand = new Command<GlobalContext>()
45+
.description("List organizations")
46+
.action(() => {
47+
orgsCommand.showHelp();
48+
})
49+
.command("list", orgsListCommand)
50+
.alias("ls");

0 commit comments

Comments
 (0)