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
6,279 changes: 4,783 additions & 1,496 deletions packages/api-client/src/schema.ts

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions packages/cli/src/commands/account/_run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { handleCliError, output } from "../../lib/run";
import {
resolveAccountTarget,
type AccountTarget,
type AccountTargetOptions,
} from "../../lib/target";

export type AccountCommandOptions = AccountTargetOptions & {
json?: boolean | undefined;
};

/**
* Run an account-scoped command end to end: resolve the account target, run the
* handler, print the result, and turn any failure into a clean CLI error.
*
* Every account endpoint acts as a user, so failures get the personal access
* token hint.
*/
export async function runAccountAction<T>(opts: {
options: AccountCommandOptions;
handler: (target: AccountTarget) => Promise<T>;
format: (data: T) => string;
}): Promise<void> {
try {
const target = await resolveAccountTarget(opts.options);
const data = await opts.handler(target);
output(data, opts.options, opts.format);
} catch (error) {
handleCliError(error, "user");
}
}
80 changes: 80 additions & 0 deletions packages/cli/src/commands/account/domain/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Command } from "commander";
import { unwrap, unwrapEmpty } from "../../../lib/api";
import { formatDomain, formatDomains } from "../../../lib/format";
import { accountSlugOption, jsonOption, tokenOption } from "../../../options";
import { runAccountAction, type AccountCommandOptions } from "../_run";

export function registerAccountDomain(account: Command) {
const domain = account
.command("domain")
.description(
"Manage the email domains a team is open to: anyone signing up with a verified address on one joins automatically",
);

domain
.command("list")
.description("List the email domains a team is open to")
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action((options: AccountCommandOptions) =>
runAccountAction({
options,
handler: async ({ client, accountSlug }) =>
unwrap(
await client.GET("/accounts/{accountSlug}/domains", {
params: { path: { accountSlug } },
}),
),
format: formatDomains,
}),
);

domain
.command("add")
.description(
"Open a team to an email domain. You must hold a verified address on it; public email providers are refused",
)
.argument("<domain>", "Email domain, e.g. acme.com")
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action((value: string, options: AccountCommandOptions) =>
runAccountAction({
options,
handler: async ({ client, accountSlug }) =>
unwrap(
await client.POST("/accounts/{accountSlug}/domains", {
params: { path: { accountSlug } },
body: { domain: value },
}),
),
format: formatDomain,
}),
);

domain
.command("remove")
.description(
"Close a team to an email domain. Members who already joined through it stay",
)
.argument("<domain>", "Email domain to remove")
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action((value: string, options: AccountCommandOptions) =>
runAccountAction({
options,
handler: async ({ client, accountSlug }) => {
unwrapEmpty(
await client.DELETE("/accounts/{accountSlug}/domains/{domain}", {
params: { path: { accountSlug, domain: value } },
}),
);
return { removed: true, domain: value };
},
format: ({ domain: removed }) =>
`Closed the team to ${removed}. Existing members keep their access.`,
}),
);
}
26 changes: 26 additions & 0 deletions packages/cli/src/commands/account/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Command } from "commander";
import { unwrap } from "../../lib/api";
import { formatAccount } from "../../lib/format";
import { accountSlugOption, jsonOption, tokenOption } from "../../options";
import { runAccountAction, type AccountCommandOptions } from "./_run";

export function registerAccountGet(account: Command) {
account
.command("get")
.description("Fetch an account with its plan and current-period usage")
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action((options: AccountCommandOptions) =>
runAccountAction({
options,
handler: async ({ client, accountSlug }) =>
unwrap(
await client.GET("/accounts/{accountSlug}", {
params: { path: { accountSlug } },
}),
),
format: formatAccount,
}),
);
}
19 changes: 19 additions & 0 deletions packages/cli/src/commands/account/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Command } from "commander";
import { registerAccountDomain } from "./domain";
import { registerAccountGet } from "./get";
import { registerAccountInvite } from "./invite";
import { registerAccountMember } from "./member";
import { registerAccountUpdate } from "./update";

export function accountCommand(program: Command) {
const account = program
.command("account")
.description(
"Inspect an account's plan and usage, and manage a team's members, invites, and email domains",
);
registerAccountGet(account);
registerAccountUpdate(account);
registerAccountMember(account);
registerAccountInvite(account);
registerAccountDomain(account);
}
28 changes: 28 additions & 0 deletions packages/cli/src/commands/account/invite/cancel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Command } from "commander";
import { unwrapEmpty } from "../../../lib/api";
import { accountSlugOption, jsonOption, tokenOption } from "../../../options";
import { runAccountAction, type AccountCommandOptions } from "../_run";

export function registerInviteCancel(invite: Command) {
invite
.command("cancel")
.description("Cancel a pending invite, invalidating its link")
.argument("<inviteId>", "Invite ID, taken from `argos account invite list`")
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action((inviteId: string, options: AccountCommandOptions) =>
runAccountAction({
options,
handler: async ({ client, accountSlug }) => {
unwrapEmpty(
await client.DELETE("/accounts/{accountSlug}/invites/{inviteId}", {
params: { path: { accountSlug, inviteId } },
}),
);
return { cancelled: true, inviteId };
},
format: ({ inviteId }) => `Cancelled invite ${inviteId}.`,
}),
);
}
53 changes: 53 additions & 0 deletions packages/cli/src/commands/account/invite/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Option, type Command } from "commander";
import type { ArgosAPISchema } from "@argos-ci/api-client";
import { unwrap } from "../../../lib/api";
import { formatInvites } from "../../../lib/format";
import { accountSlugOption, jsonOption, tokenOption } from "../../../options";
import { runAccountAction, type AccountCommandOptions } from "../_run";

type TeamUserLevel = ArgosAPISchema.components["schemas"]["TeamUserLevel"];

const TEAM_USER_LEVELS = [
"owner",
"member",
"contributor",
] as const satisfies TeamUserLevel[];

export function registerInviteCreate(invite: Command) {
invite
.command("create")
.description(
"Invite people to a team by email. Re-inviting a pending address refreshes its invite",
)
.argument("<emails...>", "Email addresses to invite")
.addOption(
new Option("--level <level>", "Role to give the invited people")
.choices(TEAM_USER_LEVELS)
.default("member" satisfies TeamUserLevel),
)
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action(
(
emails: string[],
options: AccountCommandOptions & { level: TeamUserLevel },
) =>
runAccountAction({
options,
handler: async ({ client, accountSlug }) =>
unwrap(
await client.POST("/accounts/{accountSlug}/invites", {
params: { path: { accountSlug } },
body: {
members: emails.map((email) => ({
email,
level: options.level,
})),
},
}),
),
format: formatInvites,
}),
);
}
15 changes: 15 additions & 0 deletions packages/cli/src/commands/account/invite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Command } from "commander";
import { registerInviteCancel } from "./cancel";
import { registerInviteCreate } from "./create";
import { registerInviteList } from "./list";
import { registerInviteResetLink } from "./reset-link";

export function registerAccountInvite(account: Command) {
const invite = account
.command("invite")
.description("Invite people to a team and manage pending invitations");
registerInviteList(invite);
registerInviteCreate(invite);
registerInviteCancel(invite);
registerInviteResetLink(invite);
}
47 changes: 47 additions & 0 deletions packages/cli/src/commands/account/invite/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Command } from "commander";
import { unwrap } from "../../../lib/api";
import { formatInvites } from "../../../lib/format";
import { fetchPages } from "../../../lib/pagination";
import {
accountSlugOption,
jsonOption,
limitOption,
toLimit,
tokenOption,
type LimitOption,
} from "../../../options";
import { runAccountAction, type AccountCommandOptions } from "../_run";

type ListOptions = AccountCommandOptions &
LimitOption & { search?: string | undefined };

export function registerInviteList(invite: Command) {
invite
.command("list")
.description("List a team's pending invites, most recent first")
.option("--search <query>", "Match invites on their email address")
.addOption(limitOption)
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action((options: ListOptions) =>
runAccountAction({
options,
handler: ({ client, accountSlug }) =>
fetchPages(toLimit(options.limit), async (pagination) =>
unwrap(
await client.GET("/accounts/{accountSlug}/invites", {
params: {
path: { accountSlug },
query: {
...pagination,
...(options.search ? { search: options.search } : {}),
},
},
}),
),
),
format: formatInvites,
}),
);
}
28 changes: 28 additions & 0 deletions packages/cli/src/commands/account/invite/reset-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Command } from "commander";
import { unwrap } from "../../../lib/api";
import { formatInviteLink } from "../../../lib/format";
import { accountSlugOption, jsonOption, tokenOption } from "../../../options";
import { runAccountAction, type AccountCommandOptions } from "../_run";

export function registerInviteResetLink(invite: Command) {
invite
.command("reset-link")
.description(
"Rotate the team's shared invite link, invalidating the previous one",
)
.addOption(accountSlugOption)
.addOption(tokenOption)
.addOption(jsonOption)
.action((options: AccountCommandOptions) =>
runAccountAction({
options,
handler: async ({ client, accountSlug }) =>
unwrap(
await client.POST("/accounts/{accountSlug}/invite-link/reset", {
params: { path: { accountSlug } },
}),
),
format: formatInviteLink,
}),
);
}
13 changes: 13 additions & 0 deletions packages/cli/src/commands/account/member/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Command } from "commander";
import { registerMemberList } from "./list";
import { registerMemberRemove } from "./remove";
import { registerMemberSetLevel } from "./set-level";

export function registerAccountMember(account: Command) {
const member = account
.command("member")
.description("List a team's members, change their role, or remove them");
registerMemberList(member);
registerMemberSetLevel(member);
registerMemberRemove(member);
}
Loading
Loading