@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 +1 -1
- package/packages/extension/package.json +1 -1
- package/packages/server/package.json +1 -1
- package/packages/server/src/process-manager.ts +86 -26
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bridge-register.test.ts +26 -0
- package/packages/shared/src/bridge-register.ts +3 -1
package/package.json
CHANGED
|
@@ -104,7 +104,91 @@ function resolvePiCommand(): string[] | null {
|
|
|
104
104
|
return resolver.resolvePi();
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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"));
|