diff --git a/deploy/list-commands.ts b/deploy/list-commands.ts new file mode 100644 index 0000000..5657e43 --- /dev/null +++ b/deploy/list-commands.ts @@ -0,0 +1,218 @@ +import { Command } from "@cliffy/command"; +import { createTrpcClient } from "../auth.ts"; +import { actionHandler, getApp, getOrg } from "../config.ts"; +import type { GlobalContext } from "../main.ts"; +import { + renderTemporalTimestamp, + tablePrinter, + writeJsonResult, +} from "../util.ts"; + +interface AppItem { + id: string; + slug: string; + created_at: Date; + updated_at: Date; + layers: Array<{ slug: string }>; +} + +interface OrgItem { + id: string; + name: string; + slug: string; + plan: string | null; +} + +interface RevisionItem { + id: string; + status: string; + created_at: Date; + updated_at: Date; + prod: boolean; + steps: Array<{ step: string }>; +} + +const appsListCommand = new Command() + .description("List applications in an organization") + .option("--org ", "The name of the organization") + .option("--limit ", "Maximum number of apps to return (default 20)") + .option("--cursor ", "Pagination cursor from a previous --json run") + .action(actionHandler(async (config, options) => { + config.noCreate(); + const org = await getOrg(options, config, options.org); + const trpcClient = createTrpcClient(options); + + const res = await trpcClient.query("apps.listByPage", { + cursor: options.cursor, + limit: options.limit ?? 20, + }) as { items: AppItem[]; nextCursor: string | null }; + + if (options.json) { + writeJsonResult({ + items: res.items.map((app) => ({ + id: app.id, + slug: app.slug, + createdAt: app.created_at, + updatedAt: app.updated_at, + layers: app.layers.map((l) => l.slug), + })), + nextCursor: res.nextCursor, + org, + }); + return; + } + + if (res.items.length === 0) { + console.log("No applications in this organization."); + return; + } + + tablePrinter( + ["SLUG", "CREATED", "UPDATED", "LAYERS"], + res.items, + (app) => [ + app.slug, + renderTemporalTimestamp(app.created_at.toISOString()), + renderTemporalTimestamp(app.updated_at.toISOString()), + app.layers.map((l) => l.slug).join(", ") || "—", + ], + ); + + if (res.nextCursor) { + console.log(`\nMore results available; pass --cursor ${res.nextCursor}`); + } + })); + +export const appsCommand = new Command() + .description("Manage applications") + .action(() => { + appsCommand.showHelp(); + }) + .command("list", appsListCommand) + .alias("ls"); + +const orgsListCommand = new Command() + .description("List organizations the current token can access") + .action(actionHandler(async (config, options) => { + config.noCreate(); + const trpcClient = createTrpcClient(options); + + const orgs = await trpcClient.query("orgs.list") as OrgItem[]; + + if (options.json) { + writeJsonResult(orgs.map((org) => ({ + id: org.id, + slug: org.slug, + name: org.name, + plan: org.plan, + }))); + return; + } + + if (orgs.length === 0) { + console.log("No organizations accessible with this token."); + return; + } + + tablePrinter( + ["SLUG", "NAME", "PLAN"], + orgs, + (org) => [org.slug, org.name, org.plan ?? "—"], + ); + })); + +export const orgsCommand = new Command() + .description("List organizations") + .action(() => { + orgsCommand.showHelp(); + }) + .command("list", orgsListCommand) + .alias("ls"); + +const deploymentStatuses = [ + "skipped", + "queued", + "building", + "succeeded", + "failed", +] as const; +type DeploymentStatus = typeof deploymentStatuses[number]; + +const deploymentsListCommand = new Command() + .description("List deployments (revisions) for an application") + .option("--org ", "The name of the organization") + .option("--app ", "The name of the application") + .option( + "--limit ", + "Maximum number of deployments to return (default 20)", + ) + .option("--cursor ", "Pagination cursor from a previous --json run") + .option( + "--status ", + `Filter by status: one of ${deploymentStatuses.join(", ")}`, + ) + .action(actionHandler(async (config, options) => { + config.noCreate(); + const org = await getOrg(options, config, options.org); + const { app } = await getApp(options, config, false, org, options.app); + const trpcClient = createTrpcClient(options); + + // Cliffy widens the option through its option-builder generics; the + // backend zod-validates and returns a USAGE error if it's not one of + // the enum values, which the global error envelope surfaces fine. + const status = options.status as unknown as DeploymentStatus | undefined; + + const res = await trpcClient.query("revisions.listByPage", { + org, + app, + cursor: options.cursor, + limit: options.limit ?? 20, + status, + }) as { items: RevisionItem[]; nextCursor: string | null }; + + if (options.json) { + writeJsonResult({ + items: res.items.map((r) => ({ + id: r.id, + status: r.status, + prod: r.prod, + createdAt: r.created_at, + updatedAt: r.updated_at, + lastStep: r.steps.at(-1)?.step ?? null, + })), + nextCursor: res.nextCursor, + org, + app, + }); + return; + } + + if (res.items.length === 0) { + console.log("No deployments for this application."); + return; + } + + tablePrinter( + ["REVISION", "STATUS", "PROD", "CREATED", "LAST STEP"], + res.items, + (r) => [ + r.id, + r.status, + r.prod ? "yes" : "no", + renderTemporalTimestamp(r.created_at.toISOString()), + r.steps.at(-1)?.step ?? "—", + ], + ); + + if (res.nextCursor) { + console.log(`\nMore results available; pass --cursor ${res.nextCursor}`); + } + })); + +export const deploymentsCommand = new Command() + .description("Manage deployments (revisions)") + .action(() => { + deploymentsCommand.showHelp(); + }) + .command("list", deploymentsListCommand) + .alias("ls"); diff --git a/deploy/mod.ts b/deploy/mod.ts index 49b23b8..0618395 100644 --- a/deploy/mod.ts +++ b/deploy/mod.ts @@ -11,6 +11,11 @@ import { createTrpcClient, getAuth, tokenStorage } from "../auth.ts"; import { databasesCommand } from "./database.ts"; import { envCommand } from "./env.ts"; import { createCommand } from "./create/mod.ts"; +import { + appsCommand, + deploymentsCommand, + orgsCommand, +} from "./list-commands.ts"; const setupAWSCommand = new Command() .description("Setup cloud connections for AWS") @@ -334,6 +339,9 @@ deploy your local directory to the specified application.`) .command("create", createCommand as Command) .command("env", envCommand as Command) .command("database", databasesCommand as Command) + .command("apps", appsCommand as Command) + .command("orgs", orgsCommand as Command) + .command("deployments", deploymentsCommand as Command) .command("logs", logsCommand as Command) .command("setup-aws", setupAWSCommand as Command) .command("setup-gcp", setupGCPCommand as Command)