diff --git a/.gitignore b/.gitignore index c74fccc..052ea5e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,3 @@ dist/ # Local runtime attachment uploads (created by the chat composer "save to folder" mode). .pi-web/ - -# Local paste upload directory and temporary working docs. -.pi-paste/ -docs/tmp/ diff --git a/docs/plugins.md b/docs/plugins.md index 6706641..387ef6c 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -976,13 +976,13 @@ If you are an AI agent building or editing a PI WEB plugin, follow this checklis Check discovery: ```bash -curl http://localhost:8504/pi-web-plugins/manifest.json +curl http://127.0.0.1:8504/pi-web-plugins/manifest.json ``` Check a plugin module: ```bash -curl http://localhost:8504/pi-web-plugins/my-plugin/pi-web-plugin.js +curl http://127.0.0.1:8504/pi-web-plugins/my-plugin/pi-web-plugin.js ``` Common issues: diff --git a/package.json b/package.json index 2783bc6..11e045c 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "prepublishOnly": "npm run verify", "publish:npm": "npm publish --access public", "prepare": "node scripts/install-git-hooks.mjs", - "postinstall": "node scripts/postinstall.mjs", "changeset": "changeset", "release:version": "changeset version", "changelog:status": "changeset status" diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs deleted file mode 100644 index 66b023e..0000000 --- a/scripts/postinstall.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { chmodSync, readdirSync, statSync } from "node:fs"; -import { join } from "node:path"; - -/** - * node-pty 1.1.0 ships macOS prebuilds with `spawn-helper` files at 644 - * instead of 755, causing `posix_spawnp failed` at runtime. This script - * fixes permissions after install on Darwin platforms. - */ -function fixNodePtyPermissions() { - if (process.platform === "win32") return; - const prebuildsDir = join("node_modules", "node-pty", "prebuilds"); - let entries; - try { - entries = readdirSync(prebuildsDir, { withFileTypes: true }); - } catch { - return; - } - for (const entry of entries) { - if (!entry.isDirectory()) continue; - const helper = join(prebuildsDir, entry.name, "spawn-helper"); - let stats; - try { - stats = statSync(helper); - } catch { - continue; - } - if (!stats.isFile()) continue; - // 0o100 = regular file, 0o111 = owner/group/other execute - if ((stats.mode & 0o111) === 0) { - chmodSync(helper, 0o755); - } - } -} - -fixNodePtyPermissions(); diff --git a/src/client/src/api/federatedRouteContract.test.ts b/src/client/src/api/federatedRouteContract.test.ts index 059212e..faafbaf 100644 --- a/src/client/src/api/federatedRouteContract.test.ts +++ b/src/client/src/api/federatedRouteContract.test.ts @@ -37,6 +37,9 @@ describe("federated route contract", () => { ignoreParseFailure(workspacesApi.deleteWorkspace("p 1", "w 1", machineId)), ignoreParseFailure(workspacesApi.workspaceTree("p 1", "w 1", "src", machineId)), ignoreParseFailure(workspacesApi.workspaceFile("p 1", "w 1", "README.md", machineId)), + ignoreParseFailure(workspacesApi.writeWorkspaceFile("p 1", "w 1", "README.md", "hello", { overwrite: false }, machineId)), + ignoreParseFailure(workspacesApi.deleteWorkspaceFile("p 1", "w 1", "README.md", machineId)), + ignoreParseFailure(workspacesApi.moveWorkspaceFile("p 1", "w 1", "README.md", "docs/README.md", { overwrite: false }, machineId)), ignoreParseFailure(filesApi.files("/repo", "README", { kind: "tracked", mode: "file", machineId })), ignoreParseFailure(gitApi.gitStatus("p 1", "w 1", machineId)), ignoreParseFailure(gitApi.gitDiff("p 1", "w 1", { path: "README.md", staged: true }, machineId)), diff --git a/src/server/piWebPluginService.test.ts b/src/server/piWebPluginService.test.ts index 1e11c73..7b4ec66 100644 --- a/src/server/piWebPluginService.test.ts +++ b/src/server/piWebPluginService.test.ts @@ -22,7 +22,7 @@ describe("PiWebPluginService", () => { files: { "pi-web-plugin.js": "export default { apiVersion: 1, name: 'Info', activate: () => ({ contributions: {} }) };" }, }); - const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false, configProvider: () => ({ plugins: {} }) }); + const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); await expect(service.manifest()).resolves.toEqual({ plugins: [expect.objectContaining({ id: "info", source: "test", scope: "local", machineSpecific: false })], @@ -41,7 +41,7 @@ describe("PiWebPluginService", () => { files: { "pi-web-plugin.js": "export default {};" }, }); - const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false, configProvider: () => ({ plugins: {} }) }); + const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); await expect(service.manifest()).resolves.toMatchObject({ plugins: [{ id: "updates", machineSpecific: true }] }); await expect(service.plugins()).resolves.toMatchObject({ plugins: [{ id: "updates", machineSpecific: true, enabled: true }] }); @@ -74,7 +74,7 @@ describe("PiWebPluginService", () => { files: { "dist/pi-web-plugin.js": "export default { apiVersion: 1, name: 'Source Dev', activate: () => ({ contributions: {} }) };" }, }); - const service = new PiWebPluginService({ cwd: tempDir, packageProvider: false, configProvider: () => ({ plugins: {} }) }); + const service = new PiWebPluginService({ cwd: tempDir, packageProvider: false }); const manifest = await service.manifest(); expect(manifest.plugins).toEqual(expect.arrayContaining([ @@ -92,7 +92,7 @@ describe("PiWebPluginService", () => { await mkdir(join(tempDir, "plugins"), { recursive: true }); await symlink(pluginDir, join(tempDir, "plugins", "dev"), process.platform === "win32" ? "junction" : "dir"); - const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false, configProvider: () => ({ plugins: {} }) }); + const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); const manifest = await service.manifest(); expect(manifest.plugins).toHaveLength(1); @@ -135,7 +135,7 @@ describe("PiWebPluginService", () => { files: { "pi-web-plugin.js": "export default {};" }, }); - const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false, configProvider: () => ({ plugins: {} }) }); + const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); const manifest = await service.manifest(); expect(manifest.plugins.map((plugin) => plugin.id)).toEqual(["duplicate"]); @@ -167,7 +167,7 @@ describe("PiWebPluginService", () => { files: { "pi-web-plugin.js": "export default {};" }, }); - const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false, configProvider: () => ({ plugins: {} }) }); + const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); const manifest = await service.manifest(); expect(manifest.plugins.map((plugin) => plugin.id)).toEqual(["valid"]); @@ -181,7 +181,7 @@ describe("PiWebPluginService", () => { }); await writeFile(join(tempDir, "plugins", "escape.js"), "nope"); - const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false, configProvider: () => ({ plugins: {} }) }); + const service = new PiWebPluginService({ roots: [{ path: join(tempDir, "plugins"), source: "test", scope: "local" }], packageProvider: false }); const manifest = await service.manifest(); expect(manifest.plugins).toHaveLength(1); diff --git a/src/shared/federatedRoutes.ts b/src/shared/federatedRoutes.ts index 3493d50..eab03cf 100644 --- a/src/shared/federatedRoutes.ts +++ b/src/shared/federatedRoutes.ts @@ -1,4 +1,4 @@ -export type FederatedHttpMethod = "GET" | "POST" | "DELETE"; +export type FederatedHttpMethod = "GET" | "POST" | "PUT" | "DELETE"; export interface FederatedHttpRouteSpec { method: FederatedHttpMethod; @@ -15,6 +15,9 @@ export const FEDERATED_HTTP_ROUTES = [ { method: "DELETE", path: "/projects/:projectId/workspaces/:workspaceId" }, { method: "GET", path: "/projects/:projectId/workspaces/:workspaceId/tree" }, { method: "GET", path: "/projects/:projectId/workspaces/:workspaceId/file" }, + { method: "PUT", path: "/projects/:projectId/workspaces/:workspaceId/file" }, + { method: "DELETE", path: "/projects/:projectId/workspaces/:workspaceId/file" }, + { method: "POST", path: "/projects/:projectId/workspaces/:workspaceId/file/move" }, { method: "GET", path: "/projects/:projectId/workspaces/:workspaceId/file/preview" }, { method: "GET", path: "/projects/:projectId/workspaces/:workspaceId/git/status" }, { method: "GET", path: "/projects/:projectId/workspaces/:workspaceId/git/diff" },