Archived
feat: add public plugin workspace capabilities
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
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.";
|
||||
@@ -8,46 +7,30 @@ export const tasksConfigRefreshHint = `Fix ${TASKS_CONFIG_PATH}, then click Refr
|
||||
|
||||
const missingWorkspaceFileError = "Path does not exist";
|
||||
|
||||
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
||||
export interface WorkspaceTasksFileReader {
|
||||
readFile(path: string): Promise<WorkspaceTasksFileContent>;
|
||||
}
|
||||
|
||||
interface WorkspaceTasksFileContent {
|
||||
content: string;
|
||||
truncated: boolean;
|
||||
binary: boolean;
|
||||
}
|
||||
|
||||
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<WorkspaceTasksConfigLoadResult> {
|
||||
let response: Response;
|
||||
export async function loadWorkspaceTasksConfig(files: WorkspaceTasksFileReader): Promise<WorkspaceTasksConfigLoadResult> {
|
||||
let file: WorkspaceTasksFileContent;
|
||||
try {
|
||||
response = await deps.fetch(workspaceFileUrl(workspace, TASKS_CONFIG_PATH), { cache: "no-store" });
|
||||
file = await files.readFile(TASKS_CONFIG_PATH);
|
||||
} catch (error) {
|
||||
if (errorMessage(error) === missingWorkspaceFileError) return missing();
|
||||
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`);
|
||||
|
||||
@@ -56,19 +39,6 @@ export async function loadWorkspaceTasksConfig(
|
||||
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",
|
||||
@@ -86,21 +56,10 @@ function unavailable(detail: string): WorkspaceTasksConfigLoadResult {
|
||||
};
|
||||
}
|
||||
|
||||
async function readResponseErrorMessage(response: Response): Promise<string | undefined> {
|
||||
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 errorMessage(error: unknown): string | undefined {
|
||||
return error instanceof Error ? error.message : undefined;
|
||||
}
|
||||
|
||||
function formatUnknownError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user