pi-webdesk 0.1.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 (64) hide show
  1. package/README.md +111 -0
  2. package/dist/apps/daemon/src/appearance-preferences.js +218 -0
  3. package/dist/apps/daemon/src/auth.js +88 -0
  4. package/dist/apps/daemon/src/bin.js +123 -0
  5. package/dist/apps/daemon/src/cli.js +48 -0
  6. package/dist/apps/daemon/src/event-hub.js +155 -0
  7. package/dist/apps/daemon/src/index.js +102 -0
  8. package/dist/apps/daemon/src/launcher-control.js +114 -0
  9. package/dist/apps/daemon/src/launcher.js +73 -0
  10. package/dist/apps/daemon/src/pi-auth.js +290 -0
  11. package/dist/apps/daemon/src/pi-resources.js +182 -0
  12. package/dist/apps/daemon/src/pi-runtime-factory.js +19 -0
  13. package/dist/apps/daemon/src/pi-sessions.js +265 -0
  14. package/dist/apps/daemon/src/runtime-process.js +241 -0
  15. package/dist/apps/daemon/src/secret.js +71 -0
  16. package/dist/apps/daemon/src/server.js +1662 -0
  17. package/dist/apps/daemon/src/session-projection.js +117 -0
  18. package/dist/apps/daemon/src/state-lock.js +31 -0
  19. package/dist/apps/daemon/src/static-web.js +53 -0
  20. package/dist/apps/daemon/src/task-archive.js +152 -0
  21. package/dist/apps/daemon/src/task-commit.js +503 -0
  22. package/dist/apps/daemon/src/task-merge.js +912 -0
  23. package/dist/apps/daemon/src/task-review.js +204 -0
  24. package/dist/apps/daemon/src/task-runtime.js +1124 -0
  25. package/dist/apps/daemon/src/task-validation.js +352 -0
  26. package/dist/apps/daemon/src/workspace-store.js +140 -0
  27. package/dist/apps/daemon/src/workspace.js +795 -0
  28. package/dist/extensions/webdesk.js +34 -0
  29. package/dist/packages/git/src/commit.js +675 -0
  30. package/dist/packages/git/src/errors.js +55 -0
  31. package/dist/packages/git/src/fingerprint.js +286 -0
  32. package/dist/packages/git/src/index.js +123 -0
  33. package/dist/packages/git/src/merge.js +1008 -0
  34. package/dist/packages/git/src/paths.js +58 -0
  35. package/dist/packages/git/src/repository.js +77 -0
  36. package/dist/packages/git/src/review.js +396 -0
  37. package/dist/packages/git/src/runner.js +110 -0
  38. package/dist/packages/git/src/validation.js +263 -0
  39. package/dist/packages/git/src/worktree.js +233 -0
  40. package/dist/packages/pi-bridge/extensions/pita-policy.js +117 -0
  41. package/dist/packages/pi-bridge/src/auth.js +80 -0
  42. package/dist/packages/pi-bridge/src/errors.js +19 -0
  43. package/dist/packages/pi-bridge/src/handshake.js +43 -0
  44. package/dist/packages/pi-bridge/src/index.js +76 -0
  45. package/dist/packages/pi-bridge/src/jsonl.js +105 -0
  46. package/dist/packages/pi-bridge/src/policy-approval.js +62 -0
  47. package/dist/packages/pi-bridge/src/resolve.js +59 -0
  48. package/dist/packages/pi-bridge/src/resources-child.mjs +23 -0
  49. package/dist/packages/pi-bridge/src/resources.js +481 -0
  50. package/dist/packages/pi-bridge/src/rpc/client.js +480 -0
  51. package/dist/packages/pi-bridge/src/rpc/runtime.js +496 -0
  52. package/dist/packages/pi-bridge/src/rpc/supervisor.mjs +129 -0
  53. package/dist/packages/pi-bridge/src/rpc/tool-events.js +78 -0
  54. package/dist/packages/pi-bridge/src/rpc/wire.js +263 -0
  55. package/dist/packages/pi-bridge/src/runtime.js +0 -0
  56. package/dist/packages/pi-bridge/src/sessions-child.mjs +38 -0
  57. package/dist/packages/pi-bridge/src/sessions.js +314 -0
  58. package/dist/packages/pi-bridge/src/tool-activity.js +56 -0
  59. package/dist/packages/protocol/src/index.js +1863 -0
  60. package/dist/web/assets/index-BOw_fhvO.css +2 -0
  61. package/dist/web/assets/index-oXs7yAAo.js +119 -0
  62. package/dist/web/index.html +14 -0
  63. package/package.json +69 -0
  64. package/scripts/prepare.mjs +7 -0
@@ -0,0 +1,481 @@
1
+ // packages/pi-bridge/src/resources.ts
2
+ import { spawn } from "node:child_process";
3
+ import { readFile, realpath, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ DefaultPackageManager,
8
+ ProjectTrustStore,
9
+ SettingsManager,
10
+ VERSION,
11
+ getAgentDir,
12
+ hasTrustRequiringProjectResources,
13
+ parseFrontmatter
14
+ } from "@earendil-works/pi-coding-agent";
15
+ import { z } from "zod";
16
+ import { PiBridgeError } from "./errors.js";
17
+ var INSPECTION_TIMEOUT_MS = 12e3;
18
+ var MAX_CHILD_OUTPUT_BYTES = 2 * 1024 * 1024;
19
+ var MAX_CHILD_ERROR_BYTES = 64 * 1024;
20
+ var MAX_RESOURCE_METADATA_BYTES = 256 * 1024;
21
+ var MAX_RESOURCES = 500;
22
+ var MAX_PACKAGES = 100;
23
+ var MAX_DIAGNOSTICS = 100;
24
+ var SETTING_DEFINITIONS = [
25
+ { key: "defaultProvider", label: "Default provider", category: "model", path: ["defaultProvider"], effective: (manager) => manager.getDefaultProvider() },
26
+ { key: "defaultModel", label: "Default model", category: "model", path: ["defaultModel"], effective: (manager) => manager.getDefaultModel() },
27
+ { key: "defaultThinkingLevel", label: "Thinking level", category: "model", path: ["defaultThinkingLevel"], effective: (manager) => manager.getDefaultThinkingLevel() },
28
+ { key: "theme", label: "Pi terminal theme", category: "display", path: ["theme"], effective: (manager) => manager.getThemeSetting() },
29
+ { key: "steeringMode", label: "Steering delivery", category: "behavior", path: ["steeringMode"], effective: (manager) => manager.getSteeringMode() },
30
+ { key: "followUpMode", label: "Follow-up delivery", category: "behavior", path: ["followUpMode"], effective: (manager) => manager.getFollowUpMode() },
31
+ { key: "hideThinkingBlock", label: "Hide thinking blocks", category: "display", path: ["hideThinkingBlock"], effective: (manager) => manager.getHideThinkingBlock() },
32
+ { key: "showCacheMissNotices", label: "Show cache notices", category: "display", path: ["showCacheMissNotices"], effective: (manager) => manager.getShowCacheMissNotices() },
33
+ { key: "defaultProjectTrust", label: "Default project trust", category: "privacy", path: ["defaultProjectTrust"], effective: (manager) => manager.getDefaultProjectTrust() },
34
+ { key: "enableSkillCommands", label: "Skill slash commands", category: "tools", path: ["enableSkillCommands"], effective: (manager) => manager.getEnableSkillCommands() },
35
+ { key: "defaultTools", label: "Default tools", category: "tools", path: ["defaultTools"], effective: (manager) => manager.getDefaultTools() },
36
+ { key: "enabledModels", label: "Enabled models", category: "model", path: ["enabledModels"], effective: (manager) => manager.getEnabledModels() },
37
+ { key: "compaction.enabled", label: "Automatic compaction", category: "behavior", path: ["compaction", "enabled"], effective: (manager) => manager.getCompactionSettings().enabled },
38
+ { key: "compaction.reserveTokens", label: "Compaction reserve", category: "behavior", path: ["compaction", "reserveTokens"], effective: (manager) => manager.getCompactionSettings().reserveTokens },
39
+ { key: "compaction.keepRecentTokens", label: "Recent tokens retained", category: "behavior", path: ["compaction", "keepRecentTokens"], effective: (manager) => manager.getCompactionSettings().keepRecentTokens },
40
+ { key: "retry.enabled", label: "Automatic retries", category: "behavior", path: ["retry", "enabled"], effective: (manager) => manager.getRetrySettings().enabled },
41
+ { key: "retry.maxRetries", label: "Maximum retries", category: "behavior", path: ["retry", "maxRetries"], effective: (manager) => manager.getRetrySettings().maxRetries },
42
+ { key: "quietStartup", label: "Quiet startup", category: "display", path: ["quietStartup"], effective: (manager) => manager.getQuietStartup() },
43
+ { key: "externalEditor", label: "External editor", category: "tools", path: ["externalEditor"], effective: (manager) => manager.getExternalEditorCommand() },
44
+ { key: "shellPath", label: "Shell", category: "tools", path: ["shellPath"], effective: (manager) => manager.getShellPath() },
45
+ { key: "terminal.showImages", label: "Terminal images", category: "display", path: ["terminal", "showImages"], effective: (manager) => manager.getShowImages() },
46
+ { key: "markdown.mermaid", label: "Mermaid rendering", category: "display", path: ["markdown", "mermaid"], effective: (manager) => manager.getMermaidRenderingMode() },
47
+ { key: "enableAnalytics", label: "Pi analytics", category: "privacy", path: ["enableAnalytics"], effective: (manager) => manager.getEnableAnalytics() },
48
+ { key: "enableInstallTelemetry", label: "Pi install telemetry", category: "privacy", path: ["enableInstallTelemetry"], effective: (manager) => manager.getEnableInstallTelemetry() }
49
+ ];
50
+ var diagnosticSchema = z.object({
51
+ severity: z.enum(["warning", "error"]),
52
+ message: z.string(),
53
+ path: z.string().nullable()
54
+ }).strict();
55
+ var inspectedResourceSchema = z.object({
56
+ kind: z.enum(["extension", "skill", "prompt", "theme"]),
57
+ name: z.string(),
58
+ description: z.string().nullable(),
59
+ path: z.string(),
60
+ scope: z.enum(["user", "project", "temporary"]),
61
+ origin: z.enum(["package", "top-level"]),
62
+ source: z.string(),
63
+ enabled: z.boolean(),
64
+ disabledReason: z.enum(["filtered", "project-untrusted"]).nullable()
65
+ }).strict();
66
+ var inspectedPackageSchema = z.object({
67
+ source: z.string(),
68
+ scope: z.enum(["user", "project"]),
69
+ filtered: z.boolean(),
70
+ installed: z.boolean(),
71
+ installedPath: z.string().nullable(),
72
+ enabled: z.boolean()
73
+ }).strict();
74
+ var inspectedSettingSchema = z.object({
75
+ key: z.string(),
76
+ label: z.string(),
77
+ category: z.enum(["model", "behavior", "display", "tools", "privacy"]),
78
+ globalValue: z.string().nullable(),
79
+ projectValue: z.string().nullable(),
80
+ effectiveValue: z.string(),
81
+ effectiveScope: z.enum(["default", "user", "project"])
82
+ }).strict();
83
+ var piConfigurationInspectionSchema = z.object({
84
+ piVersion: z.string(),
85
+ cwd: z.string(),
86
+ agentDir: z.string(),
87
+ settingsPaths: z.object({ global: z.string(), project: z.string(), trust: z.string() }).strict(),
88
+ trust: z.object({
89
+ required: z.boolean(),
90
+ effective: z.boolean(),
91
+ savedDecision: z.boolean().nullable(),
92
+ savedAtPath: z.string().nullable(),
93
+ defaultPolicy: z.enum(["ask", "always", "never"])
94
+ }).strict(),
95
+ packages: z.array(inspectedPackageSchema),
96
+ resources: z.array(inspectedResourceSchema),
97
+ settings: z.array(inspectedSettingSchema),
98
+ diagnostics: z.array(diagnosticSchema),
99
+ omitted: z.object({ packages: z.number().int().nonnegative(), resources: z.number().int().nonnegative(), diagnostics: z.number().int().nonnegative() }).strict()
100
+ }).strict();
101
+ function rawSetting(settings, keys) {
102
+ let value = settings;
103
+ for (const key of keys) {
104
+ if (typeof value !== "object" || value === null || !(key in value)) return void 0;
105
+ value = value[key];
106
+ }
107
+ return value;
108
+ }
109
+ function formatSetting(value) {
110
+ if (value === void 0) return null;
111
+ if (value === null) return "None";
112
+ if (typeof value === "string") return value === "" ? "Empty" : value;
113
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
114
+ if (Array.isArray(value)) {
115
+ return value.length === 0 ? "None" : value.map((item) => String(item)).join(", ");
116
+ }
117
+ try {
118
+ return JSON.stringify(value);
119
+ } catch {
120
+ return "Configured";
121
+ }
122
+ }
123
+ function resourceLabel(kind, resourcePath) {
124
+ const name = path.basename(resourcePath);
125
+ if (kind === "skill" && name === "SKILL.md") return path.basename(path.dirname(resourcePath));
126
+ return name.replace(/\.(?:ts|js|mjs|cjs|md|json)$/iu, "") || kind;
127
+ }
128
+ function isWithin(root, candidate) {
129
+ const relative = path.relative(root, candidate);
130
+ return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== "..";
131
+ }
132
+ async function readMetadataFile(filePath, allowedRoot) {
133
+ let verifiedPath = filePath;
134
+ if (allowedRoot !== void 0) {
135
+ const [canonicalRoot, canonicalFile] = await Promise.all([
136
+ realpath(allowedRoot),
137
+ realpath(filePath)
138
+ ]);
139
+ if (!isWithin(canonicalRoot, canonicalFile)) {
140
+ throw new Error("project resource metadata resolves outside the task worktree");
141
+ }
142
+ verifiedPath = canonicalFile;
143
+ }
144
+ const details = await stat(verifiedPath);
145
+ if (!details.isFile()) throw new Error("resource metadata path is not a file");
146
+ if (details.size > MAX_RESOURCE_METADATA_BYTES) {
147
+ throw new Error("resource metadata exceeds the read-only inspection limit");
148
+ }
149
+ return readFile(verifiedPath, "utf8");
150
+ }
151
+ async function resourceMetadata(kind, resourcePath, allowedRoot) {
152
+ if (kind === "extension") return {};
153
+ if (kind === "theme") {
154
+ const parsed2 = JSON.parse(await readMetadataFile(resourcePath, allowedRoot));
155
+ if (typeof parsed2 === "object" && parsed2 !== null) {
156
+ const name2 = parsed2["name"];
157
+ return typeof name2 === "string" ? { name: name2 } : {};
158
+ }
159
+ return {};
160
+ }
161
+ const metadataPath = kind === "skill" && path.basename(resourcePath) !== "SKILL.md" ? path.join(resourcePath, "SKILL.md") : resourcePath;
162
+ const parsed = parseFrontmatter(
163
+ await readMetadataFile(metadataPath, allowedRoot)
164
+ );
165
+ const name = parsed.frontmatter["name"];
166
+ const description = parsed.frontmatter["description"];
167
+ return {
168
+ ...typeof name === "string" && name.trim() !== "" ? { name } : {},
169
+ ...typeof description === "string" ? { description } : {}
170
+ };
171
+ }
172
+ function disabledState(resource, projectTrusted) {
173
+ if (resource.metadata.scope === "project" && !projectTrusted) {
174
+ return { enabled: false, disabledReason: "project-untrusted" };
175
+ }
176
+ if (!resource.enabled) return { enabled: false, disabledReason: "filtered" };
177
+ return { enabled: true, disabledReason: null };
178
+ }
179
+ function addDiagnostic(diagnostics, seen, diagnostic) {
180
+ const identity = `${diagnostic.severity}\0${diagnostic.path ?? ""}\0${diagnostic.message}`;
181
+ if (seen.has(identity)) return;
182
+ seen.add(identity);
183
+ diagnostics.push(diagnostic);
184
+ }
185
+ function settingsDiagnostics(manager, diagnostics, seen) {
186
+ for (const issue of manager.drainErrors()) {
187
+ addDiagnostic(diagnostics, seen, {
188
+ severity: "error",
189
+ message: `Pi could not read ${issue.scope} settings: ${issue.error.message}`,
190
+ path: null
191
+ });
192
+ }
193
+ }
194
+ function packageEntries(settings, scope) {
195
+ return (settings.packages ?? []).map((entry) => ({
196
+ source: typeof entry === "string" ? entry : entry.source,
197
+ scope,
198
+ filtered: typeof entry === "object"
199
+ }));
200
+ }
201
+ function bounded(items, maximum) {
202
+ return { items: items.slice(0, maximum), omitted: Math.max(0, items.length - maximum) };
203
+ }
204
+ async function inspectPiConfigurationInProcess(options) {
205
+ if (!path.isAbsolute(options.cwd)) {
206
+ throw new PiBridgeError("PI_INVALID_CONFIGURATION", "Pi inspection cwd must be absolute");
207
+ }
208
+ const cwd = path.normalize(options.cwd);
209
+ const agentDir = path.normalize(options.agentDir ?? getAgentDir());
210
+ const diagnostics = [];
211
+ const diagnosticSeen = /* @__PURE__ */ new Set();
212
+ const globalManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
213
+ const inspectionManager = SettingsManager.create(cwd, agentDir, { projectTrusted: true });
214
+ const globalSettings = globalManager.getGlobalSettings();
215
+ const projectSettings = inspectionManager.getProjectSettings();
216
+ const defaultPolicy = globalManager.getDefaultProjectTrust();
217
+ const trustRequired = hasTrustRequiringProjectResources(cwd);
218
+ const savedTrust = new ProjectTrustStore(agentDir).getEntry(cwd);
219
+ const projectTrusted = !trustRequired || savedTrust?.decision === true || savedTrust === null && defaultPolicy === "always";
220
+ const effectiveManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
221
+ settingsDiagnostics(globalManager, diagnostics, diagnosticSeen);
222
+ settingsDiagnostics(inspectionManager, diagnostics, diagnosticSeen);
223
+ settingsDiagnostics(effectiveManager, diagnostics, diagnosticSeen);
224
+ if (trustRequired && savedTrust === null && defaultPolicy === "ask") {
225
+ addDiagnostic(diagnostics, diagnosticSeen, {
226
+ severity: "warning",
227
+ message: "Project-local Pi resources are disabled in RPC mode until the project has a saved trust decision.",
228
+ path: path.join(cwd, ".pi")
229
+ });
230
+ }
231
+ const packageManager = new DefaultPackageManager({
232
+ cwd,
233
+ agentDir,
234
+ // Until trust is effective, Pi's manager receives no project settings at
235
+ // all. This prevents an untrusted repository from making discovery probe
236
+ // arbitrary configured package/resource paths merely to label them.
237
+ settingsManager: effectiveManager
238
+ });
239
+ const rawPackages = [
240
+ ...packageEntries(globalSettings, "user"),
241
+ ...packageEntries(projectSettings, "project")
242
+ ];
243
+ const packages = rawPackages.map((entry) => {
244
+ if (entry.scope === "project" && !projectTrusted) {
245
+ return {
246
+ ...entry,
247
+ installed: false,
248
+ installedPath: null,
249
+ enabled: false
250
+ };
251
+ }
252
+ try {
253
+ const installedPath = packageManager.getInstalledPath(entry.source, entry.scope);
254
+ return {
255
+ ...entry,
256
+ installed: installedPath !== void 0,
257
+ installedPath: installedPath ?? null,
258
+ enabled: (entry.scope !== "project" || projectTrusted) && installedPath !== void 0
259
+ };
260
+ } catch (error) {
261
+ addDiagnostic(diagnostics, diagnosticSeen, {
262
+ severity: "error",
263
+ message: `Pi could not inspect package ${entry.source}: ${error instanceof Error ? error.message : "invalid package source"}`,
264
+ path: null
265
+ });
266
+ return { ...entry, installed: false, installedPath: null, enabled: false };
267
+ }
268
+ });
269
+ const resources = [];
270
+ try {
271
+ const resolved = await packageManager.resolve(async () => "skip");
272
+ const groups = [
273
+ ["extension", resolved.extensions],
274
+ ["skill", resolved.skills],
275
+ ["prompt", resolved.prompts],
276
+ ["theme", resolved.themes]
277
+ ];
278
+ for (const [kind, entries] of groups) {
279
+ for (const entry of entries) {
280
+ let metadata = {};
281
+ if (entry.metadata.scope !== "project" || projectTrusted) {
282
+ try {
283
+ metadata = await resourceMetadata(
284
+ kind,
285
+ entry.path,
286
+ entry.metadata.scope === "project" ? cwd : void 0
287
+ );
288
+ } catch (error) {
289
+ addDiagnostic(diagnostics, diagnosticSeen, {
290
+ severity: "warning",
291
+ message: `${kind} metadata could not be read: ${error instanceof Error ? error.message : "unknown error"}`,
292
+ path: entry.path
293
+ });
294
+ }
295
+ }
296
+ resources.push({
297
+ kind,
298
+ name: metadata.name?.trim() || resourceLabel(kind, entry.path),
299
+ description: metadata.description?.trim() || null,
300
+ path: entry.path,
301
+ scope: entry.metadata.scope,
302
+ origin: entry.metadata.origin,
303
+ source: entry.metadata.source || "local",
304
+ ...disabledState(entry, projectTrusted)
305
+ });
306
+ }
307
+ }
308
+ } catch (error) {
309
+ addDiagnostic(diagnostics, diagnosticSeen, {
310
+ severity: "error",
311
+ message: `Pi resource discovery failed: ${error instanceof Error ? error.message : "unknown error"}`,
312
+ path: null
313
+ });
314
+ }
315
+ const settings = SETTING_DEFINITIONS.map((definition) => {
316
+ const globalRaw = rawSetting(globalSettings, definition.path);
317
+ const projectRaw = rawSetting(projectSettings, definition.path);
318
+ const effectiveScope = projectTrusted && projectRaw !== void 0 ? "project" : globalRaw !== void 0 ? "user" : "default";
319
+ return {
320
+ key: definition.key,
321
+ label: definition.label,
322
+ category: definition.category,
323
+ globalValue: formatSetting(globalRaw),
324
+ projectValue: formatSetting(projectRaw),
325
+ effectiveValue: formatSetting(definition.effective(effectiveManager)) ?? "Not set",
326
+ effectiveScope
327
+ };
328
+ });
329
+ const boundedPackages = bounded(packages, MAX_PACKAGES);
330
+ const boundedResources = bounded(resources, MAX_RESOURCES);
331
+ const boundedDiagnostics = bounded(diagnostics, MAX_DIAGNOSTICS);
332
+ return {
333
+ piVersion: VERSION,
334
+ cwd,
335
+ agentDir,
336
+ settingsPaths: {
337
+ global: path.join(agentDir, "settings.json"),
338
+ project: path.join(cwd, ".pi", "settings.json"),
339
+ trust: path.join(agentDir, "trust.json")
340
+ },
341
+ trust: {
342
+ required: trustRequired,
343
+ effective: projectTrusted,
344
+ savedDecision: savedTrust?.decision ?? null,
345
+ savedAtPath: savedTrust?.path ?? null,
346
+ defaultPolicy
347
+ },
348
+ packages: boundedPackages.items,
349
+ resources: boundedResources.items,
350
+ settings,
351
+ diagnostics: boundedDiagnostics.items,
352
+ omitted: {
353
+ packages: boundedPackages.omitted,
354
+ resources: boundedResources.omitted,
355
+ diagnostics: boundedDiagnostics.omitted
356
+ }
357
+ };
358
+ }
359
+ function killInspector(child) {
360
+ try {
361
+ child.kill("SIGKILL");
362
+ } catch {
363
+ }
364
+ }
365
+ function createPiConfigurationInspector(options = {}) {
366
+ const timeoutMs = options.timeoutMs ?? INSPECTION_TIMEOUT_MS;
367
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) {
368
+ throw new PiBridgeError("PI_INVALID_CONFIGURATION", "Pi inspection timeout is invalid");
369
+ }
370
+ const spawnProcess = options.spawnProcess ?? spawn;
371
+ const childPath = options.childPath ?? fileURLToPath(new URL("./resources-child.mjs", import.meta.url));
372
+ return {
373
+ inspect(cwd) {
374
+ if (!path.isAbsolute(cwd)) {
375
+ return Promise.reject(
376
+ new PiBridgeError("PI_INVALID_CONFIGURATION", "Pi inspection cwd must be absolute")
377
+ );
378
+ }
379
+ return new Promise((resolve, reject) => {
380
+ const child = spawnProcess(process.execPath, [childPath], {
381
+ cwd,
382
+ env: { ...options.env ?? process.env, PI_OFFLINE: "1" },
383
+ stdio: ["pipe", "pipe", "pipe"]
384
+ });
385
+ let stdout = Buffer.alloc(0);
386
+ let stderrBytes = 0;
387
+ let settled = false;
388
+ const finish = (operation) => {
389
+ if (settled) return;
390
+ settled = true;
391
+ clearTimeout(timer);
392
+ operation();
393
+ };
394
+ const timer = setTimeout(() => {
395
+ killInspector(child);
396
+ finish(
397
+ () => reject(
398
+ new PiBridgeError("PI_REQUEST_TIMEOUT", "Pi configuration inspection timed out")
399
+ )
400
+ );
401
+ }, timeoutMs);
402
+ timer.unref?.();
403
+ child.stdout.on("data", (chunk) => {
404
+ if (settled) return;
405
+ if (stdout.length + chunk.length > MAX_CHILD_OUTPUT_BYTES) {
406
+ killInspector(child);
407
+ finish(
408
+ () => reject(
409
+ new PiBridgeError(
410
+ "PI_PROTOCOL_VIOLATION",
411
+ "Pi configuration inspection exceeded its output bound"
412
+ )
413
+ )
414
+ );
415
+ return;
416
+ }
417
+ stdout = Buffer.concat([stdout, chunk]);
418
+ });
419
+ child.stderr.on("data", (chunk) => {
420
+ stderrBytes += chunk.length;
421
+ if (stderrBytes > MAX_CHILD_ERROR_BYTES) killInspector(child);
422
+ });
423
+ child.on("error", (error) => {
424
+ finish(
425
+ () => reject(
426
+ new PiBridgeError("PI_SPAWN_FAILED", "Pi configuration inspector could not start", {
427
+ cause: error
428
+ })
429
+ )
430
+ );
431
+ });
432
+ child.on("close", (code, signal) => {
433
+ finish(() => {
434
+ if (code !== 0 || signal !== null) {
435
+ reject(
436
+ new PiBridgeError(
437
+ "PI_RUNTIME_EXITED",
438
+ "Pi configuration inspection did not complete",
439
+ { details: { code, signal } }
440
+ )
441
+ );
442
+ return;
443
+ }
444
+ let decoded;
445
+ try {
446
+ decoded = JSON.parse(stdout.toString("utf8"));
447
+ } catch (error) {
448
+ reject(
449
+ new PiBridgeError(
450
+ "PI_PROTOCOL_VIOLATION",
451
+ "Pi configuration inspector returned invalid JSON",
452
+ { cause: error }
453
+ )
454
+ );
455
+ return;
456
+ }
457
+ const parsed = piConfigurationInspectionSchema.safeParse(decoded);
458
+ if (!parsed.success) {
459
+ reject(
460
+ new PiBridgeError(
461
+ "PI_PROTOCOL_VIOLATION",
462
+ "Pi configuration inspector returned incompatible data",
463
+ { cause: parsed.error }
464
+ )
465
+ );
466
+ return;
467
+ }
468
+ resolve(parsed.data);
469
+ });
470
+ });
471
+ child.stdin.on("error", () => {
472
+ });
473
+ child.stdin.end(JSON.stringify({ cwd: path.normalize(cwd) }));
474
+ });
475
+ }
476
+ };
477
+ }
478
+ export {
479
+ createPiConfigurationInspector,
480
+ inspectPiConfigurationInProcess
481
+ };