@rotriz/pi-web-ui 1.2.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 -136
- package/package.json +1 -1
- package/server.mjs +20 -1
package/extension.mjs
CHANGED
|
@@ -1,153 +1,49 @@
|
|
|
1
1
|
// pi-web-ui extension entry point for the Pi coding agent.
|
|
2
|
-
//
|
|
2
|
+
// Runs the HTTP/SSE gateway in-process (no subprocess needed).
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { exec, execFile } from "node:child_process";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
|
-
import { request } from "node:http";
|
|
4
|
+
import { exec } from "node:child_process";
|
|
10
5
|
|
|
11
6
|
const PORT = Number(process.env.PI_WEB_PORT || process.env.PORT || 3123);
|
|
12
7
|
|
|
13
|
-
// HTTP GET helper — works on Node 14+ without global fetch
|
|
14
|
-
function httpGet(url) {
|
|
15
|
-
return new Promise((resolve) => {
|
|
16
|
-
const req = request(url, (res) => {
|
|
17
|
-
let data = "";
|
|
18
|
-
res.on("data", (chunk) => (data += chunk));
|
|
19
|
-
res.on("end", () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data }));
|
|
20
|
-
});
|
|
21
|
-
req.on("error", () => resolve({ ok: false, status: 0, data: "" }));
|
|
22
|
-
req.setTimeout(3000, () => { req.destroy(); resolve({ ok: false, status: 0, data: "" }); });
|
|
23
|
-
req.end();
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
8
|
function openBrowser(url) {
|
|
28
9
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
29
10
|
exec(`${cmd} ${JSON.stringify(url)}`, () => {});
|
|
30
11
|
}
|
|
31
12
|
|
|
32
|
-
// Resolve the pi-coding-agent module path from within Pi's process context
|
|
33
|
-
function findPiModulePath() {
|
|
34
|
-
try {
|
|
35
|
-
// When running inside Pi, the module is already loaded — find it via require.resolve or import.meta
|
|
36
|
-
const candidates = [];
|
|
37
|
-
// Check if pi's entry script reveals the path
|
|
38
|
-
const entry = process.argv[1] || "";
|
|
39
|
-
if (entry) {
|
|
40
|
-
const dir = dirname(entry);
|
|
41
|
-
candidates.push(join(dir, "..", "dist", "index.js"));
|
|
42
|
-
candidates.push(join(dir, "index.js"));
|
|
43
|
-
// Go up to find the package
|
|
44
|
-
let d = dir;
|
|
45
|
-
for (let i = 0; i < 5; i++) {
|
|
46
|
-
const candidate = join(d, "dist", "index.js");
|
|
47
|
-
if (existsSync(candidate)) candidates.push(candidate);
|
|
48
|
-
const pkgCandidate = join(d, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js");
|
|
49
|
-
if (existsSync(pkgCandidate)) candidates.push(pkgCandidate);
|
|
50
|
-
d = dirname(d);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// Check common global locations
|
|
54
|
-
const globalDirs = [
|
|
55
|
-
join(homedir(), ".local", "share", "fnm"),
|
|
56
|
-
"/usr/local/lib/node_modules",
|
|
57
|
-
"/usr/lib/node_modules",
|
|
58
|
-
join(homedir(), ".nvm", "versions", "node"),
|
|
59
|
-
join(homedir(), ".volta", "tools", "image", "packages"),
|
|
60
|
-
];
|
|
61
|
-
for (const base of globalDirs) {
|
|
62
|
-
if (!existsSync(base)) continue;
|
|
63
|
-
// Recursively look for the package
|
|
64
|
-
const piPkg = "@earendil-works/pi-coding-agent/dist/index.js";
|
|
65
|
-
// Check direct path
|
|
66
|
-
const direct = join(base, piPkg);
|
|
67
|
-
if (existsSync(direct)) { candidates.push(direct); continue; }
|
|
68
|
-
// For version managers, search one level deep
|
|
69
|
-
}
|
|
70
|
-
// Also check NODE_PATH
|
|
71
|
-
if (process.env.NODE_PATH) {
|
|
72
|
-
for (const p of process.env.NODE_PATH.split(":")) {
|
|
73
|
-
const candidate = join(p, "@earendil-works", "pi-coding-agent", "dist", "index.js");
|
|
74
|
-
if (existsSync(candidate)) candidates.push(candidate);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
for (const c of candidates) {
|
|
78
|
-
if (existsSync(c)) return c;
|
|
79
|
-
}
|
|
80
|
-
} catch {}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
13
|
export default function (pi) {
|
|
85
|
-
let
|
|
86
|
-
let
|
|
87
|
-
|
|
88
|
-
const extDir = (() => {
|
|
89
|
-
try {
|
|
90
|
-
return dirname(fileURLToPath(import.meta.url));
|
|
91
|
-
} catch {
|
|
92
|
-
return join(homedir(), ".pi", "agent", "extensions", "pi-web-ui");
|
|
93
|
-
}
|
|
94
|
-
})();
|
|
95
|
-
|
|
96
|
-
function findNode() {
|
|
97
|
-
return process.execPath;
|
|
98
|
-
}
|
|
14
|
+
let serverStarted = false;
|
|
15
|
+
let startPromise = null;
|
|
16
|
+
const origin = `http://localhost:${PORT}`;
|
|
99
17
|
|
|
100
18
|
async function ensureServer() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (res.ok) return;
|
|
105
|
-
} catch {}
|
|
106
|
-
|
|
107
|
-
if (serverProcess && !serverProcess.killed) return;
|
|
108
|
-
const serverPath = join(extDir, "server.mjs");
|
|
109
|
-
if (!existsSync(serverPath)) {
|
|
110
|
-
throw new Error(`pi-web-ui server not found at ${serverPath}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Resolve the pi module path and pass it to the server
|
|
114
|
-
const piModulePath = findPiModulePath();
|
|
115
|
-
const env = { ...process.env, PORT: String(PORT) };
|
|
116
|
-
if (piModulePath) env.PI_WEB_PI_MODULE = piModulePath;
|
|
117
|
-
|
|
118
|
-
// Also pass NODE_PATH so the subprocess can find global modules
|
|
119
|
-
const nodePath = [];
|
|
120
|
-
if (process.env.NODE_PATH) nodePath.push(process.env.NODE_PATH);
|
|
121
|
-
// Add the global node_modules from the current node binary
|
|
122
|
-
const nodeDir = dirname(dirname(process.execPath));
|
|
123
|
-
const globalNM = join(nodeDir, "lib", "node_modules");
|
|
124
|
-
if (existsSync(globalNM)) nodePath.push(globalNM);
|
|
125
|
-
if (nodePath.length) env.NODE_PATH = nodePath.join(":");
|
|
126
|
-
|
|
127
|
-
serverProcess = execFile(findNode(), [serverPath], {
|
|
128
|
-
cwd: process.cwd(),
|
|
129
|
-
env,
|
|
130
|
-
stdio: "ignore",
|
|
131
|
-
});
|
|
132
|
-
serverProcess.unref();
|
|
133
|
-
serverProcess.on("exit", () => { serverProcess = null; });
|
|
134
|
-
|
|
135
|
-
// Wait for the server to be ready (up to 30 seconds)
|
|
136
|
-
for (let i = 0; i < 60; i++) {
|
|
137
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
19
|
+
if (serverStarted) return;
|
|
20
|
+
if (startPromise) return startPromise;
|
|
21
|
+
startPromise = (async () => {
|
|
138
22
|
try {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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;
|
|
144
38
|
}
|
|
145
39
|
|
|
146
|
-
async function
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 {}
|
|
151
47
|
}
|
|
152
48
|
|
|
153
49
|
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
@@ -159,7 +55,7 @@ export default function (pi) {
|
|
|
159
55
|
|
|
160
56
|
pi.on("session_shutdown", async (event) => {
|
|
161
57
|
if (event.reason === "reload" || event.reason === "quit") {
|
|
162
|
-
await
|
|
58
|
+
await stopServerFn();
|
|
163
59
|
}
|
|
164
60
|
});
|
|
165
61
|
|
|
@@ -173,7 +69,7 @@ export default function (pi) {
|
|
|
173
69
|
handler: async (args, ctx) => {
|
|
174
70
|
const arg = String(args ?? "").trim().toLowerCase();
|
|
175
71
|
if (arg === "stop") {
|
|
176
|
-
await
|
|
72
|
+
await stopServerFn();
|
|
177
73
|
ctx.ui?.notify?.("pi-web-ui stopped", "info");
|
|
178
74
|
return;
|
|
179
75
|
}
|
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -1402,5 +1402,24 @@ const server = createServer(async (req, res) => {
|
|
|
1402
1402
|
json(res, 500, { error: String(err?.stack ?? err) });
|
|
1403
1403
|
}
|
|
1404
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));
|
|
1405
1408
|
|
|
1406
|
-
|
|
1409
|
+
if (isDirectRun) {
|
|
1410
|
+
server.listen(PORT, () => console.log(`[pi-web-ui] listening on http://localhost:${PORT}`));
|
|
1411
|
+
}
|
|
1412
|
+
|
|
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
|
+
}
|