mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
feat: ship phase 1 surface area (v0.2.0)
- primora CLI + @primora/mcp server over generated client - auth users admin panel, mobile bottom nav - release workflow: GHCR images, compiled binaries, npm tarballs - fix gzip Content-Length truncation, nginx upstream re-resolution Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
f77823ad90
commit
420502eec1
90 files changed
+9172
-9718
No files matched your search
@@ -0,0 +1,69 @@
|
||||
import type { AuditLog } from "@primora/api-client";
|
||||
import { ProjectsService } from "@primora/api-client";
|
||||
import pc from "picocolors";
|
||||
|
||||
import { loadConfig } from "../config.js";
|
||||
import { configureClient, requireAuth, requireProject } from "../http.js";
|
||||
import { formatDate, isJson, printJson, printTable } from "../out.js";
|
||||
|
||||
interface AuditOptions {
|
||||
project?: string;
|
||||
q?: string;
|
||||
action?: string;
|
||||
limit?: string;
|
||||
offset?: string;
|
||||
follow?: boolean;
|
||||
interval?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
function row(log: AuditLog): string[] {
|
||||
return [
|
||||
formatDate(log.created_at),
|
||||
log.action,
|
||||
`${log.resource_type} ${log.resource_id}`,
|
||||
log.request_id,
|
||||
];
|
||||
}
|
||||
|
||||
export async function cmdAuditList(options: AuditOptions): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const projectId = requireProject(cfg, options.project);
|
||||
const query = {
|
||||
projectId,
|
||||
q: options.q || undefined,
|
||||
action: options.action || undefined,
|
||||
limit: options.limit ? Number(options.limit) : 50,
|
||||
offset: options.offset ? Number(options.offset) : undefined,
|
||||
};
|
||||
|
||||
const page = await ProjectsService.listAuditLogs(query);
|
||||
if (isJson(options)) {
|
||||
printJson(page);
|
||||
} else {
|
||||
printTable(["TIME", "ACTION", "RESOURCE", "REQUEST"], page.items.map(row));
|
||||
}
|
||||
|
||||
if (!options.follow) return;
|
||||
|
||||
// tail mode: poll and print newly seen ids
|
||||
const seen = new Set(page.items.map((i) => i.id));
|
||||
const interval = Math.max(1, Number(options.interval ?? 2)) * 1000;
|
||||
for (;;) {
|
||||
await new Promise((r) => setTimeout(r, interval));
|
||||
const next = await ProjectsService.listAuditLogs({ ...query, offset: undefined });
|
||||
const fresh = next.items.filter((i) => !seen.has(i.id));
|
||||
for (const id of fresh.map((i) => i.id)) seen.add(id);
|
||||
for (const log of fresh.reverse()) {
|
||||
if (isJson(options)) {
|
||||
process.stdout.write(JSON.stringify(log) + "\n");
|
||||
} else {
|
||||
const [time, action, resource, request] = row(log);
|
||||
process.stdout.write(`${pc.dim(time)} ${action} ${resource} ${pc.dim(request)}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import { PlatformService } from "@primora/api-client";
|
||||
|
||||
import { loadConfig, saveConfig, configFilePath } from "../config.js";
|
||||
import { configureClient, CliError } from "../http.js";
|
||||
import { dim, fail, isJson, printJson, printTable, success } from "../out.js";
|
||||
|
||||
interface LoginOptions {
|
||||
url?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
function cancel(): never {
|
||||
p.cancel("Cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export async function cmdLogin(options: LoginOptions): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
const interactive = process.stdin.isTTY;
|
||||
|
||||
let baseUrl = options.url ?? process.env.PRIMORA_BASE_URL;
|
||||
if (!baseUrl) {
|
||||
if (!interactive) throw new CliError("--url is required in non-interactive mode.");
|
||||
const entered = await p.text({
|
||||
message: "Primora URL",
|
||||
initialValue: cfg.baseUrl,
|
||||
validate: (v) => (/^https?:\/\/.+/.test(v ?? "") ? undefined : "Must be a http(s) URL"),
|
||||
});
|
||||
if (p.isCancel(entered)) cancel();
|
||||
baseUrl = entered;
|
||||
}
|
||||
cfg.baseUrl = baseUrl.replace(/\/+$/, "");
|
||||
|
||||
if (options.apiKey) {
|
||||
if (!/^\w+_\w+_.+/.test(options.apiKey)) {
|
||||
throw new CliError("Malformed API key.", "Expected the full key, e.g. prm_xxxx_…");
|
||||
}
|
||||
cfg.auth = { type: "apiKey", key: options.apiKey };
|
||||
saveConfig(cfg);
|
||||
success(`Saved API key credentials to ${configFilePath()}`);
|
||||
dim("API keys are project-scoped — set context with `primora use --org <id> --project <id>`.");
|
||||
return;
|
||||
}
|
||||
|
||||
let email = options.email;
|
||||
let password = options.password;
|
||||
if (!email || !password) {
|
||||
if (!interactive) {
|
||||
throw new CliError("--email and --password are required in non-interactive mode.");
|
||||
}
|
||||
if (!email) {
|
||||
const entered = await p.text({ message: "Email" });
|
||||
if (p.isCancel(entered)) cancel();
|
||||
email = entered;
|
||||
}
|
||||
if (!password) {
|
||||
const entered = await p.password({ message: "Password" });
|
||||
if (p.isCancel(entered)) cancel();
|
||||
password = entered;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(`${cfg.baseUrl}/auth/sign-in/email`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
origin: cfg.baseUrl,
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = (await res.json().catch(() => undefined)) as { message?: string } | undefined;
|
||||
throw new CliError(body?.message ?? `Sign-in failed (${res.status}).`);
|
||||
}
|
||||
|
||||
const sessionCookie = res.headers
|
||||
.getSetCookie()
|
||||
.map((c) => c.split(";")[0])
|
||||
.find((c) => c.includes("session_token="));
|
||||
if (!sessionCookie) {
|
||||
throw new CliError("Auth service did not return a session cookie.");
|
||||
}
|
||||
const eq = sessionCookie.indexOf("=");
|
||||
cfg.auth = {
|
||||
type: "session",
|
||||
cookieName: sessionCookie.slice(0, eq),
|
||||
sessionToken: sessionCookie.slice(eq + 1),
|
||||
};
|
||||
saveConfig(cfg);
|
||||
|
||||
configureClient(cfg);
|
||||
const me = await PlatformService.getMe();
|
||||
success(`Signed in as ${me.user.email}`);
|
||||
dim(`Config written to ${configFilePath()}`);
|
||||
}
|
||||
|
||||
export async function cmdLogout(): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
delete cfg.auth;
|
||||
saveConfig(cfg);
|
||||
success("Signed out — credentials removed.");
|
||||
}
|
||||
|
||||
export async function cmdWhoami(options: { json?: boolean }): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
configureClient(cfg);
|
||||
const me = await PlatformService.getMe();
|
||||
|
||||
if (isJson(options)) {
|
||||
printJson(me);
|
||||
return;
|
||||
}
|
||||
|
||||
printTable(["USER", "EMAIL", "VERIFIED"], [
|
||||
[me.user.name || "—", me.user.email, me.user.emailVerified ? "yes" : "no"],
|
||||
]);
|
||||
process.stdout.write("\n");
|
||||
printTable(
|
||||
["ORGANIZATION", "ROLE", "PROJECTS"],
|
||||
me.organizations.map((o) => [
|
||||
o.name,
|
||||
o.membershipRole,
|
||||
o.projects.map((pr) => pr.slug).join(", ") || "—",
|
||||
]),
|
||||
);
|
||||
dim(`\n${cfg.baseUrl} · auth: ${cfg.auth?.type ?? "none"}`);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CreateBucketRequest, StorageService } from "@primora/api-client";
|
||||
|
||||
import { loadConfig } from "../config.js";
|
||||
import { configureClient, requireAuth, requireProject } from "../http.js";
|
||||
import { isJson, printJson, printTable, success } from "../out.js";
|
||||
|
||||
export async function cmdBucketsList(options: {
|
||||
project?: string;
|
||||
q?: string;
|
||||
json?: boolean;
|
||||
}): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const projectId = requireProject(cfg, options.project);
|
||||
const { items } = await StorageService.listBuckets({
|
||||
projectId,
|
||||
q: options.q || undefined,
|
||||
});
|
||||
if (isJson(options)) {
|
||||
printJson(items);
|
||||
return;
|
||||
}
|
||||
printTable(
|
||||
["SLUG", "NAME", "VISIBILITY", "ID"],
|
||||
items.map((b) => [b.slug, b.name, b.visibility, b.id]),
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdBucketsCreate(
|
||||
name: string,
|
||||
options: { project?: string; slug?: string; public?: boolean; json?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const projectId = requireProject(cfg, options.project);
|
||||
const slug = options.slug ?? name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
const bucket = await StorageService.createBucket({
|
||||
projectId,
|
||||
requestBody: {
|
||||
name,
|
||||
slug,
|
||||
visibility: options.public
|
||||
? CreateBucketRequest.visibility.PUBLIC
|
||||
: CreateBucketRequest.visibility.PRIVATE,
|
||||
},
|
||||
});
|
||||
if (isJson(options)) {
|
||||
printJson(bucket);
|
||||
return;
|
||||
}
|
||||
success(`Bucket "${bucket.name}" created (${bucket.slug}, ${bucket.visibility})`);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import { PlatformService } from "@primora/api-client";
|
||||
|
||||
import { loadConfig, saveConfig } from "../config.js";
|
||||
import { configureClient, requireAuth } from "../http.js";
|
||||
import { dim, isJson, printJson, success } from "../out.js";
|
||||
|
||||
interface UseOptions {
|
||||
org?: string;
|
||||
project?: string;
|
||||
}
|
||||
|
||||
/** Match an id or slug against a {id, slug, name} record. */
|
||||
function matchRef<T extends { id: string; slug: string; name: string }>(
|
||||
items: T[],
|
||||
ref: string,
|
||||
): T | undefined {
|
||||
return items.find((i) => i.id === ref || i.slug === ref || i.name === ref);
|
||||
}
|
||||
|
||||
export async function cmdUse(options: UseOptions): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const me = await PlatformService.getMe();
|
||||
const orgs = me.organizations;
|
||||
if (orgs.length === 0) {
|
||||
dim("No organizations yet — create one with `primora orgs create <name>`.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.org) {
|
||||
const org = matchRef(orgs, options.org);
|
||||
if (!org) throw new Error(`Organization "${options.org}" not found.`);
|
||||
cfg.organizationId = org.id;
|
||||
if (!org.projects.some((pr) => pr.id === cfg.projectId)) delete cfg.projectId;
|
||||
} else if (process.stdin.isTTY) {
|
||||
const picked = await p.select({
|
||||
message: "Organization",
|
||||
options: orgs.map((o) => ({ value: o.id, label: o.name, hint: o.slug })),
|
||||
initialValue: cfg.organizationId,
|
||||
});
|
||||
if (p.isCancel(picked)) return;
|
||||
cfg.organizationId = picked;
|
||||
if (!orgs.find((o) => o.id === picked)?.projects.some((pr) => pr.id === cfg.projectId)) {
|
||||
delete cfg.projectId;
|
||||
}
|
||||
}
|
||||
|
||||
const org = orgs.find((o) => o.id === cfg.organizationId);
|
||||
if (!org) throw new Error("No organization selected.");
|
||||
|
||||
if (options.project) {
|
||||
const project = matchRef(org.projects, options.project);
|
||||
if (!project) throw new Error(`Project "${options.project}" not found in ${org.name}.`);
|
||||
cfg.projectId = project.id;
|
||||
} else if (process.stdin.isTTY && org.projects.length > 0) {
|
||||
const picked = await p.select({
|
||||
message: "Project",
|
||||
options: org.projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.slug })),
|
||||
initialValue: cfg.projectId,
|
||||
});
|
||||
if (p.isCancel(picked)) {
|
||||
saveConfig(cfg);
|
||||
return;
|
||||
}
|
||||
cfg.projectId = picked;
|
||||
}
|
||||
|
||||
saveConfig(cfg);
|
||||
success(`Context → ${org.slug}${cfg.projectId ? ` / ${org.projects.find((pr) => pr.id === cfg.projectId)?.slug ?? cfg.projectId}` : ""}`);
|
||||
}
|
||||
|
||||
export async function cmdContext(options: { json?: boolean }): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const me = await PlatformService.getMe();
|
||||
const org = me.organizations.find((o) => o.id === cfg.organizationId);
|
||||
const project = org?.projects.find((pr) => pr.id === cfg.projectId);
|
||||
|
||||
if (isJson(options)) {
|
||||
printJson({
|
||||
baseUrl: cfg.baseUrl,
|
||||
auth: cfg.auth?.type ?? null,
|
||||
organization: org ?? null,
|
||||
project: project ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dim(`url ${cfg.baseUrl}`);
|
||||
dim(`auth ${cfg.auth?.type ?? "none"}`);
|
||||
process.stdout.write(`org ${org ? `${org.name} (${org.slug})` : "—"}\n`);
|
||||
process.stdout.write(`project ${project ? `${project.name} (${project.slug})` : "—"}\n`);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import { ProjectsService } from "@primora/api-client";
|
||||
|
||||
import { loadConfig } from "../config.js";
|
||||
import { configureClient, requireAuth, requireProject } from "../http.js";
|
||||
import { dim, formatDate, isJson, printJson, printTable, success } from "../out.js";
|
||||
|
||||
export async function cmdKeysList(options: { project?: string; json?: boolean }): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const projectId = requireProject(cfg, options.project);
|
||||
const { items } = await ProjectsService.listApiKeys({ projectId });
|
||||
if (isJson(options)) {
|
||||
printJson(items);
|
||||
return;
|
||||
}
|
||||
printTable(
|
||||
["ID", "PREFIX", "NAME", "LAST USED", "STATUS"],
|
||||
items.map((k) => [
|
||||
k.id,
|
||||
k.prefix,
|
||||
k.name,
|
||||
formatDate(k.last_used_at),
|
||||
k.revoked_at ? "revoked" : "active",
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdKeysCreate(
|
||||
name: string,
|
||||
options: { project?: string; json?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const projectId = requireProject(cfg, options.project);
|
||||
const key = await ProjectsService.createApiKey({ projectId, requestBody: { name } });
|
||||
if (isJson(options)) {
|
||||
printJson(key);
|
||||
return;
|
||||
}
|
||||
success(`API key "${key.name}" created (${key.prefix})`);
|
||||
process.stdout.write(`${key.secret}\n`);
|
||||
dim("Store it now — the secret is shown once.");
|
||||
}
|
||||
|
||||
export async function cmdKeysRevoke(
|
||||
apiKeyId: string,
|
||||
options: { project?: string; yes?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const projectId = requireProject(cfg, options.project);
|
||||
if (!options.yes && process.stdin.isTTY) {
|
||||
const confirmed = await p.confirm({ message: `Revoke API key ${apiKeyId}?` });
|
||||
if (p.isCancel(confirmed) || !confirmed) return;
|
||||
}
|
||||
await ProjectsService.revokeApiKey({ projectId, apiKeyId });
|
||||
success(`Revoked ${apiKeyId}`);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { statSync } from "node:fs";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import { StorageService } from "@primora/api-client";
|
||||
|
||||
import { loadConfig } from "../config.js";
|
||||
import {
|
||||
apiFetch,
|
||||
CliError,
|
||||
configureClient,
|
||||
requireAuth,
|
||||
requireProject,
|
||||
resolveBucketId,
|
||||
} from "../http.js";
|
||||
import { formatBytes, formatDate, isJson, printJson, printTable, success } from "../out.js";
|
||||
|
||||
const MIME_BY_EXT: Record<string, string> = {
|
||||
".json": "application/json",
|
||||
".txt": "text/plain",
|
||||
".md": "text/markdown",
|
||||
".csv": "text/csv",
|
||||
".html": "text/html",
|
||||
".css": "text/css",
|
||||
".js": "text/javascript",
|
||||
".ts": "text/typescript",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".webp": "image/webp",
|
||||
".pdf": "application/pdf",
|
||||
".zip": "application/zip",
|
||||
".yaml": "application/yaml",
|
||||
".yml": "application/yaml",
|
||||
".xml": "application/xml",
|
||||
};
|
||||
|
||||
function guessType(path: string): string {
|
||||
const dot = path.lastIndexOf(".");
|
||||
if (dot < 0) return "application/octet-stream";
|
||||
return MIME_BY_EXT[path.slice(dot).toLowerCase()] ?? "application/octet-stream";
|
||||
}
|
||||
|
||||
async function bucketIdFor(cfg: ReturnType<typeof loadConfig>, bucketRef: string, projectOverride?: string) {
|
||||
const projectId = requireProject(cfg, projectOverride);
|
||||
return resolveBucketId(projectId, bucketRef);
|
||||
}
|
||||
|
||||
export async function cmdObjectsList(
|
||||
bucketRef: string,
|
||||
options: { project?: string; q?: string; limit?: string; offset?: string; json?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const bucketId = await bucketIdFor(cfg, bucketRef, options.project);
|
||||
const page = await StorageService.listBucketObjects({
|
||||
bucketId,
|
||||
q: options.q || undefined,
|
||||
limit: options.limit ? Number(options.limit) : 50,
|
||||
offset: options.offset ? Number(options.offset) : undefined,
|
||||
});
|
||||
if (isJson(options)) {
|
||||
printJson(page);
|
||||
return;
|
||||
}
|
||||
printTable(
|
||||
["KEY", "TYPE", "SIZE", "CREATED"],
|
||||
page.items.map((o) => [o.object_key, o.content_type, formatBytes(o.size_bytes), formatDate(o.created_at)]),
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdObjectsUpload(
|
||||
bucketRef: string,
|
||||
file: string,
|
||||
options: { project?: string; key?: string; json?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const bucketId = await bucketIdFor(cfg, bucketRef, options.project);
|
||||
const stat = statSync(file); // throws ENOENT with a clear message
|
||||
if (!stat.isFile()) throw new CliError(`Not a file: ${file}`);
|
||||
|
||||
const objectKey = options.key ?? basename(file);
|
||||
const buffer = await readFile(file);
|
||||
const blob = new Blob([buffer], { type: guessType(file) });
|
||||
const object = await StorageService.uploadBucketObject({
|
||||
bucketId,
|
||||
formData: { objectKey, file: blob },
|
||||
});
|
||||
if (isJson(options)) {
|
||||
printJson(object);
|
||||
return;
|
||||
}
|
||||
success(`Uploaded ${object.object_key} (${formatBytes(object.size_bytes)})`);
|
||||
}
|
||||
|
||||
export async function cmdObjectsDownload(
|
||||
bucketRef: string,
|
||||
objectKey: string,
|
||||
options: { project?: string; out?: string },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const bucketId = await bucketIdFor(cfg, bucketRef, options.project);
|
||||
const res = await apiFetch(
|
||||
cfg,
|
||||
`/buckets/${bucketId}/objects/${encodeURIComponent(objectKey)}`,
|
||||
);
|
||||
const outPath = options.out ?? basename(objectKey);
|
||||
const bytes = Buffer.from(await res.arrayBuffer());
|
||||
await writeFile(outPath, bytes);
|
||||
success(`Downloaded ${objectKey} → ${outPath} (${formatBytes(bytes.length)})`);
|
||||
}
|
||||
|
||||
export async function cmdObjectsRemove(
|
||||
bucketRef: string,
|
||||
objectKey: string,
|
||||
options: { project?: string },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const bucketId = await bucketIdFor(cfg, bucketRef, options.project);
|
||||
await StorageService.deleteBucketObject({ bucketId, objectKey });
|
||||
success(`Deleted ${objectKey}`);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { OrganizationsService, PlatformService } from "@primora/api-client";
|
||||
|
||||
import { loadConfig } from "../config.js";
|
||||
import { configureClient, requireAuth } from "../http.js";
|
||||
import { isJson, printJson, printTable, success } from "../out.js";
|
||||
|
||||
export async function cmdOrgsList(options: { json?: boolean }): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const me = await PlatformService.getMe();
|
||||
if (isJson(options)) {
|
||||
printJson(me.organizations);
|
||||
return;
|
||||
}
|
||||
printTable(
|
||||
["ID", "SLUG", "NAME", "ROLE"],
|
||||
me.organizations.map((o) => [o.id, o.slug, o.name, o.membershipRole]),
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdOrgsCreate(
|
||||
name: string,
|
||||
options: { slug?: string; json?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const slug = options.slug ?? name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
const org = await OrganizationsService.createOrganization({
|
||||
requestBody: { name, slug },
|
||||
});
|
||||
if (isJson(options)) {
|
||||
printJson(org);
|
||||
return;
|
||||
}
|
||||
success(`Organization "${org.name}" created (${org.slug})`);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ProjectsService } from "@primora/api-client";
|
||||
|
||||
import { loadConfig } from "../config.js";
|
||||
import { configureClient, requireAuth, requireOrg } from "../http.js";
|
||||
import { isJson, printJson, printTable, success } from "../out.js";
|
||||
|
||||
interface ListOptions {
|
||||
org?: string;
|
||||
q?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export async function cmdProjectsList(options: ListOptions): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const organizationId = requireOrg(cfg, options.org);
|
||||
const { items } = await ProjectsService.listProjects({
|
||||
organizationId,
|
||||
q: options.q || undefined,
|
||||
});
|
||||
if (isJson(options)) {
|
||||
printJson(items);
|
||||
return;
|
||||
}
|
||||
printTable(
|
||||
["ID", "SLUG", "NAME", "ROLE"],
|
||||
items.map((pr) => [pr.id, pr.slug, pr.name, pr.membership_role ?? "—"]),
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdProjectsCreate(
|
||||
name: string,
|
||||
options: { org?: string; slug?: string; description?: string; json?: boolean },
|
||||
): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
requireAuth(cfg);
|
||||
configureClient(cfg);
|
||||
|
||||
const organizationId = requireOrg(cfg, options.org);
|
||||
const slug = options.slug ?? name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
const project = await ProjectsService.createProject({
|
||||
organizationId,
|
||||
requestBody: { name, slug, description: options.description },
|
||||
});
|
||||
if (isJson(options)) {
|
||||
printJson(project);
|
||||
return;
|
||||
}
|
||||
success(`Project "${project.name}" created (${project.slug})`);
|
||||
}
|
||||
Reference in new issue
Block a user