@blackbelt-technology/pi-agent-dashboard 0.2.4 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-agent-dashboard",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Web dashboard for monitoring and interacting with pi agent sessions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-dashboard-extension",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Pi bridge extension for pi-dashboard",
5
5
  "type": "module",
6
6
  "pi": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-dashboard-server",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Dashboard server for monitoring and interacting with pi agent sessions",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -104,7 +104,91 @@ function resolvePiCommand(): string[] | null {
104
104
  return resolver.resolvePi();
105
105
  }
106
106
 
107
- function spawnHeadless(cwd: string, options?: SessionOptions): SpawnResult {
107
+ /** Windows-specific headless spawn with error detection and stderr capture.
108
+ * Waits briefly to detect immediate process death (e.g., missing deps, config errors).
109
+ */
110
+ async function spawnHeadlessWindows(
111
+ cwd: string,
112
+ piCmd: string[],
113
+ args: string[],
114
+ env: NodeJS.ProcessEnv,
115
+ ): Promise<SpawnResult> {
116
+ const [bin, ...prefixArgs] = piCmd;
117
+ const needsShell = bin.endsWith(".cmd");
118
+ const spawnBin = needsShell ? `"${bin}"` : bin;
119
+ const spawnArgs = needsShell
120
+ ? [...prefixArgs, ...args].map(a => `"${a}"`)
121
+ : [...prefixArgs, ...args];
122
+
123
+ const cmdForLog = `${bin} ${[...prefixArgs, ...args].join(" ")}`;
124
+ console.error(`[spawn] Windows headless: ${cmdForLog} (cwd=${cwd})`);
125
+
126
+ // Capture stderr for diagnostics (pi might log errors there)
127
+ const child = spawn(spawnBin, spawnArgs, {
128
+ cwd,
129
+ detached: false,
130
+ stdio: ["pipe", "ignore", "pipe"],
131
+ env,
132
+ shell: needsShell,
133
+ windowsHide: true,
134
+ });
135
+
136
+ // Collect stderr for early crash diagnostics
137
+ let stderrBuf = "";
138
+ child.stderr?.on("data", (chunk: Buffer) => {
139
+ stderrBuf += chunk.toString();
140
+ // Limit memory: keep only last 4 KB
141
+ if (stderrBuf.length > 4096) stderrBuf = stderrBuf.slice(-4096);
142
+ });
143
+
144
+ // Handle async spawn errors (e.g., ENOENT if binary disappears between check and exec)
145
+ let spawnError: string | null = null;
146
+ child.on("error", (err: Error) => {
147
+ spawnError = err.message;
148
+ console.error(`[spawn] Windows spawn error: ${err.message}`);
149
+ });
150
+
151
+ child.unref();
152
+ (child.stdin as any)?.unref();
153
+ child.stderr?.unref();
154
+
155
+ // Guard: if pid is undefined, spawn failed synchronously
156
+ if (!child.pid) {
157
+ // Wait briefly for the async error event
158
+ await new Promise(r => setTimeout(r, 200));
159
+ return {
160
+ success: false,
161
+ message: `Failed to spawn pi: ${spawnError || "unknown error (no PID)"}. Command: ${cmdForLog}`,
162
+ };
163
+ }
164
+
165
+ // Wait briefly to detect immediate crash (e.g., missing module, config error)
166
+ const exitCode = await Promise.race([
167
+ new Promise<number | null>(resolve => child.on("exit", resolve)),
168
+ new Promise<undefined>(resolve => setTimeout(() => resolve(undefined), 1500)),
169
+ ]);
170
+
171
+ if (exitCode !== undefined) {
172
+ const detail = stderrBuf.trim()
173
+ ? `\nstderr: ${stderrBuf.trim().split("\n").slice(-5).join("\n")}`
174
+ : "";
175
+ console.error(`[spawn] Pi exited immediately with code ${exitCode}${detail}`);
176
+ return {
177
+ success: false,
178
+ message: `Pi process exited immediately (code ${exitCode}).${detail}\nCommand: ${cmdForLog}`,
179
+ };
180
+ }
181
+
182
+ return {
183
+ success: true,
184
+ dashboardSpawned: true,
185
+ message: `Pi session spawned headless (pid ${child.pid})`,
186
+ pid: child.pid,
187
+ process: child,
188
+ };
189
+ }
190
+
191
+ async function spawnHeadless(cwd: string, options?: SessionOptions): Promise<SpawnResult> {
108
192
  try {
109
193
  const args = buildHeadlessArgs(options);
110
194
  const env = buildSpawnEnv();
@@ -119,31 +203,7 @@ function spawnHeadless(cwd: string, options?: SessionOptions): SpawnResult {
119
203
  }
120
204
 
121
205
  if (process.platform === "win32") {
122
- // Windows: spawn node.exe directly with pi's CLI entry point (no .cmd, no cmd window)
123
- const [bin, ...prefixArgs] = piCmd_;
124
- const needsShell = bin.endsWith(".cmd");
125
- const spawnBin = needsShell ? `"${bin}"` : bin;
126
- const spawnArgs = needsShell
127
- ? [...prefixArgs, ...args].map(a => `"${a}"`)
128
- : [...prefixArgs, ...args];
129
- const child = spawn(spawnBin, spawnArgs, {
130
- cwd,
131
- detached: false,
132
- stdio: ["pipe", "ignore", "ignore"],
133
- env,
134
- shell: needsShell,
135
- windowsHide: true,
136
- });
137
- child.unref();
138
- (child.stdin as any)?.unref();
139
-
140
- return {
141
- success: true,
142
- dashboardSpawned: true,
143
- message: `Pi session spawned headless (pid ${child.pid})`,
144
- pid: child.pid,
145
- process: child,
146
- };
206
+ return await spawnHeadlessWindows(cwd, piCmd_, args, env);
147
207
  }
148
208
 
149
209
  // Unix (macOS / Linux / WSL): wrap with "tail -f /dev/null | pi" so stdin
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-dashboard-shared",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Shared types and utilities for pi-dashboard",
5
5
  "type": "module",
6
6
  "exports": {
@@ -114,6 +114,32 @@ describe("shared bridge-register", () => {
114
114
  expect(packages).toContain(newPath);
115
115
  });
116
116
 
117
+ it("removes stale app bundle paths with spaces or mixed case (macOS PI Dashboard.app)", () => {
118
+ const stalePath = "/Applications/PI Dashboard.app/Contents/Resources/server/packages/extension";
119
+ writeSettings({ packages: [stalePath] });
120
+
121
+ const newPath = "/Applications/PI-Dashboard.app/Contents/Resources/server/packages/extension";
122
+ registerBridgeExtension(newPath);
123
+
124
+ const settings = readSettings();
125
+ const packages = settings.packages as string[];
126
+ expect(packages).not.toContain(stalePath);
127
+ expect(packages).toContain(newPath);
128
+ });
129
+
130
+ it("removes stale Windows-style dashboard paths with mixed case", () => {
131
+ const stalePath = "C:\\Program Files\\PI Dashboard\\resources\\server\\packages\\extension";
132
+ writeSettings({ packages: [stalePath] });
133
+
134
+ const newPath = "C:\\Program Files\\PI-Dashboard\\resources\\server\\packages\\extension";
135
+ registerBridgeExtension(newPath);
136
+
137
+ const settings = readSettings();
138
+ const packages = settings.packages as string[];
139
+ expect(packages).not.toContain(stalePath);
140
+ expect(packages).toContain(newPath);
141
+ });
142
+
117
143
  it("preserves non-dashboard paths", () => {
118
144
  writeSettings({ packages: ["/some/other/extension"] });
119
145
 
@@ -72,7 +72,9 @@ export function registerBridgeExtension(extensionPath: string): void {
72
72
  const isLocalPath = p.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(p);
73
73
  if (!isLocalPath) return true;
74
74
  // Only consider dashboard-related paths for cleanup
75
- if (!p.includes("pi-dashboard") && !p.includes("pi-agent-dashboard")) return true;
75
+ // Normalize: lowercase + collapse spaces/hyphens so "PI Dashboard" matches "pi-dashboard"
76
+ const normalized = p.toLowerCase().replace(/[\s_-]/g, "");
77
+ if (!normalized.includes("pidashboard") && !normalized.includes("piagentdashboard")) return true;
76
78
  // Keep paths that point to existing directories with a package.json
77
79
  try {
78
80
  return fs.existsSync(p) && fs.existsSync(path.join(p, "package.json"));