@rotriz/pi-web-ui 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extension.mjs +32 -74
- package/package.json +1 -1
- package/server.mjs +33 -6
package/extension.mjs
CHANGED
|
@@ -1,95 +1,53 @@
|
|
|
1
1
|
// pi-web-ui extension entry point for the Pi coding agent.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// When loaded as a Pi extension (via pi.extensions in package.json),
|
|
5
|
-
// this module receives the Pi ExtensionAPI and bridges the host session
|
|
6
|
-
// into the Web UI server.
|
|
2
|
+
// Runs the HTTP/SSE gateway in-process (no subprocess needed).
|
|
7
3
|
|
|
8
|
-
import {
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
10
|
-
import { exec, execFile } from "node:child_process";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { existsSync } from "node:fs";
|
|
13
|
-
import { request } from "node:http";
|
|
4
|
+
import { exec } from "node:child_process";
|
|
14
5
|
|
|
15
6
|
const PORT = Number(process.env.PI_WEB_PORT || process.env.PORT || 3123);
|
|
16
7
|
|
|
17
|
-
// HTTP GET helper — works on Node 14+ without global fetch
|
|
18
|
-
function httpGet(url) {
|
|
19
|
-
return new Promise((resolve) => {
|
|
20
|
-
const req = request(url, (res) => {
|
|
21
|
-
let data = "";
|
|
22
|
-
res.on("data", (chunk) => (data += chunk));
|
|
23
|
-
res.on("end", () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data }));
|
|
24
|
-
});
|
|
25
|
-
req.on("error", () => resolve({ ok: false, status: 0, data: "" }));
|
|
26
|
-
req.setTimeout(3000, () => { req.destroy(); resolve({ ok: false, status: 0, data: "" }); });
|
|
27
|
-
req.end();
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
8
|
function openBrowser(url) {
|
|
32
9
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
33
10
|
exec(`${cmd} ${JSON.stringify(url)}`, () => {});
|
|
34
11
|
}
|
|
35
12
|
|
|
36
13
|
export default function (pi) {
|
|
37
|
-
let
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
const extDir = (() => {
|
|
41
|
-
try {
|
|
42
|
-
return dirname(fileURLToPath(import.meta.url));
|
|
43
|
-
} catch {
|
|
44
|
-
return join(homedir(), ".pi", "agent", "extensions", "pi-web-ui");
|
|
45
|
-
}
|
|
46
|
-
})();
|
|
47
|
-
|
|
48
|
-
function findNode() {
|
|
49
|
-
// Use the same Node binary that's running pi
|
|
50
|
-
return process.execPath;
|
|
51
|
-
}
|
|
14
|
+
let serverStarted = false;
|
|
15
|
+
let startPromise = null;
|
|
16
|
+
const origin = `http://localhost:${PORT}`;
|
|
52
17
|
|
|
53
18
|
async function ensureServer() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (res.ok) return; // Already running, reuse it
|
|
58
|
-
} catch {}
|
|
59
|
-
|
|
60
|
-
if (serverProcess && !serverProcess.killed) return;
|
|
61
|
-
const serverPath = join(extDir, "server.mjs");
|
|
62
|
-
if (!existsSync(serverPath)) {
|
|
63
|
-
throw new Error(`pi-web-ui server not found at ${serverPath}`);
|
|
64
|
-
}
|
|
65
|
-
serverProcess = execFile(findNode(), [serverPath], {
|
|
66
|
-
cwd: process.cwd(),
|
|
67
|
-
env: { ...process.env, PORT: String(PORT) },
|
|
68
|
-
stdio: "ignore",
|
|
69
|
-
});
|
|
70
|
-
serverProcess.unref();
|
|
71
|
-
serverProcess.on("exit", () => { serverProcess = null; });
|
|
72
|
-
// Wait for the server to be ready (up to 30 seconds for slow machines)
|
|
73
|
-
for (let i = 0; i < 60; i++) {
|
|
74
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
19
|
+
if (serverStarted) return;
|
|
20
|
+
if (startPromise) return startPromise;
|
|
21
|
+
startPromise = (async () => {
|
|
75
22
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
23
|
+
const { startServer } = await import("./server.mjs");
|
|
24
|
+
await startServer(PORT);
|
|
25
|
+
serverStarted = true;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// Server may already be listening (port in use = already running)
|
|
28
|
+
if (err?.code === "EADDRINUSE") {
|
|
29
|
+
serverStarted = true;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
throw err;
|
|
33
|
+
} finally {
|
|
34
|
+
startPromise = null;
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
return startPromise;
|
|
81
38
|
}
|
|
82
39
|
|
|
83
|
-
async function
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
40
|
+
async function stopServerFn() {
|
|
41
|
+
if (!serverStarted) return;
|
|
42
|
+
try {
|
|
43
|
+
const { stopServer } = await import("./server.mjs");
|
|
44
|
+
await stopServer();
|
|
45
|
+
serverStarted = false;
|
|
46
|
+
} catch {}
|
|
88
47
|
}
|
|
89
48
|
|
|
90
49
|
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
91
50
|
pi.on("session_start", async (_event, ctx) => {
|
|
92
|
-
// Start server in background — don't block Pi's session startup
|
|
93
51
|
ensureServer().catch((err) => {
|
|
94
52
|
ctx.ui?.notify?.(`pi-web-ui: ${err?.message ?? err}`, "warning");
|
|
95
53
|
});
|
|
@@ -97,7 +55,7 @@ export default function (pi) {
|
|
|
97
55
|
|
|
98
56
|
pi.on("session_shutdown", async (event) => {
|
|
99
57
|
if (event.reason === "reload" || event.reason === "quit") {
|
|
100
|
-
await
|
|
58
|
+
await stopServerFn();
|
|
101
59
|
}
|
|
102
60
|
});
|
|
103
61
|
|
|
@@ -111,7 +69,7 @@ export default function (pi) {
|
|
|
111
69
|
handler: async (args, ctx) => {
|
|
112
70
|
const arg = String(args ?? "").trim().toLowerCase();
|
|
113
71
|
if (arg === "stop") {
|
|
114
|
-
await
|
|
72
|
+
await stopServerFn();
|
|
115
73
|
ctx.ui?.notify?.("pi-web-ui stopped", "info");
|
|
116
74
|
return;
|
|
117
75
|
}
|
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -19,7 +19,7 @@ const PORT = process.env.PORT || 3123;
|
|
|
19
19
|
const CWD = process.cwd();
|
|
20
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
21
|
|
|
22
|
-
// Resolve the pi-coding-agent module: env override → bare specifier →
|
|
22
|
+
// Resolve the pi-coding-agent module: env override → bare specifier → NODE_PATH → common global paths
|
|
23
23
|
async function resolvePiModule() {
|
|
24
24
|
const attempts = [];
|
|
25
25
|
if (process.env.PI_WEB_PI_MODULE) attempts.push(process.env.PI_WEB_PI_MODULE);
|
|
@@ -30,13 +30,21 @@ async function resolvePiModule() {
|
|
|
30
30
|
attempts.push(join(dir, "..", "dist", "index.js"));
|
|
31
31
|
attempts.push(join(dir, "index.js"));
|
|
32
32
|
}
|
|
33
|
-
// Try
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// Try NODE_PATH entries
|
|
34
|
+
if (process.env.NODE_PATH) {
|
|
35
|
+
for (const p of process.env.NODE_PATH.split(":")) {
|
|
36
|
+
attempts.push(join(p, "@earendil-works", "pi-coding-agent", "dist", "index.js"));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Common global install locations
|
|
40
|
+
const nodeDir = dirname(dirname(process.execPath));
|
|
41
|
+
attempts.push(join(nodeDir, "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"));
|
|
42
|
+
// npm global prefix
|
|
43
|
+
attempts.push(join(homedir(), ".local", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"));
|
|
36
44
|
for (const attempt of attempts) {
|
|
37
45
|
try { return await import(attempt); } catch {}
|
|
38
46
|
}
|
|
39
|
-
throw new Error("Could not resolve @earendil-works/pi-coding-agent. Install it globally or set PI_WEB_PI_MODULE.");
|
|
47
|
+
throw new Error("Could not resolve @earendil-works/pi-coding-agent. Install it globally or set PI_WEB_PI_MODULE env var.");
|
|
40
48
|
}
|
|
41
49
|
const pi = await resolvePiModule();
|
|
42
50
|
|
|
@@ -1394,5 +1402,24 @@ const server = createServer(async (req, res) => {
|
|
|
1394
1402
|
json(res, 500, { error: String(err?.stack ?? err) });
|
|
1395
1403
|
}
|
|
1396
1404
|
});
|
|
1405
|
+
// When run directly (node server.mjs), start listening immediately.
|
|
1406
|
+
// When imported as a module (by extension.mjs), export control functions.
|
|
1407
|
+
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
1408
|
+
|
|
1409
|
+
if (isDirectRun) {
|
|
1410
|
+
server.listen(PORT, () => console.log(`[pi-web-ui] listening on http://localhost:${PORT}`));
|
|
1411
|
+
}
|
|
1397
1412
|
|
|
1398
|
-
server
|
|
1413
|
+
export { server, PORT, shared };
|
|
1414
|
+
export function startServer(port) {
|
|
1415
|
+
return new Promise((res, rej) => {
|
|
1416
|
+
server.listen(port || PORT, () => {
|
|
1417
|
+
console.log(`[pi-web-ui] listening on http://localhost:${port || PORT}`);
|
|
1418
|
+
res();
|
|
1419
|
+
});
|
|
1420
|
+
server.on("error", rej);
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
export function stopServer() {
|
|
1424
|
+
return new Promise((res) => server.close(() => res()));
|
|
1425
|
+
}
|