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");
|
await piWebApi.piWebStatus("remote a");
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
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 () => {
|
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");
|
await machinesApi.runtime("remote a");
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
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());
|
await expect(pluginsApi.plugins()).resolves.toEqual(piWebPluginsResponse());
|
||||||
|
|
||||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||||
"/api/config",
|
"api/config",
|
||||||
"/api/config",
|
"api/config",
|
||||||
"/api/plugins",
|
"api/plugins",
|
||||||
]);
|
]);
|
||||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("PUT");
|
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("PUT");
|
||||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ config: { spawnSessions: true } });
|
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());
|
await expect(pluginsApi.plugins("remote a")).resolves.toEqual(piWebPluginsResponse());
|
||||||
|
|
||||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||||
"/api/machines/remote%20a/config",
|
"api/machines/remote%20a/config",
|
||||||
"/api/machines/remote%20a/config",
|
"api/machines/remote%20a/config",
|
||||||
"/api/machines/remote%20a/plugins",
|
"api/machines/remote%20a/plugins",
|
||||||
]);
|
]);
|
||||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("PUT");
|
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("PUT");
|
||||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ config: { spawnSessions: true } });
|
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ config: { spawnSessions: true } });
|
||||||
@@ -144,11 +144,11 @@ describe("Pi package API", () => {
|
|||||||
await piPackagesApi.update();
|
await piPackagesApi.update();
|
||||||
|
|
||||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||||
"/api/pi-packages",
|
"api/pi-packages",
|
||||||
"/api/pi-packages/install",
|
"api/pi-packages/install",
|
||||||
"/api/pi-packages/remove",
|
"api/pi-packages/remove",
|
||||||
"/api/pi-packages/update",
|
"api/pi-packages/update",
|
||||||
"/api/pi-packages/update",
|
"api/pi-packages/update",
|
||||||
]);
|
]);
|
||||||
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("POST");
|
expect(fetchCall(fetchMock, 1)[1]?.method).toBe("POST");
|
||||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ source: "npm:@acme/new-tools" });
|
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");
|
await piPackagesApi.update(undefined, "remote a");
|
||||||
|
|
||||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
||||||
"/api/machines/local/pi-packages",
|
"api/machines/local/pi-packages",
|
||||||
"/api/machines/remote%20a/pi-packages",
|
"api/machines/remote%20a/pi-packages",
|
||||||
"/api/machines/remote%20a/pi-packages/install",
|
"api/machines/remote%20a/pi-packages/install",
|
||||||
"/api/machines/remote%20a/pi-packages/remove",
|
"api/machines/remote%20a/pi-packages/remove",
|
||||||
"/api/machines/remote%20a/pi-packages/update",
|
"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, 2)[1]))).toEqual({ source: "npm:@acme/new-tools" });
|
||||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 3)[1]))).toEqual({ source: "../project-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);
|
await expect(sessionsApi.cleanup({ archiveIdleDays: 7, projectCwds: ["/repo"] }, "remote a")).resolves.toEqual(executed);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
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(fetchCall(fetchMock, 0)[1]?.method).toBe("POST");
|
||||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 0)[1]))).toEqual({ archiveIdleDays: 7, deleteArchivedDays: null });
|
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(fetchCall(fetchMock, 1)[1]?.method).toBe("POST");
|
||||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ archiveIdleDays: 7, projectCwds: ["/repo"] });
|
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);
|
await expect(sessionsApi.deleteArchivedMany([{ id: "s 1", cwd: "/repo" }], "remote a")).resolves.toEqual(deleted);
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
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(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(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(fetchCall(fetchMock, 1)[1]?.method).toBe("POST");
|
||||||
expect(JSON.parse(requestBody(fetchCall(fetchMock, 1)[1]))).toEqual({ sessions: [{ id: "s 1", cwd: "/repo" }] });
|
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();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url, init] = fetchCall(fetchMock, 0);
|
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" });
|
expect(JSON.parse(requestBody(init))).toEqual({ text: "hello", streamingBehavior: "followUp" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ describe("session API compatibility", () => {
|
|||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url, init] = fetchCall(fetchMock, 0);
|
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" });
|
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 });
|
await filesApi.files("/repo", "README", { projectId: "p 1", workspaceId: "w/1", scope: "tracked", machineId: "remote a", workspaceScoped: true });
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
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 () => {
|
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" });
|
await filesApi.files("/repo", "README", { projectId: "p 1", workspaceId: "w/1", scope: "tracked", machineId: "remote a" });
|
||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
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();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url, init] = fetchCall(fetchMock, 0);
|
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");
|
expect(init?.method).toBe("DELETE");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ describe("machine-scoped terminal command-run API", () => {
|
|||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url, init] = fetchCall(fetchMock, 0);
|
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(init?.method).toBe("POST");
|
||||||
expect(JSON.parse(requestBody(init))).toEqual({ origin: "core", title: "Build", command: "npm test", metadata: {} });
|
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();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url, init] = fetchCall(fetchMock, 0);
|
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");
|
expect(init?.method).toBe("DELETE");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,9 +311,9 @@ describe("machine-scoped terminal command-run API", () => {
|
|||||||
await terminalsApi.cancelCommandRun("run 1", "remote a");
|
await terminalsApi.cancelCommandRun("run 1", "remote a");
|
||||||
|
|
||||||
expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([
|
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?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",
|
||||||
"/api/machines/remote%20a/terminal-command-runs/run%201/cancel",
|
"api/machines/remote%20a/terminal-command-runs/run%201/cancel",
|
||||||
]);
|
]);
|
||||||
expect(fetchCall(fetchMock, 2)[1]?.method).toBe("POST");
|
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();
|
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();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url, init] = fetchCall(fetchMock, 0);
|
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(init?.method).toBe("PUT");
|
||||||
expect(new Headers(init?.headers).get("content-type")).toBe("text/plain");
|
expect(new Headers(init?.headers).get("content-type")).toBe("text/plain");
|
||||||
});
|
});
|
||||||
@@ -348,7 +348,7 @@ describe("workspace file write API", () => {
|
|||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url, init] = fetchCall(fetchMock, 0);
|
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(init?.method).toBe("PUT");
|
||||||
expect(new Headers(init?.headers).get("content-type")).toBe("application/octet-stream");
|
expect(new Headers(init?.headers).get("content-type")).toBe("application/octet-stream");
|
||||||
});
|
});
|
||||||
@@ -386,7 +386,7 @@ describe("workspace file write API", () => {
|
|||||||
|
|
||||||
expect(fetchMock).toHaveBeenCalledOnce();
|
expect(fetchMock).toHaveBeenCalledOnce();
|
||||||
const [url] = fetchCall(fetchMock, 0);
|
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";
|
} from "./parsers";
|
||||||
import { machineGitDiffUrl, messageUrl } from "./urls";
|
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;
|
type SessionLookup = SessionRef | string;
|
||||||
|
|
||||||
@@ -100,29 +100,29 @@ function sessionBulkMutationRef(session: SessionLookup): SessionBulkMutationRef
|
|||||||
}
|
}
|
||||||
|
|
||||||
function piWebStatusUrl(machineId: string): string {
|
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 = {
|
export const piWebApi = {
|
||||||
piWebStatus: (machineId = "local") => request(piWebStatusUrl(machineId), parsePiWebStatusResponse),
|
piWebStatus: (machineId = "local") => request(piWebStatusUrl(machineId), parsePiWebStatusResponse),
|
||||||
checkForUpdates: (machineId = "local") => request(`${piWebStatusUrl(machineId)}?refresh=1`, parsePiWebStatusResponse, { cache: "no-store" }),
|
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 = {
|
export const machinesApi = {
|
||||||
machines: () => request("/api/machines", parseMachinesResponse),
|
machines: () => request("api/machines", parseMachinesResponse),
|
||||||
addMachine: (input: { name: string; baseUrl: string; token?: string }) => request("/api/machines", parseMachine, { method: "POST", body: JSON.stringify(input) }),
|
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" }),
|
deleteMachine: (machineId: string) => request(`api/machines/${encodeURIComponent(machineId)}`, (value) => value, { method: "DELETE" }),
|
||||||
health: (machineId: string) => request(`/api/machines/${encodeURIComponent(machineId)}/health`, parseMachineHealth),
|
health: (machineId: string) => request(`api/machines/${encodeURIComponent(machineId)}/health`, parseMachineHealth),
|
||||||
runtime: (machineId: string) => request(`/api/machines/${encodeURIComponent(machineId)}/runtime`, parseMachineRuntime),
|
runtime: (machineId: string) => request(`api/machines/${encodeURIComponent(machineId)}/runtime`, parseMachineRuntime),
|
||||||
};
|
};
|
||||||
|
|
||||||
function configUrl(machineId?: string): string {
|
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 {
|
function pluginsUrl(machineId?: string): string {
|
||||||
return machineId === undefined ? "/api/plugins" : `${machinePrefix(machineId)}/plugins`;
|
return machineId === undefined ? "api/plugins" : `${machinePrefix(machineId)}/plugins`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const configApi = {
|
export const configApi = {
|
||||||
@@ -135,7 +135,7 @@ export const pluginsApi = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function piPackageUrl(endpoint = "", machineId?: string): string {
|
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}`;
|
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 {
|
function routeFromMachineUrl(method: string, input: string | URL | Request, scopedMachineId: string): ObservedHttpRoute {
|
||||||
const url = toUrl(input);
|
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}`);
|
if (!url.pathname.startsWith(prefix)) throw new Error(`Expected machine-scoped URL, got ${url.pathname}`);
|
||||||
return { method, path: url.pathname.slice(prefix.length) || "/" };
|
return { method, path: url.pathname.slice(prefix.length) || "/" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ describe("machine-scoped socket urls", () => {
|
|||||||
realtimeEvents();
|
realtimeEvents();
|
||||||
|
|
||||||
expect(webSocketUrls).toEqual([
|
expect(webSocketUrls).toEqual([
|
||||||
"wss://pi.example.test/api/machines/local/sessions/s1/events?cwd=%2Frepo",
|
"api/machines/local/sessions/s1/events?cwd=%2Frepo",
|
||||||
"wss://pi.example.test/api/machines/local/sessions/events",
|
"api/machines/local/sessions/events",
|
||||||
"wss://pi.example.test/api/machines/local/events",
|
"api/machines/local/events",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ describe("machine-scoped socket urls", () => {
|
|||||||
sessionEvents("s1");
|
sessionEvents("s1");
|
||||||
|
|
||||||
expect(webSocketUrls).toEqual([
|
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");
|
terminalSocket("p 1", "w/1", "t?1", { cols: 120, rows: 40 }, "remote-a");
|
||||||
|
|
||||||
expect(webSocketUrls).toEqual([
|
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 {
|
function machinePrefix(machineId: string): string {
|
||||||
return `/api/machines/${encodeURIComponent(machineId)}`;
|
return `api/machines/${encodeURIComponent(machineId)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function webSocketBaseUrl(): string {
|
function webSocketBaseUrl(): string {
|
||||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
return "";
|
||||||
return `${protocol}//${location.host}`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function machineGitDiffUrl(machineId: string, projectId: string, workspac
|
|||||||
if (options?.path !== undefined) params.set("path", options.path);
|
if (options?.path !== undefined) params.set("path", options.path);
|
||||||
if (options?.staged === true) params.set("staged", "true");
|
if (options?.staged === true) params.set("staged", "true");
|
||||||
const query = params.toString();
|
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 {
|
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?.limit !== undefined) params.set("limit", String(options.limit));
|
||||||
if (options?.before !== undefined) params.set("before", String(options.before));
|
if (options?.before !== undefined) params.set("before", String(options.before));
|
||||||
const query = params.toString();
|
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 {
|
export function workspaceFileWriteUrl(projectId: string, workspaceId: string, path: string, options?: { createDirs?: boolean; overwrite?: boolean; machineId?: string }): string {
|
||||||
const params = new URLSearchParams({ path });
|
const params = new URLSearchParams({ path });
|
||||||
if (options?.createDirs === false) params.set("createDirs", "false");
|
if (options?.createDirs === false) params.set("createDirs", "false");
|
||||||
if (options?.overwrite === false) params.set("overwrite", "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()}`;
|
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();
|
const params = new URLSearchParams();
|
||||||
params.set("path", path);
|
params.set("path", path);
|
||||||
if (options?.modifiedAt !== undefined) params.set("v", options.modifiedAt);
|
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()}`;
|
return `${prefix}/projects/${encodeURIComponent(projectId)}/workspaces/${encodeURIComponent(workspaceId)}/file/preview?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe("workspace upload helpers", () => {
|
|||||||
|
|
||||||
const xhr = xhrs.only();
|
const xhr = xhrs.only();
|
||||||
expect(xhr.method).toBe("PUT");
|
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.headers.get("content-type")).toBe("text/plain");
|
||||||
expect(xhr.body).toBe(file);
|
expect(xhr.body).toBe(file);
|
||||||
|
|
||||||
@@ -78,13 +78,13 @@ describe("workspace upload helpers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const first = xhrs.at(0);
|
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.emitUploadProgress(1, 2);
|
||||||
first.respondJson(200, { path: "uploads/manual/a.txt", size: 2, modifiedAt: "2026-06-25T00:00:00.000Z", created: true });
|
first.respondJson(200, { path: "uploads/manual/a.txt", size: 2, modifiedAt: "2026-06-25T00:00:00.000Z", created: true });
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
const second = xhrs.at(1);
|
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.emitUploadProgress(3, 3);
|
||||||
second.respondJson(200, { path: "uploads/manual/b.txt", size: 3, modifiedAt: "2026-06-25T00:00:01.000Z", created: true });
|
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();
|
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 });
|
xhr.respondJson(200, { path: "uploads/nested.txt", size: 5, modifiedAt: "2026-06-25T00:00:00.000Z", created: true });
|
||||||
|
|
||||||
await expect(task.promise).resolves.toEqual([
|
await expect(task.promise).resolves.toEqual([
|
||||||
|
|||||||
@@ -1488,7 +1488,7 @@ export class PiWebApp extends LitElement {
|
|||||||
const existing = this.machinePluginLoadPromises.get(machine.id);
|
const existing = this.machinePluginLoadPromises.get(machine.id);
|
||||||
if (existing !== undefined) return existing;
|
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,
|
machineId: machine.id,
|
||||||
shouldLoadPlugin: (entry) => this.plugins.shouldLoadRemotePlugin(entry.id, entry.machineSpecific),
|
shouldLoadPlugin: (entry) => this.plugins.shouldLoadRemotePlugin(entry.id, entry.machineSpecific),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface LoadExternalPluginsOptions {
|
|||||||
shouldLoadPlugin?: (entry: PluginManifestEntry) => boolean;
|
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);
|
const manifest = await fetchPluginManifest(manifestUrl);
|
||||||
if (manifest === undefined) return [];
|
if (manifest === undefined) return [];
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function rewriteRemotePluginManifest(machineId: string, manifest: RemotePluginMa
|
|||||||
if (modulePath === undefined) return [];
|
if (modulePath === undefined) return [];
|
||||||
return [{
|
return [{
|
||||||
...plugin,
|
...plugin,
|
||||||
module: `/pi-web-plugins/${encodeURIComponent(machineScopedPluginId(machineId, plugin.id))}/${modulePath.path}${modulePath.query}`,
|
module: `${piWebBasePath()}/pi-web-plugins/${encodeURIComponent(machineScopedPluginId(machineId, plugin.id))}/${modulePath.path}${modulePath.query}`,
|
||||||
}];
|
}];
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -190,6 +190,11 @@ function sendGatewayError(reply: FastifyReply, machineId: string, error: unknown
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function piWebBasePath(): string {
|
||||||
|
const basePath = process.env["PI_WEB_BASE_PATH"] ?? "";
|
||||||
|
return basePath.replace(/\/$/u, "");
|
||||||
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ interface PiWebPluginServiceOptions {
|
|||||||
configProvider?: () => PiWebConfig;
|
configProvider?: () => PiWebConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function piWebBasePath(): string {
|
||||||
|
const basePath = process.env["PI_WEB_BASE_PATH"] ?? "";
|
||||||
|
return basePath.replace(/\/$/u, "");
|
||||||
|
}
|
||||||
|
|
||||||
interface LocalPluginRoot {
|
interface LocalPluginRoot {
|
||||||
path: string;
|
path: string;
|
||||||
source: string;
|
source: string;
|
||||||
@@ -135,7 +140,7 @@ export class PiWebPluginService {
|
|||||||
private pluginInfo(plugin: PluginRecord, config: PiWebConfig): PiWebPluginInfo {
|
private pluginInfo(plugin: PluginRecord, config: PiWebConfig): PiWebPluginInfo {
|
||||||
return {
|
return {
|
||||||
id: plugin.id,
|
id: plugin.id,
|
||||||
module: `/pi-web-plugins/${encodeURIComponent(plugin.id)}/${plugin.entryFile}?${pluginModuleQuery(plugin)}`,
|
module: `${piWebBasePath()}/pi-web-plugins/${encodeURIComponent(plugin.id)}/${plugin.entryFile}?${pluginModuleQuery(plugin)}`,
|
||||||
source: plugin.source,
|
source: plugin.source,
|
||||||
scope: plugin.scope,
|
scope: plugin.scope,
|
||||||
machineSpecific: plugin.machineSpecific,
|
machineSpecific: plugin.machineSpecific,
|
||||||
|
|||||||
Reference in New Issue
Block a user