Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions deploy/list-commands.ts
Original file line number Diff line number Diff line change
@@ -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<GlobalContext>()
.description("List applications in an organization")
.option("--org <name:string>", "The name of the organization")
.option("--limit <n:number>", "Maximum number of apps to return (default 20)")
.option("--cursor <c:string>", "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<GlobalContext>()
.description("Manage applications")
.action(() => {
appsCommand.showHelp();
})
.command("list", appsListCommand)
.alias("ls");

const orgsListCommand = new Command<GlobalContext>()
.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<GlobalContext>()
.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<GlobalContext>()
.description("List deployments (revisions) for an application")
.option("--org <name:string>", "The name of the organization")
.option("--app <name:string>", "The name of the application")
.option(
"--limit <n:number>",
"Maximum number of deployments to return (default 20)",
)
.option("--cursor <c:string>", "Pagination cursor from a previous --json run")
.option(
"--status <status:string>",
`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<GlobalContext>()
.description("Manage deployments (revisions)")
.action(() => {
deploymentsCommand.showHelp();
})
.command("list", deploymentsListCommand)
.alias("ls");
8 changes: 8 additions & 0 deletions deploy/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GlobalContext>()
.description("Setup cloud connections for AWS")
Expand Down Expand Up @@ -334,6 +339,9 @@ deploy your local directory to the specified application.`)
.command("create", createCommand as Command<any>)
.command("env", envCommand as Command<any>)
.command("database", databasesCommand as Command<any>)
.command("apps", appsCommand as Command<any>)
.command("orgs", orgsCommand as Command<any>)
.command("deployments", deploymentsCommand as Command<any>)
.command("logs", logsCommand as Command<any>)
.command("setup-aws", setupAWSCommand as Command<any>)
.command("setup-gcp", setupGCPCommand as Command<any>)
Expand Down
Loading