import type { Workspace } from "@jmfederico/pi-web/plugin-api"; import { TASKS_CONFIG_PATH, parseTasksConfigText, type WorkspaceTasksConfig } from "./config.js"; export const tasksConfigMissingMessage = "No workspace tasks configured here."; export const tasksConfigMissingHint = `${TASKS_CONFIG_PATH} is optional. Create it in this workspace if you want custom tasks.`; export const tasksConfigUnavailableMessage = "Could not load workspace tasks."; export const tasksConfigRefreshHint = `Fix ${TASKS_CONFIG_PATH}, then click Refresh.`; const missingWorkspaceFileError = "Path does not exist"; export type FetchLike = (input: string, init?: RequestInit) => Promise; export type WorkspaceTasksConfigLoadResult = | { kind: "loaded"; config: WorkspaceTasksConfig; path: string } | { kind: "missing"; message: string; hint: string } | { kind: "unavailable"; message: string; hint: string; detail?: string }; interface WorkspaceFileResponse { content: string; truncated: boolean; binary: boolean; } export async function loadWorkspaceTasksConfig( workspace: Workspace, deps: { fetch: FetchLike } = { fetch: window.fetch.bind(window) }, ): Promise { let response: Response; try { response = await deps.fetch(workspaceFileUrl(workspace, TASKS_CONFIG_PATH), { cache: "no-store" }); } catch (error) { return unavailable(`Unable to read ${TASKS_CONFIG_PATH}: ${formatUnknownError(error)}`); } if (!response.ok) { const errorMessage = await readResponseErrorMessage(response); if (errorMessage === missingWorkspaceFileError) return missing(); const responseSummary = errorMessage === undefined ? `HTTP ${String(response.status)}` : `HTTP ${String(response.status)}: ${errorMessage}`; return unavailable(`Unable to read ${TASKS_CONFIG_PATH}: ${responseSummary}`); } let body: unknown; try { body = await response.json(); } catch (error) { return unavailable(`Invalid response while reading ${TASKS_CONFIG_PATH}: ${formatUnknownError(error)}`); } const file = parseWorkspaceFileResponse(body); if (file === undefined) return unavailable(`Invalid response while reading ${TASKS_CONFIG_PATH}`); if (file.binary) return unavailable(`${TASKS_CONFIG_PATH} must be a text file`); if (file.truncated) return unavailable(`${TASKS_CONFIG_PATH} is too large and was truncated`); const result = parseTasksConfigText(file.content); if (!result.ok) return unavailable(result.error); return { kind: "loaded", config: result.config, path: TASKS_CONFIG_PATH }; } export function workspaceFileUrl(workspace: Workspace, path: string): string { return `/api/projects/${encodeURIComponent(workspace.projectId)}/workspaces/${encodeURIComponent(workspace.id)}/file?path=${encodeURIComponent(path)}`; } export function parseWorkspaceFileResponse(value: unknown): WorkspaceFileResponse | undefined { if (!isRecord(value)) return undefined; const content = value["content"]; const truncated = value["truncated"]; const binary = value["binary"]; if (typeof content !== "string" || typeof truncated !== "boolean" || typeof binary !== "boolean") return undefined; return { content, truncated, binary }; } function missing(): WorkspaceTasksConfigLoadResult { return { kind: "missing", message: tasksConfigMissingMessage, hint: tasksConfigMissingHint, }; } function unavailable(detail: string): WorkspaceTasksConfigLoadResult { return { kind: "unavailable", message: tasksConfigUnavailableMessage, hint: tasksConfigRefreshHint, detail, }; } async function readResponseErrorMessage(response: Response): Promise { try { const body: unknown = await response.json(); if (!isRecord(body)) return undefined; const error = body["error"]; return typeof error === "string" ? error : undefined; } catch { return undefined; } } function formatUnknownError(error: unknown): string { return error instanceof Error ? error.message : String(error); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }