feat: add CI deployment and live progress

This commit is contained in:
2026-09-05 12:09:16 +00:00 Unverified
parent 321f4e807a
commit aa02826dbb
27 changed files with 2711 additions and 180 deletions
+142
View File
@@ -5,6 +5,10 @@ import { apiRequest, type ApiRequestInit } from "../lib/api";
import { toTable } from "../lib/format";
import type {
CreateUserRequest,
ApiKey,
CreateApiKeyRequest,
CreateApiKeyResponse,
ListApiKeysResponse,
ListUsersResponse,
UpdateUserRequest,
User,
@@ -40,6 +44,27 @@ function parseRoles(value: unknown): UserRole[] {
return [...new Set(roles)];
}
function parseCapabilities(
value: unknown,
): CreateApiKeyRequest["capabilities"] {
const capabilities = String(value ?? "")
.split(",")
.map((capability) => capability.trim())
.filter(Boolean);
const allowed = new Set([
"kubernetes:read",
"kubernetes:write",
"kubernetes:exec",
"users:read",
"users:write",
"sessions:revoke",
"platform:adopt",
]);
if (!capabilities.length || capabilities.some((item) => !allowed.has(item)))
throw new Error("Provide at least one valid capability");
return [...new Set(capabilities)] as CreateApiKeyRequest["capabilities"];
}
function renderUsers(users: User[]): string {
if (users.length === 0) return "No users";
return toTable(
@@ -172,6 +197,56 @@ export async function revokeUserSessions(
return `Revoked ${result.revoked} session${result.revoked === 1 ? "" : "s"} for ${result.username}`;
}
function renderApiKeys(keys: ApiKey[]): string {
if (!keys.length) return "No API keys";
return toTable(
keys.map((key) => ({
id: key.id,
capabilities: key.capabilities.join(","),
workspace: key.workspace ?? "",
expires: key.expiresAt,
disabled: key.disabled ? "yes" : "no",
})),
);
}
export async function listApiKeys(
username: string,
request: UsersApiRequest = apiRequest,
): Promise<string> {
const response = await request<ListApiKeysResponse>(
`/users/${encodeURIComponent(username)}/keys`,
);
return renderApiKeys(response.items);
}
export async function createApiKey(
username: string,
body: CreateApiKeyRequest,
request: UsersApiRequest = apiRequest,
): Promise<CreateApiKeyResponse> {
return request<CreateApiKeyResponse>(
`/users/${encodeURIComponent(username)}/keys`,
{ method: "POST", json: body },
);
}
export async function revokeApiKey(
username: string,
id: string,
confirmed: boolean,
request: UsersApiRequest = apiRequest,
): Promise<string> {
if (!confirmed) return "API key revocation cancelled";
await request<void>(
`/users/${encodeURIComponent(username)}/keys/${encodeURIComponent(id)}`,
{
method: "DELETE",
},
);
return `Revoked API key ${id} for ${username}`;
}
const list = defineCommand({
meta: { name: "ls", description: "List users" },
async run() {
@@ -239,6 +314,72 @@ const revoke = defineCommand({
},
});
const keys = defineCommand({
meta: { name: "keys", description: "Manage user API keys" },
subCommands: {
ls: defineCommand({
meta: { name: "ls", description: "List a user's API keys" },
async run({ args }) {
console.log(await listApiKeys(requireUsername(args._[0])));
},
}),
create: defineCommand({
meta: { name: "create", description: "Create an API key" },
args: {
capabilities: {
type: "string",
required: true,
description: "Comma-separated capabilities",
},
workspace: {
type: "string",
description: "Restrict the key to a workspace",
},
expiresDays: {
type: "string",
default: "90",
description: "Expiry in days (1-365)",
},
},
async run({ args }) {
const days = Number(args.expiresDays);
if (!Number.isSafeInteger(days) || days < 1 || days > 365)
throw new Error("--expires-days must be an integer from 1 to 365");
const key = await createApiKey(requireUsername(args._[0]), {
capabilities: parseCapabilities(args.capabilities),
...(args.workspace && { workspace: args.workspace }),
expiresAt: new Date(
Date.now() + days * 24 * 60 * 60 * 1000,
).toISOString(),
});
console.log(
"Store this API key securely now. It will not be shown again:",
);
console.log(key.token);
console.log(renderApiKeys([key]));
},
}),
revoke: defineCommand({
meta: { name: "revoke", description: "Revoke an API key" },
args: {
yes: {
type: "boolean",
alias: "y",
description: "Revoke without confirmation",
},
},
async run({ args }) {
const username = requireUsername(args._[0]);
const id = requireUsername(args._[1]);
const confirmed =
Boolean(args.yes) ||
(await confirmDeletion(`API key '${id}' for ${username}`));
console.log(await revokeApiKey(username, id, confirmed));
},
}),
},
});
export const users = defineCommand({
meta: { name: "users", description: "Administer users" },
subCommands: {
@@ -246,6 +387,7 @@ export const users = defineCommand({
delete: remove,
disable: disabledCommand("disable", true),
enable: disabledCommand("enable", false),
keys,
ls: list,
revoke,
update,