export type ArtifactListScope = | { scope: "all" } | { scope: "global" } | { scope: "project"; projectId: string }; export type ArtifactCommand = | ({ kind: "list" } & ArtifactListScope) | { kind: "upload"; path: string; title: string; description?: string; projectId?: string; } | { kind: "get"; id: string } | { kind: "update"; id: string; title?: string; description?: string | null; projectId?: string | null; path?: string; } | { kind: "delete"; id: string; yes: boolean } | { kind: "restore"; id: string } | { kind: "url"; id: string }; const option = (args: ReadonlyArray, name: string): string | undefined => { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : undefined; }; const valueOptions = new Set(["--project", "--title", "--description", "--file"]); const booleanOptions = new Set(["--global", "--all", "--yes"]); const validateOptions = (args: ReadonlyArray): string | undefined => { const seen = new Set(); for (let index = 0; index < args.length; index += 1) { const value = args[index]; if (!value.startsWith("--")) return `Unexpected artifact argument: ${value}`; if (!valueOptions.has(value) && !booleanOptions.has(value)) { return `Unknown artifact option: ${value}`; } if (seen.has(value)) return `Artifact option can occur only once: ${value}`; seen.add(value); if (valueOptions.has(value)) { const next = args[index + 1]; if (!next || next.startsWith("--")) return `${value} needs a value.`; index += 1; } } return undefined; }; export const parseArtifactArgs = ( positional: ReadonlyArray, ): ArtifactCommand | { error: string } | undefined => { if (positional[0] === "artifacts") { const args = positional.slice(1); const error = validateOptions(args); if (error) return { error }; const projectId = option(args, "--project"); const global = args.includes("--global"); const all = args.includes("--all"); if ([Boolean(projectId), global, all].filter(Boolean).length > 1) { return { error: "Use only one artifact scope: --all, --global, or --project." }; } if (projectId) return { kind: "list", scope: "project", projectId }; if (global) return { kind: "list", scope: "global" }; return { kind: "list", scope: "all" }; } if (positional[0] !== "artifact") return undefined; const subcommand = positional[1]; const id = positional[2]; const args = positional.slice(3); const error = validateOptions(args); if (error) return { error }; const allowedOptions = new Set(); switch (subcommand) { case "upload": allowedOptions.add("--title"); allowedOptions.add("--description"); allowedOptions.add("--project"); break; case "update": allowedOptions.add("--title"); allowedOptions.add("--description"); allowedOptions.add("--project"); allowedOptions.add("--file"); break; case "delete": allowedOptions.add("--yes"); break; } const invalidOption = args.find((value) => value.startsWith("--") && !allowedOptions.has(value)); if (invalidOption) return { error: `Unknown ${subcommand ?? "artifact"} option: ${invalidOption}` }; switch (subcommand) { case "upload": { if (!id) return { error: "Artifact upload needs an HTML file path." }; const title = option(args, "--title"); if (!title) return { error: "Artifact upload needs --title." }; return { kind: "upload", path: id, title, description: option(args, "--description"), projectId: option(args, "--project"), }; } case "get": return id ? { kind: "get", id } : { error: "Artifact get needs an ID." }; case "delete": return id ? { kind: "delete", id, yes: args.includes("--yes") } : { error: "Artifact delete needs an ID." }; case "restore": return id ? { kind: "restore", id } : { error: "Artifact restore needs an ID." }; case "url": return id ? { kind: "url", id } : { error: "Artifact url needs an ID." }; case "update": { if (!id) return { error: "Artifact update needs an ID." }; const projectIndex = args.indexOf("--project"); const projectId = projectIndex >= 0 ? args[projectIndex + 1] === "-" ? null : args[projectIndex + 1] : undefined; const descriptionIndex = args.indexOf("--description"); const description = descriptionIndex >= 0 ? args[descriptionIndex + 1] === "-" ? null : args[descriptionIndex + 1] : undefined; const update = { kind: "update" as const, id, title: option(args, "--title"), description, projectId, path: option(args, "--file"), }; if ( update.title === undefined && update.description === undefined && update.projectId === undefined && update.path === undefined ) { return { error: "Artifact update needs --title, --description, --project, or --file." }; } return update; } default: return undefined; } }; export interface ArtifactDeletePrompt { readonly isTTY?: boolean; readonly ask?: (question: string) => Promise; } const terminalQuestion = async (question: string): Promise => { const { createInterface } = await import("node:readline/promises"); const prompt = createInterface({ input: process.stdin, output: process.stdout }); try { return await prompt.question(question); } finally { prompt.close(); } }; export const artifactDeleteConfirmed = async ( id: string, yes: boolean, prompt: ArtifactDeletePrompt = {}, ): Promise => { if (yes) return true; const isTTY = prompt.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY); if (!isTTY) { throw new Error("Artifact delete needs an interactive terminal or --yes."); } const answer = await (prompt.ask ?? terminalQuestion)( `Delete artifact ${id}? This moves it to trash. [y/N] `, ); return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes"; };