@pasko70/pibo 2.5.2 → 3.0.1

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 (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +464 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -0,0 +1,644 @@
1
+ import { execFile, spawn, spawnSync } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { basename } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { sanitizePreviewServerSettings } from "../core/preview-server-settings.js";
8
+ import { loadPreviewConfig } from "./config.js";
9
+ import { findPreviewTargetProcess, isPreviewTargetProcessCurrent, previewProcessStartTicks, probePreviewTarget, } from "./network.js";
10
+ import { previewExposureState } from "./store.js";
11
+ const execFileAsync = promisify(execFile);
12
+ const DEFAULT_STARTUP_TIMEOUT_MS = 15_000;
13
+ const DEFAULT_POLL_INTERVAL_MS = 100;
14
+ const PREVIEW_OWNER_ENV = "PIBO_PREVIEW_OWNER_TOKEN";
15
+ const PREVIEW_COMMAND_ENV = "PIBO_PREVIEW_START_COMMAND";
16
+ export function validatePreviewStartCommand(value) {
17
+ const command = value.replace(/\r\n/g, "\n").trim();
18
+ if (!command)
19
+ throw new Error("Preview start command is required");
20
+ if (command.includes("\0"))
21
+ throw new Error("Preview start command cannot contain NUL bytes");
22
+ if (command.length > 8_192)
23
+ throw new Error("Preview start command is too long");
24
+ return command;
25
+ }
26
+ export async function startManagedPreview(store, id, options = {}) {
27
+ const controller = options.controller ?? createDefaultPreviewProcessController();
28
+ const settings = options.settings ?? (() => {
29
+ const previewConfig = loadPreviewConfig();
30
+ return sanitizePreviewServerSettings({
31
+ maxRunningServers: previewConfig.maxRunningServers,
32
+ autoStopMinutes: previewConfig.autoStopMinutes,
33
+ });
34
+ })();
35
+ const now = options.now?.() ?? new Date();
36
+ await reconcileManagedPreviews(store, { ...options, controller, settings, now: () => now });
37
+ let current = store.requireExposure(id);
38
+ if (current.managementMode !== "managed" || !current.startCommand)
39
+ throw new Error(`Preview "${id}" has no managed start command`);
40
+ const startCommand = current.startCommand;
41
+ if (current.serverState === "running" && await managedPreviewIsOnline(current, controller, now))
42
+ return current;
43
+ if (current.serverState === "starting")
44
+ return current;
45
+ if (current.serverState === "running") {
46
+ await stopManagedPreview(store, id, { ...options, controller, settings });
47
+ current = store.requireExposure(id);
48
+ }
49
+ const stopAt = new Date(Math.min(now.getTime() + settings.autoStopMinutes * 60_000, Date.parse(current.expiresAt))).toISOString();
50
+ const launchInput = {
51
+ previewId: current.id,
52
+ command: startCommand,
53
+ workspace: current.workspace,
54
+ port: current.targetPort,
55
+ };
56
+ const preparedManager = controller.createIdentity(launchInput);
57
+ const reservation = store.reserveManagedServerStart(id, settings.maxRunningServers, now.toISOString(), stopAt, preparedManager);
58
+ if (!reservation.reserved)
59
+ return reservation.exposure;
60
+ const generation = reservation.exposure.serverGeneration;
61
+ if (!generation)
62
+ throw new Error(`Preview "${id}" did not retain its start generation`);
63
+ const durableManager = managerIdentity(reservation.exposure);
64
+ if (!durableManager || !sameManagerOwner(durableManager, preparedManager)) {
65
+ throw new Error(`Preview "${id}" did not durably retain its manager identity`);
66
+ }
67
+ let manager = durableManager;
68
+ try {
69
+ if (await probePreviewTarget(current.targetPort, { timeoutMs: 150 })) {
70
+ throw new Error(`Preview port ${current.targetPort} is already occupied`);
71
+ }
72
+ manager = await controller.launch(launchInput, durableManager);
73
+ if (!sameManagerOwner(manager, durableManager)) {
74
+ throw new Error("Preview controller changed its durable owner identity during launch");
75
+ }
76
+ const assigned = store.markManagedServerManager(id, generation, manager);
77
+ if (!managedGenerationMatches(assigned, generation, manager) || assigned.serverState !== "starting") {
78
+ if (await stopManagedOwner(controller, manager)) {
79
+ const latest = store.requireExposure(id);
80
+ if (managedGenerationMatches(latest, generation, manager)) {
81
+ return store.markManagedServerStopped(id, { expectedGeneration: generation });
82
+ }
83
+ }
84
+ return store.requireExposure(id);
85
+ }
86
+ const target = await waitForManagedTarget(current, manager, controller, {
87
+ startupTimeoutMs: options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS,
88
+ pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
89
+ });
90
+ const running = store.markManagedServerRunning(id, generation, {
91
+ targetHost: target.host,
92
+ targetProcessId: target.process?.pid,
93
+ targetProcessStartTicks: target.process?.startTicks,
94
+ manager,
95
+ });
96
+ if (!managedGenerationMatches(running, generation, manager) || running.serverState !== "running") {
97
+ if (await stopManagedOwner(controller, manager)) {
98
+ const latest = store.requireExposure(id);
99
+ if (managedGenerationMatches(latest, generation, manager)) {
100
+ return store.markManagedServerStopped(id, { expectedGeneration: generation });
101
+ }
102
+ }
103
+ }
104
+ return running;
105
+ }
106
+ catch (error) {
107
+ const stopped = await stopManagedOwner(controller, manager);
108
+ const latest = store.requireExposure(id);
109
+ if (!managedGenerationMatches(latest, generation, durableManager))
110
+ return latest;
111
+ if (stopped) {
112
+ store.markManagedServerStopped(id, {
113
+ stoppedAt: (options.now?.() ?? new Date()).toISOString(),
114
+ error: managedPreviewError(error),
115
+ expectedGeneration: generation,
116
+ });
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+ export async function stopManagedPreview(store, id, options = {}) {
122
+ const controller = options.controller ?? createDefaultPreviewProcessController();
123
+ const exposure = store.requireExposure(id);
124
+ if (exposure.managementMode !== "managed")
125
+ throw new Error(`Preview "${id}" is not managed by Pibo`);
126
+ if (!["starting", "running", "stopping", "error"].includes(exposure.serverState ?? ""))
127
+ return exposure;
128
+ const identity = managerIdentity(exposure);
129
+ if (!identity || !exposure.serverGeneration) {
130
+ if (exposure.serverState === "error" && !identity && !exposure.serverGeneration) {
131
+ return store.markManagedServerStopped(id);
132
+ }
133
+ throw new Error(`Preview "${id}" has no durable managed owner identity`);
134
+ }
135
+ const wasStarting = exposure.serverState === "starting";
136
+ const launchMayStillPublish = !exposure.targetProcessId &&
137
+ (exposure.serverState === "starting" || exposure.serverState === "stopping");
138
+ if (exposure.serverState !== "stopping")
139
+ store.markManagedServerStopping(id, exposure.serverGeneration);
140
+ await controller.stop(identity);
141
+ if (await controller.isRunning(identity)) {
142
+ throw new Error(`Preview "${id}" exact owner cleanup is incomplete`);
143
+ }
144
+ if (wasStarting || launchMayStillPublish)
145
+ return store.requireExposure(id);
146
+ return store.markManagedServerStopped(id, { expectedGeneration: exposure.serverGeneration });
147
+ }
148
+ async function stopManagedPreviewForReconcile(controller, identity, previewId) {
149
+ try {
150
+ await controller.stop(identity);
151
+ return !await controller.isRunning(identity);
152
+ }
153
+ catch (error) {
154
+ console.error(`Preview server reconciliation could not stop ${previewId}: ${managedPreviewError(error)}`);
155
+ return false;
156
+ }
157
+ }
158
+ export async function reconcileManagedPreviews(store, options = {}) {
159
+ const controller = options.controller ?? createDefaultPreviewProcessController();
160
+ const now = options.now?.() ?? new Date();
161
+ const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
162
+ for (const exposure of store.listManagedServerCandidates()) {
163
+ const identity = managerIdentity(exposure);
164
+ const expired = previewExposureState(exposure, now) !== "active";
165
+ const leaseEnded = Boolean(exposure.serverStopAt && Date.parse(exposure.serverStopAt) <= now.getTime());
166
+ const startupTimedOut = (exposure.serverState === "starting" ||
167
+ (exposure.serverState === "stopping" && !exposure.targetProcessId)) &&
168
+ Boolean(exposure.serverStartedAt) &&
169
+ now.getTime() - Date.parse(exposure.serverStartedAt) > startupTimeoutMs + 5_000;
170
+ if (expired || leaseEnded || startupTimedOut) {
171
+ if (identity && exposure.serverGeneration && exposure.serverState !== "stopping") {
172
+ store.markManagedServerStopping(exposure.id, exposure.serverGeneration);
173
+ }
174
+ if (identity && !await stopManagedPreviewForReconcile(controller, identity, exposure.id))
175
+ continue;
176
+ if (exposure.serverState === "starting" && !startupTimedOut)
177
+ continue;
178
+ store.markManagedServerStopped(exposure.id, {
179
+ stoppedAt: now.toISOString(),
180
+ error: startupTimedOut ? "Managed Preview server startup timed out" : undefined,
181
+ expectedGeneration: exposure.serverGeneration,
182
+ });
183
+ continue;
184
+ }
185
+ if (exposure.serverState === "stopping") {
186
+ if (identity &&
187
+ await stopManagedPreviewForReconcile(controller, identity, exposure.id) &&
188
+ exposure.targetProcessId) {
189
+ store.markManagedServerStopped(exposure.id, {
190
+ stoppedAt: now.toISOString(),
191
+ expectedGeneration: exposure.serverGeneration,
192
+ });
193
+ }
194
+ continue;
195
+ }
196
+ if (exposure.serverState === "error" && identity) {
197
+ if (!await stopManagedPreviewForReconcile(controller, identity, exposure.id))
198
+ continue;
199
+ store.markManagedServerStopped(exposure.id, {
200
+ stoppedAt: now.toISOString(),
201
+ error: exposure.serverError,
202
+ expectedGeneration: exposure.serverGeneration,
203
+ });
204
+ continue;
205
+ }
206
+ if (!identity) {
207
+ const startedAt = exposure.serverStartedAt ? Date.parse(exposure.serverStartedAt) : 0;
208
+ if (!startedAt) {
209
+ store.markManagedServerStopped(exposure.id, {
210
+ stoppedAt: now.toISOString(),
211
+ error: "Managed Preview server did not retain a process identity",
212
+ expectedGeneration: exposure.serverGeneration,
213
+ });
214
+ }
215
+ continue;
216
+ }
217
+ const ownerRunning = await controller.isRunning(identity);
218
+ const managerRunning = await exactManagerIsRunning(controller, identity);
219
+ if (exposure.serverState !== "starting" && (!ownerRunning || !managerRunning)) {
220
+ if (exposure.serverGeneration) {
221
+ store.markManagedServerStopping(exposure.id, exposure.serverGeneration);
222
+ }
223
+ if (!await stopManagedPreviewForReconcile(controller, identity, exposure.id))
224
+ continue;
225
+ store.markManagedServerStopped(exposure.id, {
226
+ stoppedAt: now.toISOString(),
227
+ expectedGeneration: exposure.serverGeneration,
228
+ });
229
+ continue;
230
+ }
231
+ if (exposure.serverState === "running") {
232
+ const targetCurrent = isPreviewTargetProcessCurrent(exposure, { cacheMs: 0 });
233
+ const targetReachable = targetCurrent && Boolean(await probePreviewTarget(exposure.targetPort, { timeoutMs: 500 }));
234
+ if (!targetReachable) {
235
+ if (!await stopManagedPreviewForReconcile(controller, identity, exposure.id))
236
+ continue;
237
+ store.markManagedServerStopped(exposure.id, {
238
+ stoppedAt: now.toISOString(),
239
+ error: "Managed Preview listener no longer matches its pinned process",
240
+ expectedGeneration: exposure.serverGeneration,
241
+ });
242
+ }
243
+ }
244
+ }
245
+ }
246
+ export async function managedPreviewIsOnline(exposure, controller = createDefaultPreviewProcessController(), now = new Date()) {
247
+ if (previewExposureState(exposure, now) !== "active")
248
+ return false;
249
+ if (exposure.managementMode !== "managed" || exposure.serverState !== "running")
250
+ return false;
251
+ const identity = managerIdentity(exposure);
252
+ if (!identity || !await exactManagerIsRunning(controller, identity))
253
+ return false;
254
+ if (!isPreviewTargetProcessCurrent(exposure, { cacheMs: 0 }))
255
+ return false;
256
+ return Boolean(await probePreviewTarget(exposure.targetPort, { timeoutMs: 500 }));
257
+ }
258
+ export function managerIdentity(exposure) {
259
+ if (!exposure.managerKind || !exposure.managerId)
260
+ return undefined;
261
+ return {
262
+ kind: exposure.managerKind,
263
+ id: exposure.managerId,
264
+ pid: exposure.managerPid,
265
+ processStartTicks: exposure.managerProcessStartTicks,
266
+ };
267
+ }
268
+ export function createDefaultPreviewProcessController() {
269
+ const preferSystemd = systemdAvailable();
270
+ const controllerFor = (identity) => identity.kind === "systemd"
271
+ ? systemdPreviewProcessController
272
+ : detachedPreviewProcessController;
273
+ return {
274
+ createIdentity(input) {
275
+ return preferSystemd
276
+ ? systemdPreviewProcessController.createIdentity(input)
277
+ : detachedPreviewProcessController.createIdentity(input);
278
+ },
279
+ async launch(input, identity) {
280
+ return preferSystemd
281
+ ? systemdPreviewProcessController.launch(input, identity)
282
+ : detachedPreviewProcessController.launch(input, identity);
283
+ },
284
+ isRunning(identity) {
285
+ return controllerFor(identity).isRunning(identity);
286
+ },
287
+ isManagerRunning(identity) {
288
+ const controller = controllerFor(identity);
289
+ return controller.isManagerRunning?.(identity) ?? controller.isRunning(identity);
290
+ },
291
+ ownsTarget(identity, targetPid) {
292
+ return controllerFor(identity).ownsTarget(identity, targetPid);
293
+ },
294
+ stop(identity) {
295
+ return controllerFor(identity).stop(identity);
296
+ },
297
+ };
298
+ }
299
+ const systemdPreviewProcessController = {
300
+ createIdentity(input) {
301
+ if (process.platform !== "linux") {
302
+ throw new Error("Managed Preview process ownership requires Linux process identity support");
303
+ }
304
+ return {
305
+ kind: "systemd",
306
+ id: `pibo-preview-${input.previewId.replace(/^pv-/, "").slice(0, 18)}-${randomBytes(12).toString("hex")}.service`,
307
+ };
308
+ },
309
+ async launch(input, identity) {
310
+ if (process.platform !== "linux") {
311
+ throw new Error("Managed Preview process ownership requires Linux process identity support");
312
+ }
313
+ if (identity.kind !== "systemd")
314
+ throw new Error("Preview systemd owner identity is invalid");
315
+ const unit = identity.id;
316
+ const shell = previewShell();
317
+ const args = [
318
+ "--quiet",
319
+ `--unit=${unit}`,
320
+ "--collect",
321
+ "--service-type=exec",
322
+ `--working-directory=${input.workspace}`,
323
+ "--property=KillMode=control-group",
324
+ "--property=TimeoutStopSec=5s",
325
+ ...systemdEnvironment(input.port),
326
+ "--",
327
+ shell.command,
328
+ ...shell.args(input.command),
329
+ ];
330
+ await execFileAsync("systemd-run", args, { timeout: 10_000, windowsHide: true });
331
+ return identity;
332
+ },
333
+ async isRunning(identity) {
334
+ try {
335
+ const { stdout } = await execFileAsync("systemctl", ["show", "--property=ActiveState", "--value", identity.id], { timeout: 5_000 });
336
+ return ["active", "activating", "reloading"].includes(stdout.trim());
337
+ }
338
+ catch {
339
+ return false;
340
+ }
341
+ },
342
+ async isManagerRunning(identity) {
343
+ return this.isRunning(identity);
344
+ },
345
+ async ownsTarget(identity, targetPid) {
346
+ try {
347
+ return readFileSync(`/proc/${targetPid}/cgroup`, "utf8").includes(`/${identity.id}`);
348
+ }
349
+ catch {
350
+ return false;
351
+ }
352
+ },
353
+ async stop(identity) {
354
+ try {
355
+ await execFileAsync("systemctl", ["stop", identity.id], { timeout: 10_000, windowsHide: true });
356
+ }
357
+ catch (error) {
358
+ if (await this.isRunning(identity))
359
+ throw error;
360
+ }
361
+ },
362
+ };
363
+ const detachedPreviewProcessController = {
364
+ createIdentity(input) {
365
+ if (process.platform !== "linux") {
366
+ throw new Error("Managed Preview process ownership requires Linux process identity support");
367
+ }
368
+ return {
369
+ kind: "process",
370
+ id: `pibo-preview-${input.previewId.replace(/^pv-/, "").slice(0, 18)}-${randomBytes(24).toString("base64url")}`,
371
+ };
372
+ },
373
+ async launch(input, identity) {
374
+ if (process.platform !== "linux") {
375
+ throw new Error("Managed Preview process ownership requires Linux process identity support");
376
+ }
377
+ if (identity.kind !== "process")
378
+ throw new Error("Preview process owner identity is invalid");
379
+ const child = spawn(process.execPath, [fileURLToPath(new URL("./process-supervisor.js", import.meta.url))], {
380
+ cwd: input.workspace,
381
+ env: {
382
+ ...previewEnvironment(input.port, identity.id),
383
+ [PREVIEW_COMMAND_ENV]: input.command,
384
+ },
385
+ detached: true,
386
+ stdio: "ignore",
387
+ windowsHide: true,
388
+ });
389
+ if (!child.pid)
390
+ throw new Error("Preview server process did not provide a pid");
391
+ const processStartTicks = previewProcessStartTicks(child.pid);
392
+ if (process.platform === "linux" && !processStartTicks) {
393
+ try {
394
+ process.kill(-child.pid, "SIGKILL");
395
+ }
396
+ catch { }
397
+ throw new Error("Preview server process could not be pinned");
398
+ }
399
+ child.unref();
400
+ return {
401
+ ...identity,
402
+ pid: child.pid,
403
+ processStartTicks,
404
+ };
405
+ },
406
+ async isRunning(identity) {
407
+ return process.platform === "linux" && ownedLinuxProcesses(identity).length > 0;
408
+ },
409
+ async isManagerRunning(identity) {
410
+ return process.platform === "linux" && exactLinuxManagerIsRunning(identity);
411
+ },
412
+ async ownsTarget(identity, targetPid) {
413
+ if (process.platform !== "linux")
414
+ return false;
415
+ if (processOwnerToken(targetPid) === identity.id)
416
+ return true;
417
+ const targetGroup = await processGroupId(targetPid);
418
+ return targetGroup !== undefined && ownedLinuxProcesses(identity).some((owned) => owned.processGroupId === targetGroup);
419
+ },
420
+ async stop(identity) {
421
+ if (process.platform !== "linux") {
422
+ throw new Error("Refusing to signal a managed Preview process without verifiable Linux ownership");
423
+ }
424
+ const ownGroup = await processGroupId(process.pid);
425
+ const tracked = new Map();
426
+ const scan = () => {
427
+ const current = ownedLinuxProcesses(identity);
428
+ for (const owned of current)
429
+ tracked.set(owned.pid, owned);
430
+ return current;
431
+ };
432
+ const initial = scan();
433
+ const groups = verifiedOwnedProcessGroups(identity, initial);
434
+ if (ownGroup && groups.has(ownGroup))
435
+ throw new Error("Refusing to stop the current Pibo process group");
436
+ for (const group of groups) {
437
+ try {
438
+ process.kill(-group, "SIGTERM");
439
+ }
440
+ catch (error) {
441
+ if (!missingProcess(error))
442
+ throw error;
443
+ }
444
+ }
445
+ for (let attempt = 0; attempt < 20 && exactTrackedProcessesRemain(tracked, scan()); attempt += 1)
446
+ await delay(100);
447
+ let remaining = scan();
448
+ if (exactTrackedProcessesRemain(tracked, remaining)) {
449
+ for (const group of verifiedOwnedProcessGroups(identity, remaining)) {
450
+ try {
451
+ process.kill(-group, "SIGKILL");
452
+ }
453
+ catch (error) {
454
+ if (!missingProcess(error))
455
+ throw error;
456
+ }
457
+ }
458
+ for (const owned of tracked.values()) {
459
+ if (previewProcessStartTicks(owned.pid) !== owned.processStartTicks)
460
+ continue;
461
+ try {
462
+ process.kill(owned.pid, "SIGKILL");
463
+ }
464
+ catch (error) {
465
+ if (!missingProcess(error))
466
+ throw error;
467
+ }
468
+ }
469
+ }
470
+ for (let attempt = 0; attempt < 20 && exactTrackedProcessesRemain(tracked, scan()); attempt += 1)
471
+ await delay(100);
472
+ remaining = scan();
473
+ if (exactTrackedProcessesRemain(tracked, remaining))
474
+ throw new Error("Managed Preview exact process group did not terminate");
475
+ },
476
+ };
477
+ async function waitForManagedTarget(exposure, manager, controller, options) {
478
+ const deadline = Date.now() + options.startupTimeoutMs;
479
+ while (Date.now() < deadline) {
480
+ if (!await exactManagerIsRunning(controller, manager))
481
+ throw new Error("Managed Preview command exited before opening its port");
482
+ const target = await probePreviewTarget(exposure.targetPort, { timeoutMs: Math.min(250, options.pollIntervalMs) });
483
+ if (target) {
484
+ const process = findPreviewTargetProcess(target.host, exposure.targetPort);
485
+ if (globalThis.process.platform === "linux" && !process) {
486
+ throw new Error(`Preview port ${exposure.targetPort} is not bound exclusively to ${target.host}`);
487
+ }
488
+ if (process && !await controller.ownsTarget(manager, process.pid)) {
489
+ throw new Error(`Preview port ${exposure.targetPort} is owned by a different process`);
490
+ }
491
+ return { host: target.host, process };
492
+ }
493
+ await delay(options.pollIntervalMs);
494
+ }
495
+ throw new Error(`Managed Preview command did not open loopback port ${exposure.targetPort} within ${options.startupTimeoutMs}ms`);
496
+ }
497
+ function systemdAvailable() {
498
+ if (process.env.NODE_ENV === "test" || process.platform !== "linux" || !existsSync("/run/systemd/system"))
499
+ return false;
500
+ try {
501
+ return spawnSync("systemd-run", ["--version"], { stdio: "ignore", timeout: 2_000 }).status === 0;
502
+ }
503
+ catch {
504
+ return false;
505
+ }
506
+ }
507
+ function previewShell() {
508
+ if (process.platform === "win32")
509
+ return { command: process.env.ComSpec ?? "cmd.exe", args: (command) => ["/d", "/s", "/c", command] };
510
+ const command = process.env.SHELL && existsSync(process.env.SHELL) ? process.env.SHELL : existsSync("/bin/bash") ? "/bin/bash" : "/bin/sh";
511
+ return { command, args: (value) => basename(command).includes("bash") ? ["-lc", value] : ["-c", value] };
512
+ }
513
+ function previewEnvironment(port, ownerToken) {
514
+ const environment = {};
515
+ for (const key of ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "TMPDIR", "TEMP", "TMP"]) {
516
+ if (process.env[key] !== undefined)
517
+ environment[key] = process.env[key];
518
+ }
519
+ environment.PIBO_PREVIEW_HOST = "127.0.0.1";
520
+ environment.PIBO_PREVIEW_PORT = String(port);
521
+ environment.HOST = "127.0.0.1";
522
+ environment.PORT = String(port);
523
+ if (ownerToken)
524
+ environment[PREVIEW_OWNER_ENV] = ownerToken;
525
+ return environment;
526
+ }
527
+ function systemdEnvironment(port) {
528
+ return Object.entries(previewEnvironment(port)).flatMap(([key, value]) => value === undefined ? [] : [`--setenv=${key}=${value}`]);
529
+ }
530
+ async function processGroupId(pid) {
531
+ try {
532
+ const { stdout } = await execFileAsync("ps", ["-o", "pgid=", "-p", String(pid)], { timeout: 5_000 });
533
+ const value = Number.parseInt(stdout.trim(), 10);
534
+ return Number.isInteger(value) && value > 0 ? value : undefined;
535
+ }
536
+ catch {
537
+ return undefined;
538
+ }
539
+ }
540
+ function processOwnerToken(pid) {
541
+ if (process.platform !== "linux")
542
+ return undefined;
543
+ try {
544
+ const prefix = `${PREVIEW_OWNER_ENV}=`;
545
+ return readFileSync(`/proc/${pid}/environ`, "utf8")
546
+ .split("\0")
547
+ .find((entry) => entry.startsWith(prefix))
548
+ ?.slice(prefix.length);
549
+ }
550
+ catch {
551
+ return undefined;
552
+ }
553
+ }
554
+ function ownedLinuxProcesses(identity) {
555
+ if (process.platform !== "linux" || identity.kind !== "process")
556
+ return [];
557
+ let entries;
558
+ try {
559
+ entries = readdirSync("/proc");
560
+ }
561
+ catch {
562
+ return [];
563
+ }
564
+ const owned = [];
565
+ for (const entry of entries) {
566
+ if (!/^\d+$/.test(entry))
567
+ continue;
568
+ const pid = Number(entry);
569
+ if (processOwnerToken(pid) !== identity.id)
570
+ continue;
571
+ try {
572
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
573
+ const commandEnd = stat.lastIndexOf(") ");
574
+ if (commandEnd < 0)
575
+ continue;
576
+ const fields = stat.slice(commandEnd + 2).trim().split(/\s+/);
577
+ const processGroupId = Number.parseInt(fields[2] ?? "", 10);
578
+ const processStartTicks = fields[19];
579
+ if (Number.isInteger(processGroupId) && processGroupId > 0 && processStartTicks) {
580
+ owned.push({ pid, processGroupId, processStartTicks });
581
+ }
582
+ }
583
+ catch { }
584
+ }
585
+ return owned;
586
+ }
587
+ function exactTrackedProcessesRemain(tracked, current) {
588
+ if (current.length > 0)
589
+ return true;
590
+ for (const owned of tracked.values()) {
591
+ if (previewProcessStartTicks(owned.pid) === owned.processStartTicks)
592
+ return true;
593
+ }
594
+ return false;
595
+ }
596
+ function verifiedOwnedProcessGroups(identity, candidates) {
597
+ const groups = new Set();
598
+ for (const owned of candidates) {
599
+ if (previewProcessStartTicks(owned.pid) !== owned.processStartTicks)
600
+ continue;
601
+ if (processOwnerToken(owned.pid) !== identity.id)
602
+ continue;
603
+ groups.add(owned.processGroupId);
604
+ }
605
+ return groups;
606
+ }
607
+ function exactLinuxManagerIsRunning(identity) {
608
+ if (process.platform !== "linux" ||
609
+ identity.kind !== "process" ||
610
+ !identity.pid ||
611
+ !identity.processStartTicks)
612
+ return false;
613
+ return previewProcessStartTicks(identity.pid) === identity.processStartTicks &&
614
+ processOwnerToken(identity.pid) === identity.id;
615
+ }
616
+ function exactManagerIsRunning(controller, identity) {
617
+ return controller.isManagerRunning?.(identity) ?? controller.isRunning(identity);
618
+ }
619
+ function sameManagerOwner(first, second) {
620
+ return first.kind === second.kind && first.id === second.id;
621
+ }
622
+ function managedGenerationMatches(exposure, generation, manager) {
623
+ const current = managerIdentity(exposure);
624
+ return exposure.serverGeneration === generation && Boolean(current && sameManagerOwner(current, manager));
625
+ }
626
+ async function stopManagedOwner(controller, identity) {
627
+ try {
628
+ await controller.stop(identity);
629
+ return !await controller.isRunning(identity);
630
+ }
631
+ catch {
632
+ return false;
633
+ }
634
+ }
635
+ function missingProcess(error) {
636
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
637
+ }
638
+ function managedPreviewError(error) {
639
+ const message = error instanceof Error ? error.message : String(error);
640
+ return message.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 500);
641
+ }
642
+ function delay(milliseconds) {
643
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
644
+ }