/* eslint-disable no-console */ import { readFileSync } from "node:fs"; import * as c from "../../ansi.js"; import { migrateIconChoice } from "../../icon-choice.js"; import type { LocalApiConnection } from "../local-api-client.js"; import { createLocalApiCommandContext } from "../command-support.js"; import { codeValue, nonEmptyDetails, printDetails, printList, printNextCommands, setCapturedCliExitCode, writeJsonEnvelope, } from "../output.js"; import { apiStatus, listWorkspacesForCli, resolveWorkspaceForCli } from "../resources.js"; export async function cmdWorkspace( storage: LocalApiConnection, action: string | undefined, positional: string[], flags: Record, ): Promise { const mode = action || "list"; const jsonOutput = flags.json === "true"; const { call, output } = createLocalApiCommandContext(storage, jsonOutput); try { if (mode === "list") { const workspaces = await listWorkspacesForCli(storage); output({ workspaces }, () => { printList( `Workspaces (${workspaces.length})`, workspaces.map((workspace) => ({ id: workspace.id, title: workspace.name ?? "(unnamed)", details: [typeof workspace.hostMount === "string" ? workspace.hostMount : ""], })), { empty: "No workspaces configured." }, ); }); return; } if (mode === "get") { const reference = positional[0]; if (!reference) throw new Error("workspace id or name is required"); const workspace = await resolveWorkspaceForCli(storage, reference); output({ workspace }, () => printWorkspaceDetails("Workspace", workspace)); return; } if (mode === "create") { const definition = workspaceDefinitionFromFlags(flags); if (!definition.name) throw new Error("--name or definition.name is required"); const result = await call>("/workspaces", { method: "POST", body: definition, }); output(result, () => { const workspace = result.workspace as Record | undefined; printWorkspaceDetails("✓ Workspace created", workspace, definition.name as string); printNextCommands([ `oppi workspace get ${workspace?.id ?? ""}`, `oppi session create --workspace ${workspace?.id ?? ""} --prompt "..."`, ]); }); return; } if (mode === "update") { const reference = positional[0]; if (!reference) throw new Error("workspace id or name is required"); const workspace = await resolveWorkspaceForCli(storage, reference); const definition = workspaceDefinitionFromFlags(flags, { requireFields: true }); const result = await call>( `/workspaces/${encodeURIComponent(workspace.id)}`, { method: "PUT", body: definition, }, ); output(result, () => { const updated = result.workspace as Record | undefined; printWorkspaceDetails("✓ Workspace updated", updated, workspace.name); }); return; } if (mode === "delete" || mode === "remove") { const reference = positional[0]; if (!reference) throw new Error("workspace id or name is required"); const workspace = await resolveWorkspaceForCli(storage, reference); const result = await call>( `/workspaces/${encodeURIComponent(workspace.id)}`, { method: "DELETE", }, ); output(result, () => { printDetails("✓ Workspace deleted", [["Workspace", codeValue(workspace.id)]]); }); return; } throw new Error("Usage: oppi workspace list|get|create|update|delete"); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); if (jsonOutput) { writeJsonEnvelope({ ok: false, error: { message, ...(apiStatus(error) ? { status: apiStatus(error) } : {}) }, }); setCapturedCliExitCode(1); return; } console.log(c.red(` Error: ${message}`)); process.exit(1); } } function workspaceDefinitionFromFlags( flags: Record, options: { requireFields?: boolean } = {}, ): Record { const definition = flags.definition ? parseJsonFile(flags.definition) : {}; applyStringFlag(definition, flags, "name", "name"); applyStringFlag(definition, flags, "description", "description"); if (Object.prototype.hasOwnProperty.call(flags, "icon")) { definition.icon = iconChoiceFromFlag(flags.icon); } applyStringFlag(definition, flags, "system-prompt", "systemPrompt"); applyStringFlag(definition, flags, "host-mount", "hostMount"); applyStringFlag(definition, flags, "default-model", "defaultModel"); applyStringFlag(definition, flags, "runtime", "runtime"); if (options.requireFields && Object.keys(definition).length === 0) { throw new Error("--definition or at least one workspace field flag is required"); } return definition; } function iconChoiceFromFlag(value: string): Record { const trimmed = value.trim(); if (trimmed.toLowerCase() === "default") return { kind: "default" }; const icon = migrateIconChoice(trimmed); if (icon.kind === "default") { throw new Error("--icon must be default, one Unicode emoji, or an SF Symbol name"); } return icon; } function applyStringFlag( definition: Record, flags: Record, flagName: string, fieldName: string, ): void { if (Object.prototype.hasOwnProperty.call(flags, flagName)) { definition[fieldName] = flags[flagName]; } } function parseJsonFile(path: string): Record { const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("definition must be a JSON object"); } return parsed as Record; } function printWorkspaceDetails( title: string, workspace: Record | undefined, fallbackName?: string, ): void { printDetails( title, nonEmptyDetails([ ["ID", codeValue(typeof workspace?.id === "string" ? workspace.id : "?")], [ "Name", typeof workspace?.name === "string" ? workspace.name : (fallbackName ?? "(unnamed)"), ], ["Path", typeof workspace?.hostMount === "string" ? codeValue(workspace.hostMount) : ""], ["Runtime", typeof workspace?.runtime === "string" ? workspace.runtime : ""], ["Default model", typeof workspace?.defaultModel === "string" ? workspace.defaultModel : ""], ]), ); }