@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.
Files changed (3) hide show
  1. package/extension.mjs +32 -74
  2. package/package.json +1 -1
  3. 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
- // Registers the /web command and starts the HTTP/SSE gateway.
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 { homedir } from "node:os";
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 serverProcess = null;
38
- let origin = `http://localhost:${PORT}`;
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
- // Check if server is already running (from a previous session or manual start)
55
- try {
56
- const res = await httpGet(`${origin}/api/tabs`);
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 res = await httpGet(`${origin}/api/tabs`);
77
- if (res.ok) return;
78
- } catch {}
79
- }
80
- throw new Error("pi-web-ui server did not start in time");
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 stopServer() {
84
- if (serverProcess && !serverProcess.killed) {
85
- serverProcess.kill();
86
- serverProcess = null;
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 stopServer();
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 stopServer();
72
+ await stopServerFn();
115
73
  ctx.ui?.notify?.("pi-web-ui stopped", "info");
116
74
  return;
117
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rotriz/pi-web-ui",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Web UI extension for the Pi coding agent — browser-based session management, git integration, and task tracking",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 → relative to pi entry
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 global node_modules as last resort
34
- const globalPrefix = process.env.NODE_PATH?.split(":")[0];
35
- if (globalPrefix) attempts.push(join(globalPrefix, "@earendil-works/pi-coding-agent/dist/index.js"));
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.listen(PORT, () => console.log(`[pi-web-ui] listening on http://localhost:${PORT}`));
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
+ }