@rotriz/pi-web-ui 1.2.0 → 1.4.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 +47 -136
  2. package/package.json +1 -1
  3. package/server.mjs +66 -18
package/extension.mjs CHANGED
@@ -1,153 +1,64 @@
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
+ // Make @earendil-works/pi-coding-agent resolvable by the server module.
24
+ // Since we're in Pi's process, the module is already loaded — expose it globally.
25
+ try {
26
+ const piMod = await import("@earendil-works/pi-coding-agent");
27
+ globalThis.__PI_MODULE__ = piMod;
28
+ } catch {
29
+ // Fallback: resolve relative to Pi's entry script
30
+ const entry = process.argv[1] || "";
31
+ if (entry) {
32
+ const { dirname, join } = await import("node:path");
33
+ const dir = dirname(entry);
34
+ for (const c of [join(dir, "..", "dist", "index.js"), join(dir, "index.js")]) {
35
+ try { globalThis.__PI_MODULE__ = await import(c); break; } catch {}
36
+ }
37
+ }
38
+ }
39
+ const { startServer } = await import("./server.mjs");
40
+ await startServer(PORT);
41
+ serverStarted = true;
42
+ } catch (err) {
43
+ if (err?.code === "EADDRINUSE") {
44
+ serverStarted = true;
45
+ return;
46
+ }
47
+ throw err;
48
+ } finally {
49
+ startPromise = null;
50
+ }
51
+ })();
52
+ return startPromise;
144
53
  }
145
54
 
146
- async function stopServer() {
147
- if (serverProcess && !serverProcess.killed) {
148
- serverProcess.kill();
149
- serverProcess = null;
150
- }
55
+ async function stopServerFn() {
56
+ if (!serverStarted) return;
57
+ try {
58
+ const { stopServer } = await import("./server.mjs");
59
+ await stopServer();
60
+ serverStarted = false;
61
+ } catch {}
151
62
  }
152
63
 
153
64
  // ── Lifecycle ────────────────────────────────────────────────────────
@@ -159,7 +70,7 @@ export default function (pi) {
159
70
 
160
71
  pi.on("session_shutdown", async (event) => {
161
72
  if (event.reason === "reload" || event.reason === "quit") {
162
- await stopServer();
73
+ await stopServerFn();
163
74
  }
164
75
  });
165
76
 
@@ -173,7 +84,7 @@ export default function (pi) {
173
84
  handler: async (args, ctx) => {
174
85
  const arg = String(args ?? "").trim().toLowerCase();
175
86
  if (arg === "stop") {
176
- await stopServer();
87
+ await stopServerFn();
177
88
  ctx.ui?.notify?.("pi-web-ui stopped", "info");
178
89
  return;
179
90
  }
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.4.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,8 +19,17 @@ 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 → NODE_PATH → common global paths
22
+ // ─── Pi module resolution ─────────────────────────────────────────────────
23
+ let _piModule = null;
24
+
25
+ export function initPiModule(mod) {
26
+ _piModule = mod;
27
+ }
28
+
23
29
  async function resolvePiModule() {
30
+ if (_piModule) return _piModule;
31
+ // Check if the extension pre-loaded the module via globalThis
32
+ if (globalThis.__PI_MODULE__) { _piModule = globalThis.__PI_MODULE__; return _piModule; }
24
33
  const attempts = [];
25
34
  if (process.env.PI_WEB_PI_MODULE) attempts.push(process.env.PI_WEB_PI_MODULE);
26
35
  attempts.push("@earendil-works/pi-coding-agent");
@@ -30,35 +39,51 @@ async function resolvePiModule() {
30
39
  attempts.push(join(dir, "..", "dist", "index.js"));
31
40
  attempts.push(join(dir, "index.js"));
32
41
  }
33
- // Try NODE_PATH entries
34
42
  if (process.env.NODE_PATH) {
35
43
  for (const p of process.env.NODE_PATH.split(":")) {
36
44
  attempts.push(join(p, "@earendil-works", "pi-coding-agent", "dist", "index.js"));
37
45
  }
38
46
  }
39
- // Common global install locations
40
47
  const nodeDir = dirname(dirname(process.execPath));
41
48
  attempts.push(join(nodeDir, "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"));
42
- // npm global prefix
43
49
  attempts.push(join(homedir(), ".local", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "dist", "index.js"));
44
50
  for (const attempt of attempts) {
45
- try { return await import(attempt); } catch {}
51
+ try { _piModule = await import(attempt); return _piModule; } catch {}
46
52
  }
47
53
  throw new Error("Could not resolve @earendil-works/pi-coding-agent. Install it globally or set PI_WEB_PI_MODULE env var.");
48
54
  }
49
- const pi = await resolvePiModule();
50
55
 
51
- const {
52
- createAgentSessionRuntime,
53
- createAgentSessionFromServices,
54
- createAgentSessionServices,
55
- DefaultResourceLoader,
56
- SessionManager,
57
- SettingsManager,
58
- getAgentDir,
59
- } = pi;
56
+ // Only auto-initialize when run directly (node server.mjs)
57
+ const _isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
58
+ if (_isDirectRun) {
59
+ await resolvePiModule();
60
+ } else if (globalThis.__PI_MODULE__) {
61
+ _piModule = globalThis.__PI_MODULE__;
62
+ }
63
+
64
+ let createAgentSessionRuntime, createAgentSessionFromServices, createAgentSessionServices,
65
+ DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir;
66
+
67
+ if (_piModule) {
68
+ ({ createAgentSessionRuntime, createAgentSessionFromServices, createAgentSessionServices,
69
+ DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir } = _piModule);
70
+ }
71
+
72
+ let AGENT_DIR = null;
73
+
74
+ let server = null;
75
+ let _booted = false;
76
+
77
+ async function boot() {
78
+ if (_booted) return;
79
+ if (!_piModule) {
80
+ if (globalThis.__PI_MODULE__) _piModule = globalThis.__PI_MODULE__;
81
+ else await resolvePiModule();
82
+ ({ createAgentSessionRuntime, createAgentSessionFromServices, createAgentSessionServices,
83
+ DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir } = _piModule);
84
+ }
85
+ AGENT_DIR = getAgentDir();
60
86
 
61
- const AGENT_DIR = getAgentDir();
62
87
 
63
88
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
64
89
  const services = await createAgentSessionServices({ cwd });
@@ -868,7 +893,7 @@ function readBody(req) {
868
893
  });
869
894
  }
870
895
 
871
- const server = createServer(async (req, res) => {
896
+ server = createServer(async (req, res) => {
872
897
  const url = new URL(req.url, `http://localhost:${PORT}`);
873
898
  const path = url.pathname;
874
899
 
@@ -1403,4 +1428,27 @@ const server = createServer(async (req, res) => {
1403
1428
  }
1404
1429
  });
1405
1430
 
1406
- server.listen(PORT, () => console.log(`[pi-web-ui] listening on http://localhost:${PORT}`));
1431
+ _booted = true;
1432
+ }
1433
+
1434
+ // When run directly (node server.mjs), boot and listen immediately.
1435
+ const _isDirect = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
1436
+ if (_isDirect) {
1437
+ await boot();
1438
+ server.listen(PORT, () => console.log(`[pi-web-ui] listening on http://localhost:${PORT}`));
1439
+ }
1440
+
1441
+ export { PORT };
1442
+ export async function startServer(port) {
1443
+ await boot();
1444
+ return new Promise((res, rej) => {
1445
+ server.listen(port || PORT, () => {
1446
+ console.log(`[pi-web-ui] listening on http://localhost:${port || PORT}`);
1447
+ res();
1448
+ });
1449
+ server.on("error", rej);
1450
+ });
1451
+ }
1452
+ export async function stopServer() {
1453
+ if (server) return new Promise((res) => server.close(() => res()));
1454
+ }