Archived
feat: support deploying pi-web under an arbitrary base path
Make all browser-facing API paths and WebSocket URLs relative so that vite's <base href> can resolve them correctly when pi-web is served from a subpath (e.g. /ai or /test/ai). Also introduce PI_WEB_BASE_PATH so the server can generate plugin module URLs that include the base path.
This commit is contained in:
committed by
Federico Jaramillo Martinez
parent
d72b14f40a
commit
27af5a70ad
@@ -51,7 +51,7 @@ describe("machine-scoped runtime API", () => {
|
||||
await piWebApi.piWebStatus("remote a");
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("/api/machines/remote%20a/pi-web/status");
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("api/machines/remote%20a/pi-web/status");
|
||||
});
|
||||
|
||||
it("requests an uncached update check through the local status route", async () => {
|
||||
@@ -80,7 +80,7 @@ describe("machine-scoped runtime API", () => {
|
||||
await machinesApi.runtime("remote a");
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("/api/machines/remote%20a/runtime");
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("api/machines/remote%20a/runtime");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,9 +97,9 @@ describe("settings config and plugin APIs", () => {
|
||||
await expect(pluginsApi.plugins()).resolves.toEqual(piWebPluginsResponse());
|
||||
|
||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||
"/api/config",
|
||||
"/api/config",
|
||||
"/api/plugins",
|
||||
"api/config",
|
||||
"api/config",
|
||||
"api/plugins",
|
||||
]);
|
||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("PUT");
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ config: { spawnSessions: true } });
|
||||
@@ -117,9 +117,9 @@ describe("settings config and plugin APIs", () => {
|
||||
await expect(pluginsApi.plugins("remote a")).resolves.toEqual(piWebPluginsResponse());
|
||||
|
||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||
"/api/machines/remote%20a/config",
|
||||
"/api/machines/remote%20a/config",
|
||||
"/api/machines/remote%20a/plugins",
|
||||
"api/machines/remote%20a/config",
|
||||
"api/machines/remote%20a/config",
|
||||
"api/machines/remote%20a/plugins",
|
||||
]);
|
||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("PUT");
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ config: { spawnSessions: true } });
|
||||
@@ -144,11 +144,11 @@ describe("Pi package API", () => {
|
||||
await piPackagesApi.update();
|
||||
|
||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||
"/api/pi-packages",
|
||||
"/api/pi-packages/install",
|
||||
"/api/pi-packages/remove",
|
||||
"/api/pi-packages/update",
|
||||
"/api/pi-packages/update",
|
||||
"api/pi-packages",
|
||||
"api/pi-packages/install",
|
||||
"api/pi-packages/remove",
|
||||
"api/pi-packages/update",
|
||||
"api/pi-packages/update",
|
||||
]);
|
||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("POST");
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ source: "npm:@acme/new-tools" });
|
||||
@@ -174,11 +174,11 @@ describe("Pi package API", () => {
|
||||
await piPackagesApi.update(undefined, "remote a");
|
||||
|
||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||
"/api/machines/local/pi-packages",
|
||||
"/api/machines/remote%20a/pi-packages",
|
||||
"/api/machines/remote%20a/pi-packages/install",
|
||||
"/api/machines/remote%20a/pi-packages/remove",
|
||||
"/api/machines/remote%20a/pi-packages/update",
|
||||
"api/machines/local/pi-packages",
|
||||
"api/machines/remote%20a/pi-packages",
|
||||
"api/machines/remote%20a/pi-packages/install",
|
||||
"api/machines/remote%20a/pi-packages/remove",
|
||||
"api/machines/remote%20a/pi-packages/update",
|
||||
]);
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 2)[1]))).toEqual({ source: "npm:@acme/new-tools" });
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 3)[1]))).toEqual({ source: "../project-tools" });
|
||||
@@ -196,10 +196,10 @@ describe("session API compatibility", () => {
|
||||
await expect(sessionsApi.cleanup({ archiveIdleDays: 7, projectCwds: ["/repo"] }, "remote a")).resolves.toEqual(executed);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("/api/machines/remote%20a/sessions/cleanup/preview");
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("api/machines/remote%20a/sessions/cleanup/preview");
|
||||
expect(fetchCall(fetchMock, 0)[1]?.method).toBe("POST");
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 0)[1]))).toEqual({ archiveIdleDays: 7, deleteArchivedDays: null });
|
||||
expect(fetchCall(fetchMock, 1)[0]).toBe("/api/machines/remote%20a/sessions/cleanup");
|
||||
expect(fetchCall(fetchMock, 1)[0]).toBe("api/machines/remote%20a/sessions/cleanup");
|
||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("POST");
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ archiveIdleDays: 7, projectCwds: ["/repo"] });
|
||||
});
|
||||
@@ -213,10 +213,10 @@ describe("session API compatibility", () => {
|
||||
await expect(sessionsApi.deleteArchivedMany([{ id: "s 1", cwd: "/repo" }], "remote a")).resolves.toEqual(deleted);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("/api/machines/remote%20a/sessions/bulk/archive");
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("api/machines/remote%20a/sessions/bulk/archive");
|
||||
expect(fetchCall(fetchMock, 0)[1]?.method).toBe("POST");
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 0)[1]))).toEqual({ sessions: [{ id: "s 1", cwd: "/repo" }, { id: "s 2" }] });
|
||||
expect(fetchCall(fetchMock, 1)[0]).toBe("/api/machines/remote%20a/sessions/bulk/delete-archived");
|
||||
expect(fetchCall(fetchMock, 1)[0]).toBe("api/machines/remote%20a/sessions/bulk/delete-archived");
|
||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("POST");
|
||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ sessions: [{ id: "s 1", cwd: "/repo" }] });
|
||||
});
|
||||
@@ -228,7 +228,7 @@ describe("session API compatibility", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/remote%20a/sessions/s%201/prompt");
|
||||
expect(url).toBe("api/machines/remote%20a/sessions/s%201/prompt");
|
||||
expect(JSON.parse(requestBody(init))).toEqual({ text: "hello", streamingBehavior: "followUp" });
|
||||
});
|
||||
|
||||
@@ -239,7 +239,7 @@ describe("session API compatibility", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/remote%20a/sessions/s%201/prompt");
|
||||
expect(url).toBe("api/machines/remote%20a/sessions/s%201/prompt");
|
||||
expect(JSON.parse(requestBody(init))).toEqual({ cwd: "/repo", text: "hello" });
|
||||
});
|
||||
});
|
||||
@@ -251,7 +251,7 @@ describe("machine-scoped file suggestion API", () => {
|
||||
await filesApi.files("/repo", "README", { projectId: "p 1", workspaceId: "w/1", scope: "tracked", machineId: "remote a", workspaceScoped: true });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("/api/machines/remote%20a/projects/p%201/workspaces/w%2F1/files?q=README&scope=tracked");
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("api/machines/remote%20a/projects/p%201/workspaces/w%2F1/files?q=README&scope=tracked");
|
||||
});
|
||||
|
||||
it("falls back to the legacy cwd route when workspace-scoped suggestions are not enabled", async () => {
|
||||
@@ -260,7 +260,7 @@ describe("machine-scoped file suggestion API", () => {
|
||||
await filesApi.files("/repo", "README", { projectId: "p 1", workspaceId: "w/1", scope: "tracked", machineId: "remote a" });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("/api/machines/remote%20a/files?q=README&scope=tracked&cwd=%2Frepo");
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("api/machines/remote%20a/files?q=README&scope=tracked&cwd=%2Frepo");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -272,7 +272,7 @@ describe("machine-scoped terminal command-run API", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/remote%20a/projects/p%201/workspaces/w%2F1");
|
||||
expect(url).toBe("api/machines/remote%20a/projects/p%201/workspaces/w%2F1");
|
||||
expect(init?.method).toBe("DELETE");
|
||||
});
|
||||
|
||||
@@ -283,7 +283,7 @@ describe("machine-scoped terminal command-run API", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/remote%20a/projects/p%201/workspaces/w%2F1/terminal-command-runs");
|
||||
expect(url).toBe("api/machines/remote%20a/projects/p%201/workspaces/w%2F1/terminal-command-runs");
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(JSON.parse(requestBody(init))).toEqual({ origin: "core", title: "Build", command: "npm test", metadata: {} });
|
||||
});
|
||||
@@ -295,7 +295,7 @@ describe("machine-scoped terminal command-run API", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/remote%20a/projects/p%201/workspaces/w%2F1/terminals");
|
||||
expect(url).toBe("api/machines/remote%20a/projects/p%201/workspaces/w%2F1/terminals");
|
||||
expect(init?.method).toBe("DELETE");
|
||||
});
|
||||
|
||||
@@ -311,9 +311,9 @@ describe("machine-scoped terminal command-run API", () => {
|
||||
await terminalsApi.cancelCommandRun("run 1", "remote a");
|
||||
|
||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||
"/api/machines/remote%20a/terminal-command-runs?projectId=p+1&workspaceId=w%2F1&statuses=running&metadata=%7B%22pi.operation%22%3A%22workspace.delete%22%7D",
|
||||
"/api/machines/remote%20a/terminal-command-runs/run%201",
|
||||
"/api/machines/remote%20a/terminal-command-runs/run%201/cancel",
|
||||
"api/machines/remote%20a/terminal-command-runs?projectId=p+1&workspaceId=w%2F1&statuses=running&metadata=%7B%22pi.operation%22%3A%22workspace.delete%22%7D",
|
||||
"api/machines/remote%20a/terminal-command-runs/run%201",
|
||||
"api/machines/remote%20a/terminal-command-runs/run%201/cancel",
|
||||
]);
|
||||
expect(fetchCall(fetchMock, 2)[1]?.method).toBe("POST");
|
||||
});
|
||||
@@ -323,7 +323,7 @@ describe("machine-scoped terminal command-run API", () => {
|
||||
|
||||
await expect(terminalsApi.getCommandRun("missing", "remote-a")).resolves.toBeUndefined();
|
||||
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("/api/machines/remote-a/terminal-command-runs/missing");
|
||||
expect(fetchCall(fetchMock, 0)[0]).toBe("api/machines/remote-a/terminal-command-runs/missing");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -335,7 +335,7 @@ describe("workspace file write API", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/local/projects/p%201/workspaces/w%2F1/file?path=hello.txt");
|
||||
expect(url).toBe("api/machines/local/projects/p%201/workspaces/w%2F1/file?path=hello.txt");
|
||||
expect(init?.method).toBe("PUT");
|
||||
expect(new Headers(init?.headers).get("content-type")).toBe("text/plain");
|
||||
});
|
||||
@@ -348,7 +348,7 @@ describe("workspace file write API", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url, init] = fetchCall(fetchMock, 0);
|
||||
expect(url).toBe("/api/machines/local/projects/p%201/workspaces/w%2F1/file?path=image.png");
|
||||
expect(url).toBe("api/machines/local/projects/p%201/workspaces/w%2F1/file?path=image.png");
|
||||
expect(init?.method).toBe("PUT");
|
||||
expect(new Headers(init?.headers).get("content-type")).toBe("application/octet-stream");
|
||||
});
|
||||
@@ -386,7 +386,7 @@ describe("workspace file write API", () => {
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [url] = fetchCall(fetchMock, 0);
|
||||
expect(url).toContain("/api/machines/remote%20a/");
|
||||
expect(url).toContain("api/machines/remote%20a/");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
} from "./parsers";
|
||||
import { machineGitDiffUrl, messageUrl } from "./urls";
|
||||
|
||||
const machinePrefix = (machineId = "local") => `/api/machines/${encodeURIComponent(machineId)}`;
|
||||
const machinePrefix = (machineId = "local") => `api/machines/${encodeURIComponent(machineId)}`;
|
||||
|
||||
type SessionLookup = SessionRef | string;
|
||||
|
||||
@@ -100,29 +100,29 @@ function sessionBulkMutationRef(session: SessionLookup): SessionBulkMutationRef
|
||||
}
|
||||
|
||||
function piWebStatusUrl(machineId: string): string {
|
||||
return machineId === "local" ? "/api/pi-web/status" : `${machinePrefix(machineId)}/pi-web/status`;
|
||||
return machineId === "local" ? "api/pi-web/status" : `${machinePrefix(machineId)}/pi-web/status`;
|
||||
}
|
||||
|
||||
export const piWebApi = {
|
||||
piWebStatus: (machineId = "local") => request(piWebStatusUrl(machineId), parsePiWebStatusResponse),
|
||||
checkForUpdates: (machineId = "local") => request(`${piWebStatusUrl(machineId)}?refresh=1`, parsePiWebStatusResponse, { cache: "no-store" }),
|
||||
piWebRuntime: () => request("/api/pi-web/runtime", parsePiWebRuntimeResponse),
|
||||
piWebRuntime: () => request("api/pi-web/runtime", parsePiWebRuntimeResponse),
|
||||
};
|
||||
|
||||
export const machinesApi = {
|
||||
machines: () => request("/api/machines", parseMachinesResponse),
|
||||
addMachine: (input: { name: string; baseUrl: string; token?: string }) => request("/api/machines", parseMachine, { method: "POST", body: JSON.stringify(input) }),
|
||||
deleteMachine: (machineId: string) => request(`/api/machines/${encodeURIComponent(machineId)}`, (value) => value, { method: "DELETE" }),
|
||||
health: (machineId: string) => request(`/api/machines/${encodeURIComponent(machineId)}/health`, parseMachineHealth),
|
||||
runtime: (machineId: string) => request(`/api/machines/${encodeURIComponent(machineId)}/runtime`, parseMachineRuntime),
|
||||
machines: () => request("api/machines", parseMachinesResponse),
|
||||
addMachine: (input: { name: string; baseUrl: string; token?: string }) => request("api/machines", parseMachine, { method: "POST", body: JSON.stringify(input) }),
|
||||
deleteMachine: (machineId: string) => request(`api/machines/${encodeURIComponent(machineId)}`, (value) => value, { method: "DELETE" }),
|
||||
health: (machineId: string) => request(`api/machines/${encodeURIComponent(machineId)}/health`, parseMachineHealth),
|
||||
runtime: (machineId: string) => request(`api/machines/${encodeURIComponent(machineId)}/runtime`, parseMachineRuntime),
|
||||
};
|
||||
|
||||
function configUrl(machineId?: string): string {
|
||||
return machineId === undefined ? "/api/config" : `${machinePrefix(machineId)}/config`;
|
||||
return machineId === undefined ? "api/config" : `${machinePrefix(machineId)}/config`;
|
||||
}
|
||||
|
||||
function pluginsUrl(machineId?: string): string {
|
||||
return machineId === undefined ? "/api/plugins" : `${machinePrefix(machineId)}/plugins`;
|
||||
return machineId === undefined ? "api/plugins" : `${machinePrefix(machineId)}/plugins`;
|
||||
}
|
||||
|
||||
export const configApi = {
|
||||
@@ -135,7 +135,7 @@ export const pluginsApi = {
|
||||
};
|
||||
|
||||
function piPackageUrl(endpoint = "", machineId?: string): string {
|
||||
const baseUrl = machineId === undefined ? "/api/pi-packages" : `${machinePrefix(machineId)}/pi-packages`;
|
||||
const baseUrl = machineId === undefined ? "api/pi-packages" : `${machinePrefix(machineId)}/pi-packages`;
|
||||
return endpoint === "" ? baseUrl : `${baseUrl}/${endpoint}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ function fetchCallToRoute(call: Parameters<FetchLike>, scopedMachineId: string):
|
||||
|
||||
function routeFromMachineUrl(method: string, input: string | URL | Request, scopedMachineId: string): ObservedHttpRoute {
|
||||
const url = toUrl(input);
|
||||
const prefix = `/api/machines/${encodeURIComponent(scopedMachineId)}`;
|
||||
const prefix = `api/machines/${encodeURIComponent(scopedMachineId)}`;
|
||||
if (!url.pathname.startsWith(prefix)) throw new Error(`Expected machine-scoped URL, got ${url.pathname}`);
|
||||
return { method, path: url.pathname.slice(prefix.length) || "/" };
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ describe("machine-scoped socket urls", () => {
|
||||
realtimeEvents();
|
||||
|
||||
expect(webSocketUrls).toEqual([
|
||||
"wss://pi.example.test/api/machines/local/sessions/s1/events?cwd=%2Frepo",
|
||||
"wss://pi.example.test/api/machines/local/sessions/events",
|
||||
"wss://pi.example.test/api/machines/local/events",
|
||||
"api/machines/local/sessions/s1/events?cwd=%2Frepo",
|
||||
"api/machines/local/sessions/events",
|
||||
"api/machines/local/events",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("machine-scoped socket urls", () => {
|
||||
sessionEvents("s1");
|
||||
|
||||
expect(webSocketUrls).toEqual([
|
||||
"wss://pi.example.test/api/machines/local/sessions/s1/events",
|
||||
"api/machines/local/sessions/s1/events",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("machine-scoped socket urls", () => {
|
||||
terminalSocket("p 1", "w/1", "t?1", { cols: 120, rows: 40 }, "remote-a");
|
||||
|
||||
expect(webSocketUrls).toEqual([
|
||||
"wss://pi.example.test/api/machines/remote-a/projects/p%201/workspaces/w%2F1/terminals/t%3F1/socket?cols=120&rows=40",
|
||||
"api/machines/remote-a/projects/p%201/workspaces/w%2F1/terminals/t%3F1/socket?cols=120&rows=40",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,10 +23,9 @@ export function realtimeEvents(machineId = "local"): WebSocket {
|
||||
}
|
||||
|
||||
function machinePrefix(machineId: string): string {
|
||||
return `/api/machines/${encodeURIComponent(machineId)}`;
|
||||
return `api/machines/${encodeURIComponent(machineId)}`;
|
||||
}
|
||||
|
||||
function webSocketBaseUrl(): string {
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${location.host}`;
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function machineGitDiffUrl(machineId: string, projectId: string, workspac
|
||||
if (options?.path !== undefined) params.set("path", options.path);
|
||||
if (options?.staged === true) params.set("staged", "true");
|
||||
const query = params.toString();
|
||||
return `/api/machines/${encodeURIComponent(machineId)}/projects/${encodeURIComponent(projectId)}/workspaces/${encodeURIComponent(workspaceId)}/git/diff${query ? `?${query}` : ""}`;
|
||||
return `api/machines/${encodeURIComponent(machineId)}/projects/${encodeURIComponent(projectId)}/workspaces/${encodeURIComponent(workspaceId)}/git/diff${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
export function messageUrl(session: SessionLookup, options?: { limit?: number; before?: number }, machineId = "local"): string {
|
||||
@@ -25,14 +25,14 @@ export function messageUrl(session: SessionLookup, options?: { limit?: number; b
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options?.before !== undefined) params.set("before", String(options.before));
|
||||
const query = params.toString();
|
||||
return `/api/machines/${encodeURIComponent(machineId)}/sessions/${encodeURIComponent(sessionId(session))}/messages${query === "" ? "" : `?${query}`}`;
|
||||
return `api/machines/${encodeURIComponent(machineId)}/sessions/${encodeURIComponent(sessionId(session))}/messages${query === "" ? "" : `?${query}`}`;
|
||||
}
|
||||
|
||||
export function workspaceFileWriteUrl(projectId: string, workspaceId: string, path: string, options?: { createDirs?: boolean; overwrite?: boolean; machineId?: string }): string {
|
||||
const params = new URLSearchParams({ path });
|
||||
if (options?.createDirs === false) params.set("createDirs", "false");
|
||||
if (options?.overwrite === false) params.set("overwrite", "false");
|
||||
const prefix = `/api/machines/${encodeURIComponent(options?.machineId ?? "local")}`;
|
||||
const prefix = `api/machines/${encodeURIComponent(options?.machineId ?? "local")}`;
|
||||
return `${prefix}/projects/${encodeURIComponent(projectId)}/workspaces/${encodeURIComponent(workspaceId)}/file?${params.toString()}`;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,6 @@ export function workspaceImagePreviewUrl(projectId: string, workspaceId: string,
|
||||
const params = new URLSearchParams();
|
||||
params.set("path", path);
|
||||
if (options?.modifiedAt !== undefined) params.set("v", options.modifiedAt);
|
||||
const prefix = `/api/machines/${encodeURIComponent(options?.machineId ?? "local")}`;
|
||||
const prefix = `api/machines/${encodeURIComponent(options?.machineId ?? "local")}`;
|
||||
return `${prefix}/projects/${encodeURIComponent(projectId)}/workspaces/${encodeURIComponent(workspaceId)}/file/preview?${params.toString()}`;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ describe("workspace upload helpers", () => {
|
||||
|
||||
const xhr = xhrs.only();
|
||||
expect(xhr.method).toBe("PUT");
|
||||
expect(xhr.url).toBe("/api/machines/remote%20a/projects/p%201/workspaces/w%2F1/file?path=manual%2Fhello.txt&overwrite=false");
|
||||
expect(xhr.url).toBe("api/machines/remote%20a/projects/p%201/workspaces/w%2F1/file?path=manual%2Fhello.txt&overwrite=false");
|
||||
expect(xhr.headers.get("content-type")).toBe("text/plain");
|
||||
expect(xhr.body).toBe(file);
|
||||
|
||||
@@ -78,13 +78,13 @@ describe("workspace upload helpers", () => {
|
||||
});
|
||||
|
||||
const first = xhrs.at(0);
|
||||
expect(first.url).toBe("/api/machines/remote%20a/projects/p%201/workspaces/w%2F1/file?path=uploads%2Fmanual%2Fa.txt");
|
||||
expect(first.url).toBe("api/machines/remote%20a/projects/p%201/workspaces/w%2F1/file?path=uploads%2Fmanual%2Fa.txt");
|
||||
first.emitUploadProgress(1, 2);
|
||||
first.respondJson(200, { path: "uploads/manual/a.txt", size: 2, modifiedAt: "2026-06-25T00:00:00.000Z", created: true });
|
||||
await Promise.resolve();
|
||||
|
||||
const second = xhrs.at(1);
|
||||
expect(second.url).toBe("/api/machines/remote%20a/projects/p%201/workspaces/w%2F1/file?path=uploads%2Fmanual%2Fb.txt");
|
||||
expect(second.url).toBe("api/machines/remote%20a/projects/p%201/workspaces/w%2F1/file?path=uploads%2Fmanual%2Fb.txt");
|
||||
second.emitUploadProgress(3, 3);
|
||||
second.respondJson(200, { path: "uploads/manual/b.txt", size: 3, modifiedAt: "2026-06-25T00:00:01.000Z", created: true });
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("workspace upload helpers", () => {
|
||||
});
|
||||
|
||||
const xhr = xhrs.only();
|
||||
expect(xhr.url).toBe("/api/machines/local/projects/p1/workspaces/w1/file?path=uploads%2Fnested.txt&createDirs=false");
|
||||
expect(xhr.url).toBe("api/machines/local/projects/p1/workspaces/w1/file?path=uploads%2Fnested.txt&createDirs=false");
|
||||
xhr.respondJson(200, { path: "uploads/nested.txt", size: 5, modifiedAt: "2026-06-25T00:00:00.000Z", created: true });
|
||||
|
||||
await expect(task.promise).resolves.toEqual([
|
||||
|
||||
@@ -1488,7 +1488,7 @@ export class PiWebApp extends LitElement {
|
||||
const existing = this.machinePluginLoadPromises.get(machine.id);
|
||||
if (existing !== undefined) return existing;
|
||||
|
||||
const load = this.registerExternalPlugins(`PI WEB plugins from ${machine.name}`, () => loadExternalPlugins(`/api/machines/${encodeURIComponent(machine.id)}/pi-web-plugins/manifest.json`, {
|
||||
const load = this.registerExternalPlugins(`PI WEB plugins from ${machine.name}`, () => loadExternalPlugins(`api/machines/${encodeURIComponent(machine.id)}/pi-web-plugins/manifest.json`, {
|
||||
machineId: machine.id,
|
||||
shouldLoadPlugin: (entry) => this.plugins.shouldLoadRemotePlugin(entry.id, entry.machineSpecific),
|
||||
}))
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface LoadExternalPluginsOptions {
|
||||
shouldLoadPlugin?: (entry: PluginManifestEntry) => boolean;
|
||||
}
|
||||
|
||||
export async function loadExternalPlugins(manifestUrl = "/pi-web-plugins/manifest.json", options: LoadExternalPluginsOptions = {}): Promise<PiWebPluginRegistration[]> {
|
||||
export async function loadExternalPlugins(manifestUrl = "pi-web-plugins/manifest.json", options: LoadExternalPluginsOptions = {}): Promise<PiWebPluginRegistration[]> {
|
||||
const manifest = await fetchPluginManifest(manifestUrl);
|
||||
if (manifest === undefined) return [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user