Skip to content
Merged
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
76 changes: 76 additions & 0 deletions deploy/apps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Command } from "@cliffy/command";
import { createTrpcClient } from "../auth.ts";
import { actionHandler, 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 }>;
}

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");
106 changes: 106 additions & 0 deletions deploy/deployments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 RevisionItem {
id: string;
status: string;
created_at: Date;
updated_at: Date;
prod: boolean;
steps: Array<{ step: string }>;
}

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");
6 changes: 6 additions & 0 deletions deploy/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ 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 } from "./apps.ts";
import { orgsCommand } from "./orgs.ts";
import { deploymentsCommand } from "./deployments.ts";

const setupAWSCommand = new Command<GlobalContext>()
.description("Setup cloud connections for AWS")
Expand Down Expand Up @@ -330,6 +333,9 @@ deploy your local directory to the specified application.`)
.command("create", createCommand)
.command("env", envCommand)
.command("database", databasesCommand)
.command("apps", appsCommand)
.command("orgs", orgsCommand)
.command("deployments", deploymentsCommand)
.command("logs", logsCommand)
.command("setup-aws", setupAWSCommand)
.command("setup-gcp", setupGCPCommand)
Expand Down
50 changes: 50 additions & 0 deletions deploy/orgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Command } from "@cliffy/command";
import { createTrpcClient } from "../auth.ts";
import { actionHandler } from "../config.ts";
import type { GlobalContext } from "../main.ts";
import { tablePrinter, writeJsonResult } from "../util.ts";

interface OrgItem {
id: string;
name: string;
slug: string;
plan: string | null;
}

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");
Loading