@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.
Files changed (3) hide show
  1. package/extension.mjs +32 -136
  2. package/package.json +1 -1
  3. 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
- // Registers the /web command and starts the HTTP/SSE gateway.
2
+ // Runs the HTTP/SSE gateway in-process (no subprocess needed).
3
3
 
4
- import { homedir } from "node:os";
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 serverProcess = null;
86
- let origin = `http://localhost:${PORT}`;
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
- // Check if server is already running (from a previous session or manual start)
102
- try {
103
- const res = await httpGet(`${origin}/api/tabs`);
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 res = await httpGet(`${origin}/api/tabs`);
140
- if (res.ok) return;
141
- } catch {}
142
- }
143
- 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;
144
38
  }
145
39
 
146
- async function stopServer() {
147
- if (serverProcess && !serverProcess.killed) {
148
- serverProcess.kill();
149
- serverProcess = null;
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 stopServer();
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 stopServer();
72
+ await stopServerFn();
177
73
  ctx.ui?.notify?.("pi-web-ui stopped", "info");
178
74
  return;
179
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rotriz/pi-web-ui",
3
- "version": "1.2.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
@@ -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
- server.listen(PORT, () => console.log(`[pi-web-ui] listening on http://localhost:${PORT}`));
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
+ }