@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem layout for the agent-board store.
|
|
3
|
+
*
|
|
4
|
+
* Every helper takes an explicit `root` so tests can point at a tmp dir.
|
|
5
|
+
* The live default is `~/.pi/agent/agent-board/` (override with $AGENT_BOARD_ROOT;
|
|
6
|
+
* legacy $AGENT_VIEW_ROOT is also honored for migration).
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
/** @returns {string} the live store root (env override or ~/.pi/agent/agent-board). */
|
|
13
|
+
export function defaultRoot() {
|
|
14
|
+
if (process.env.AGENT_BOARD_ROOT) return path.resolve(process.env.AGENT_BOARD_ROOT);
|
|
15
|
+
if (process.env.AGENT_VIEW_ROOT) return path.resolve(process.env.AGENT_VIEW_ROOT);
|
|
16
|
+
return path.join(os.homedir(), ".pi", "agent", "agent-board");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @param {string} root */
|
|
20
|
+
export const rosterPath = (root) => path.join(root, "roster.json");
|
|
21
|
+
/** @param {string} root */
|
|
22
|
+
export const launchPrefsPath = (root) => path.join(root, "launch-prefs.json");
|
|
23
|
+
/** @param {string} root */
|
|
24
|
+
export const gcHistoryPath = (root) => path.join(root, "gc-history.jsonl");
|
|
25
|
+
/** @param {string} root */
|
|
26
|
+
export const cwdStatsPath = (root) => path.join(root, "cwd-stats.json");
|
|
27
|
+
/** @param {string} root */
|
|
28
|
+
export const providersPath = (root) => path.join(root, "providers.json");
|
|
29
|
+
|
|
30
|
+
/** @param {string} root */
|
|
31
|
+
export const viewsDir = (root) => path.join(root, "views");
|
|
32
|
+
/** @param {string} root @param {string} viewId */
|
|
33
|
+
export const viewDir = (root, viewId) => path.join(root, "views", viewId);
|
|
34
|
+
/** @param {string} root @param {string} viewId */
|
|
35
|
+
export const metaPath = (root, viewId) => path.join(viewDir(root, viewId), "meta.json");
|
|
36
|
+
/** @param {string} root @param {string} viewId */
|
|
37
|
+
export const statePath = (root, viewId) => path.join(viewDir(root, viewId), "state.json");
|
|
38
|
+
/** @param {string} root @param {string} viewId */
|
|
39
|
+
export const hostPath = (root, viewId) => path.join(viewDir(root, viewId), "host.json");
|
|
40
|
+
/** Durable control-command journal for a view's host (issue #91 phase 5, spec
|
|
41
|
+
* D4): one JSONL line per durable-command lifecycle transition
|
|
42
|
+
* (accepted/applied). Sibling of host.json in the view dir. */
|
|
43
|
+
export const controlJournalPath = (root, viewId) => path.join(viewDir(root, viewId), "control-journal.jsonl");
|
|
44
|
+
/** @param {string} root @param {string} viewId */
|
|
45
|
+
export const hostConfigPath = (root, viewId) => path.join(viewDir(root, viewId), "host-config.json");
|
|
46
|
+
/**
|
|
47
|
+
* Per-instance host config path. Each host launch owns its own config file so a
|
|
48
|
+
* concurrent launch can never overwrite another instance's prompt/cwd/model (issue #70).
|
|
49
|
+
* @param {string} root @param {string} viewId @param {string} instanceId
|
|
50
|
+
*/
|
|
51
|
+
export const hostConfigPathFor = (root, viewId, instanceId) =>
|
|
52
|
+
path.join(viewDir(root, viewId), `host-config.${instanceId}.json`);
|
|
53
|
+
/** @param {string} root @param {string} viewId */
|
|
54
|
+
export const titleConfigPath = (root, viewId) => path.join(viewDir(root, viewId), "title-config.json");
|
|
55
|
+
/** @param {string} root @param {string} viewId */
|
|
56
|
+
export const autoStateConfigPath = (root, viewId) => path.join(viewDir(root, viewId), "auto-state-config.json");
|
|
57
|
+
/**
|
|
58
|
+
* Named pipe used for the host control socket on Windows. Named pipes are kernel
|
|
59
|
+
* objects, not filesystem entries, so existsSync/unlink never apply to them; they
|
|
60
|
+
* vanish automatically when the owning process exits. Names must be ≤ 256 chars.
|
|
61
|
+
* @param {string} viewId
|
|
62
|
+
*/
|
|
63
|
+
export const controlPipeName = (viewId) => `\\\\.\\pipe\\pi-agent-board-${viewId}`;
|
|
64
|
+
/**
|
|
65
|
+
* Control socket address for a view, platform-aware. On Windows plain filesystem
|
|
66
|
+
* paths cannot be bound by node's net module (EACCES) — only `\\.\pipe\`-style
|
|
67
|
+
* names work (see libuv#458, nodejs/node#55979).
|
|
68
|
+
* @param {"win32"|"linux"|"darwin"} platform
|
|
69
|
+
* @param {string} root
|
|
70
|
+
* @param {string} viewId
|
|
71
|
+
*/
|
|
72
|
+
export function controlSocketPathFor(platform, root, viewId) {
|
|
73
|
+
if (platform === "win32") return controlPipeName(viewId);
|
|
74
|
+
return path.join(viewDir(root, viewId), "control.sock");
|
|
75
|
+
}
|
|
76
|
+
/** @param {string} root @param {string} viewId */
|
|
77
|
+
export const controlSocketPath = (root, viewId) => controlSocketPathFor(process.platform, root, viewId);
|
|
78
|
+
/**
|
|
79
|
+
* Endpoint a hosted child's editor-state reporter should connect to.
|
|
80
|
+
*
|
|
81
|
+
* The runner knows which endpoint it actually bound and injects it into the
|
|
82
|
+
* child env as AGENT_BOARD_CONTROL_SOCKET (issue #103): since #70 the owned
|
|
83
|
+
* runner binds a per-instance address, while the reporter kept dialing the
|
|
84
|
+
* stable per-view one, so it never connected and the attach gate lost its
|
|
85
|
+
* authoritative editor state. The injected value wins; the stable address
|
|
86
|
+
* remains the fallback for legacy hosts and older runners that predate the key.
|
|
87
|
+
* @param {{ envSocketPath?: string | null, platform: "win32"|"linux"|"darwin", root: string, viewId: string }} args
|
|
88
|
+
*/
|
|
89
|
+
export function resolveControlEndpoint({ envSocketPath, platform, root, viewId }) {
|
|
90
|
+
const injected = typeof envSocketPath === "string" ? envSocketPath.trim() : "";
|
|
91
|
+
return injected.length > 0 ? injected : controlSocketPathFor(platform, root, viewId);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Per-instance control endpoint. Each new host instance binds its own socket/pipe,
|
|
95
|
+
* so a superseded runner can never unlink the current owner's endpoint (issue #70).
|
|
96
|
+
* win32 pipe names embed an 8-hex hash of the instanceId to stay under the 256-char limit.
|
|
97
|
+
* @param {"win32"|"linux"|"darwin"} platform
|
|
98
|
+
* @param {string} root
|
|
99
|
+
* @param {string} viewId
|
|
100
|
+
* @param {string} instanceId
|
|
101
|
+
*/
|
|
102
|
+
export function hostEndpointPathFor(platform, root, viewId, instanceId) {
|
|
103
|
+
if (platform === "win32") {
|
|
104
|
+
const hash = createHash("sha256").update(String(instanceId)).digest("hex").slice(0, 8);
|
|
105
|
+
return `\\\\.\\pipe\\pi-agent-board-${viewId}-${hash}`;
|
|
106
|
+
}
|
|
107
|
+
return path.join(viewDir(root, viewId), `control.${instanceId}.sock`);
|
|
108
|
+
}
|
|
109
|
+
/** @param {string} root @param {string} viewId */
|
|
110
|
+
export const screenLogPath = (root, viewId) => path.join(viewDir(root, viewId), "screen.log");
|
|
111
|
+
/**
|
|
112
|
+
* Well-known endpoint for the board-root View State Coordinator (issue #91, spec D3).
|
|
113
|
+
* Exactly one coordinator may own a root (token-fenced lease), so one stable path
|
|
114
|
+
* suffices; a stale POSIX socket left by a crashed coordinator is unlinked by the
|
|
115
|
+
* new lease owner before bind. win32 pipe names embed a 16-hex hash of the
|
|
116
|
+
* *normalized* root to stay under the 256-char limit and keep per-root isolation.
|
|
117
|
+
* @param {"win32"|"linux"|"darwin"} platform
|
|
118
|
+
* @param {string} root
|
|
119
|
+
*/
|
|
120
|
+
export function coordinatorEndpointPathFor(platform, root) {
|
|
121
|
+
if (platform === "win32") {
|
|
122
|
+
// Normalize before hashing: the pipe name must be invariant to the root's
|
|
123
|
+
// spelling (C:/x vs C:\x, trailing separators, dot segments). The lock path
|
|
124
|
+
// derived from the same root already is (via path.join); a mismatch yields
|
|
125
|
+
// "same lock, two pipes" — the panel probes a pipe nobody bound, spawns
|
|
126
|
+
// replacements that cannot take the held lease, and locks itself out
|
|
127
|
+
// (issue #124). resolve() is idempotent on canonical roots, so coordinators
|
|
128
|
+
// already deployed keep their pipe name and need no restart.
|
|
129
|
+
// win32 semantics are named explicitly rather than using the ambient
|
|
130
|
+
// path.resolve: this branch must emit the same pipe name on every host OS
|
|
131
|
+
// (on POSIX, ambient resolve treats `C:\x` as a relative path: no
|
|
132
|
+
// drive-letter or backslash-separator semantics), so platform-injected
|
|
133
|
+
// tests and CI behave identically everywhere.
|
|
134
|
+
const normalized = path.win32.resolve(root);
|
|
135
|
+
const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
136
|
+
return `\\\\.\\pipe\\agent-board-coordinator-${hash}`;
|
|
137
|
+
}
|
|
138
|
+
return path.join(root, "coordinator.sock");
|
|
139
|
+
}
|
|
140
|
+
/** @param {string} root @param {string} viewId */
|
|
141
|
+
export const hostPidPath = (root, viewId) => path.join(viewDir(root, viewId), "host-pid.json");
|
|
142
|
+
/** @param {string} root @param {string} viewId */
|
|
143
|
+
export const runsDir = (root, viewId) => path.join(viewDir(root, viewId), "runs");
|
|
144
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
145
|
+
export const runDir = (root, viewId, runId) => path.join(runsDir(root, viewId), runId);
|
|
146
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
147
|
+
export const statusPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "status.json");
|
|
148
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
149
|
+
export const eventsPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "events.jsonl");
|
|
150
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
151
|
+
export const stdoutPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "stdout.log");
|
|
152
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
153
|
+
export const stderrPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "stderr.log");
|
|
154
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
155
|
+
export const pidPath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "pid.json");
|
|
156
|
+
/** @param {string} root @param {string} viewId */
|
|
157
|
+
export const diagnosticsPath = (root, viewId) => path.join(viewDir(root, viewId), "diagnostics.jsonl");
|
|
158
|
+
/** @param {string} root @param {string} viewId */
|
|
159
|
+
export const evidencePath = (root, viewId) => path.join(viewDir(root, viewId), "evidence.json");
|
|
160
|
+
/** @param {string} root @param {string} viewId */
|
|
161
|
+
export const codeRefsPath = (root, viewId) => path.join(viewDir(root, viewId), "github.json");
|
|
162
|
+
/** @param {string} root @param {string} viewId */
|
|
163
|
+
export const viewEvidencePath = evidencePath;
|
|
164
|
+
/** @param {string} root @param {string} viewId */
|
|
165
|
+
export const followUpQueuePath = (root, viewId) => path.join(viewDir(root, viewId), "queue.json");
|
|
166
|
+
/** @param {string} root @param {string} viewId */
|
|
167
|
+
export const queuePath = followUpQueuePath;
|
|
168
|
+
/** @param {string} root @param {string} viewId */
|
|
169
|
+
export const steeringPath = (root, viewId) => path.join(viewDir(root, viewId), "steering.json");
|
|
170
|
+
/** @param {string} root @param {string} viewId @param {string} name */
|
|
171
|
+
export const viewLockPath = (root, viewId, name) => path.join(viewDir(root, viewId), `${name}.lock`);
|
|
172
|
+
/** @param {string} root @param {string} viewId @param {string} runId */
|
|
173
|
+
export const runEvidencePath = (root, viewId, runId) => path.join(runDir(root, viewId, runId), "evidence.json");
|
|
174
|
+
|
|
175
|
+
/** @param {string} root */
|
|
176
|
+
export const sessionsDir = (root) => path.join(root, "sessions");
|
|
177
|
+
/** @param {string} root @param {string} viewId */
|
|
178
|
+
export const sessionFilePath = (root, viewId) => path.join(sessionsDir(root), `${viewId}.jsonl`);
|
|
179
|
+
|
|
180
|
+
/** @param {string} root */
|
|
181
|
+
export const worktreesDir = (root) => path.join(root, "worktrees");
|
|
182
|
+
/** @param {string} root @param {string} viewId */
|
|
183
|
+
export const worktreePath = (root, viewId) => path.join(worktreesDir(root), viewId);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Process liveness checks and process identity capture. */
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether `pid` refers to a live process.
|
|
6
|
+
* `process.kill(pid, 0)` throws ESRCH when the process is gone, and EPERM when it
|
|
7
|
+
* exists but we lack permission to signal it — EPERM still means "alive".
|
|
8
|
+
* @param {number|null|undefined} pid
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function isAlive(pid) {
|
|
12
|
+
if (!pid || pid <= 0) return false;
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
return /** @type {NodeJS.ErrnoException} */ (err).code === "EPERM";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Try to terminate a process tree gently, then force after `graceMs`.
|
|
23
|
+
* Safe no-op if already dead.
|
|
24
|
+
* @param {number|null|undefined} pid
|
|
25
|
+
* @param {number} [graceMs]
|
|
26
|
+
*/
|
|
27
|
+
export function killProcess(pid, graceMs = 4000) {
|
|
28
|
+
if (!isAlive(pid)) return;
|
|
29
|
+
try {
|
|
30
|
+
process.kill(/** @type {number} */ (pid), "SIGTERM");
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
if (isAlive(pid)) {
|
|
36
|
+
try {
|
|
37
|
+
process.kill(/** @type {number} */ (pid), "SIGKILL");
|
|
38
|
+
} catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, graceMs).unref?.();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* POSIX process start token — /proc/<pid>/stat field 22 (starttime), stable
|
|
47
|
+
* across exec(2). Distinguishes an owned-live pid from a reused one; null on
|
|
48
|
+
* failure or non-Linux platforms. This is the single synchronous implementation
|
|
49
|
+
* (issue #238 removed the runner copies); the cross-platform async reader in
|
|
50
|
+
* process-identity-probe.mjs layers a win32 creation-time token on top, and
|
|
51
|
+
* host-meta lease identity uses this function directly (issue #112).
|
|
52
|
+
* @param {number|null|undefined} pid
|
|
53
|
+
* @returns {string|null}
|
|
54
|
+
*/
|
|
55
|
+
export function captureStartToken(pid) {
|
|
56
|
+
if (process.platform !== "linux" || !pid) return null;
|
|
57
|
+
try {
|
|
58
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
59
|
+
// comm (field 2) may contain spaces and parens — fields resume AFTER the
|
|
60
|
+
// last ')'. fields[0] is state (field 3) → starttime (field 22) is [19].
|
|
61
|
+
const afterComm = stat.slice(stat.lastIndexOf(")") + 1).trimStart();
|
|
62
|
+
return afterComm.split(/\s+/)[19] ?? null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Launch-time identity for the current process, as stamped on host-meta
|
|
70
|
+
* acquisitions (issue #112).
|
|
71
|
+
* @returns {{pid: number, startToken: string|null}}
|
|
72
|
+
*/
|
|
73
|
+
export function currentProcessIdentity() {
|
|
74
|
+
return { pid: process.pid, startToken: captureStartToken(process.pid) };
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-flight debounce for dashboard prewarm.
|
|
3
|
+
*
|
|
4
|
+
* Arrow-key navigation must move the selection and repaint immediately; host
|
|
5
|
+
* prewarm (which may spawn a PTY host and re-scan rows) is deferred so bursts
|
|
6
|
+
* of keypresses trigger exactly one prewarm for the final resting selection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {() => void} prewarm Invoked (with errors swallowed) once scheduling
|
|
11
|
+
* goes quiet for `delayMs`. Re-reads current state at fire time.
|
|
12
|
+
* @param {number} [delayMs=200]
|
|
13
|
+
* @returns {{ schedule: () => void, cancel: () => void }}
|
|
14
|
+
*/
|
|
15
|
+
export function createPrewarmScheduler(prewarm, delayMs = 200) {
|
|
16
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
17
|
+
let timer = null;
|
|
18
|
+
const fire = () => {
|
|
19
|
+
timer = null;
|
|
20
|
+
try {
|
|
21
|
+
prewarm();
|
|
22
|
+
} catch {
|
|
23
|
+
/* prewarm is best-effort; never break navigation */
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
schedule() {
|
|
28
|
+
if (timer !== null) clearTimeout(timer);
|
|
29
|
+
const pending = setTimeout(fire, delayMs);
|
|
30
|
+
// Never let a pending debounce hold the event loop open on exit.
|
|
31
|
+
if (typeof pending.unref === "function") pending.unref();
|
|
32
|
+
timer = pending;
|
|
33
|
+
},
|
|
34
|
+
cancel() {
|
|
35
|
+
if (timer !== null) {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
timer = null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform process identity probe (issue #217) and start-token reader
|
|
3
|
+
* (issue #238).
|
|
4
|
+
*
|
|
5
|
+
* The probe produces the two corroborating signals — process creation time and
|
|
6
|
+
* command line — that `classifyUnverifiableProcess` judges when a recorded
|
|
7
|
+
* start token is missing. `readProcessStartToken` produces the comparable token
|
|
8
|
+
* itself: linux reads `/proc` and win32 converts the probe's creation time into
|
|
9
|
+
* a `ct:<ms>` token. Pure parsers are exported for exhaustive unit tests;
|
|
10
|
+
* `readProcessIdentityProbe` is the only I/O layer (one async `execFile` per
|
|
11
|
+
* pid, never throwing).
|
|
12
|
+
*
|
|
13
|
+
* `readProcessStartToken` has no darwin path on purpose: `ps` reports elapsed
|
|
14
|
+
* time at second resolution, so two reads of one process drift by up to ~1 s
|
|
15
|
+
* and a token comparison would misclassify a live process as `foreign`.
|
|
16
|
+
* Returning `null` keeps callers on the "unverifiable" path instead of
|
|
17
|
+
* guessing. The probe itself stays off linux: the `/proc` token already
|
|
18
|
+
* identifies processes there, so probing would only add latency.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { captureStartToken } from "./pid.mjs";
|
|
22
|
+
|
|
23
|
+
/** Hard cap on one probe: a hung PowerShell/ps must not stall recovery. */
|
|
24
|
+
export const PROBE_TIMEOUT_MS = 3_000;
|
|
25
|
+
|
|
26
|
+
/** Cap on captured stdout — a command line is small; anything larger is junk. */
|
|
27
|
+
const PROBE_MAX_BUFFER = 64 * 1024;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format an absolute creation time as the comparable `ct:` identity token
|
|
31
|
+
* (issue #238): two reads of the same process yield the same token regardless
|
|
32
|
+
* of when they happen. Only positive safe integers format — anything else
|
|
33
|
+
* (missing creation time, a fractional or out-of-range value, a coerced
|
|
34
|
+
* string) would produce a token that can never match a recorded one.
|
|
35
|
+
* @param {number} creationTimeMs
|
|
36
|
+
* @returns {string|null}
|
|
37
|
+
*/
|
|
38
|
+
export function creationTimeToken(creationTimeMs) {
|
|
39
|
+
if (!Number.isSafeInteger(creationTimeMs) || creationTimeMs <= 0) return null;
|
|
40
|
+
return `ct:${creationTimeMs}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read the comparable start token for a process (issue #238): the token is
|
|
45
|
+
* recorded next to a lease and compared later to tell "same process" from
|
|
46
|
+
* "reused pid".
|
|
47
|
+
*
|
|
48
|
+
* - linux: `captureStartToken(pid)` — `/proc/<pid>/stat` starttime.
|
|
49
|
+
* - win32: probe → `creationTimeToken(creationTimeMs)` — CIM creation time is
|
|
50
|
+
* millisecond-resolution, so two reads of one process are strictly equal.
|
|
51
|
+
* - anything else, including darwin: `null`, and the probe is never called.
|
|
52
|
+
*
|
|
53
|
+
* A pid that is not a positive safe integer yields `null` before any I/O; a
|
|
54
|
+
* probe rejection or unusable result also yields `null` (unverifiable, never
|
|
55
|
+
* "foreign").
|
|
56
|
+
*
|
|
57
|
+
* @param {number} pid
|
|
58
|
+
* @param {{ platform?: string, probe?: typeof readProcessIdentityProbe }} [opts]
|
|
59
|
+
* @returns {Promise<string|null>}
|
|
60
|
+
*/
|
|
61
|
+
export async function readProcessStartToken(pid, {
|
|
62
|
+
platform = process.platform,
|
|
63
|
+
probe = readProcessIdentityProbe,
|
|
64
|
+
} = {}) {
|
|
65
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
66
|
+
if (platform === "linux") return captureStartToken(pid);
|
|
67
|
+
if (platform !== "win32") return null;
|
|
68
|
+
try {
|
|
69
|
+
const observed = await probe(pid);
|
|
70
|
+
return observed ? creationTimeToken(observed.creationTimeMs) : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse an epoch-milliseconds value from CIM JSON output. Accepts decimal
|
|
78
|
+
* digits only: PowerShell can hand back an empty string or a negative number
|
|
79
|
+
* for protected processes, neither of which is a usable creation time.
|
|
80
|
+
* @param {string} text
|
|
81
|
+
* @returns {number|null}
|
|
82
|
+
*/
|
|
83
|
+
export function parseUnixMillis(text) {
|
|
84
|
+
const trimmed = String(text ?? "").trim();
|
|
85
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
86
|
+
const millis = Number(trimmed);
|
|
87
|
+
return Number.isSafeInteger(millis) && millis > 0 ? millis : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse the compressed CIM object emitted for a Windows process:
|
|
92
|
+
* `{"creation":<unix ms>|null,"cmd":"<command line>"|null}`. Malformed JSON or
|
|
93
|
+
* a non-object payload yields `null` (the judgment layer reads that as
|
|
94
|
+
* `unknown`, which is always safe).
|
|
95
|
+
* @param {string} text
|
|
96
|
+
* @returns {{creationTimeMs:number|null, commandLine:string|null}|null}
|
|
97
|
+
*/
|
|
98
|
+
export function parseProbeJson(text) {
|
|
99
|
+
if (typeof text !== "string") return null;
|
|
100
|
+
const trimmed = text.trim();
|
|
101
|
+
if (!trimmed) return null;
|
|
102
|
+
/** @type {any} */ let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(trimmed);
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
109
|
+
return {
|
|
110
|
+
creationTimeMs: parseUnixMillis(parsed.creation),
|
|
111
|
+
commandLine: typeof parsed.cmd === "string" && parsed.cmd.trim() ? parsed.cmd : null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `[DD-]HH:MM:SS` (ps drops leading units it does not need). */
|
|
116
|
+
const PS_ELAPSED_PATTERN = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse a POSIX `ps` elapsed-time field into milliseconds. Handles the three
|
|
120
|
+
* shapes `ps -o etime=` emits: `MM:SS`, `HH:MM:SS`, and `D-HH:MM:SS`.
|
|
121
|
+
* @param {string} text
|
|
122
|
+
* @returns {number|null}
|
|
123
|
+
*/
|
|
124
|
+
export function parsePsElapsedMs(text) {
|
|
125
|
+
const match = PS_ELAPSED_PATTERN.exec(String(text ?? "").trim());
|
|
126
|
+
if (!match) return null;
|
|
127
|
+
const days = match[1] == null ? 0 : Number(match[1]);
|
|
128
|
+
const hours = match[2] == null ? 0 : Number(match[2]);
|
|
129
|
+
const minutes = Number(match[3]);
|
|
130
|
+
const seconds = Number(match[4]);
|
|
131
|
+
if (minutes > 59 || seconds > 59) return null;
|
|
132
|
+
return ((days * 24 + hours) * 3600 + minutes * 60 + seconds) * 1000;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse one `ps -p <pid> -o etime=,command=` line: the first whitespace-
|
|
137
|
+
* separated token is the elapsed time, everything after it is the command.
|
|
138
|
+
* `ps` reports age, not creation time, so callers pass the clock reading.
|
|
139
|
+
* @param {string} text
|
|
140
|
+
* @param {number} nowMs Epoch ms at which the probe output was read.
|
|
141
|
+
* @returns {{creationTimeMs:number|null, commandLine:string|null}|null}
|
|
142
|
+
*/
|
|
143
|
+
export function parsePsProbeLine(text, nowMs) {
|
|
144
|
+
if (typeof text !== "string") return null;
|
|
145
|
+
const trimmed = text.trim();
|
|
146
|
+
if (!trimmed) return null;
|
|
147
|
+
const split = trimmed.search(/\s/);
|
|
148
|
+
const elapsedText = split === -1 ? trimmed : trimmed.slice(0, split);
|
|
149
|
+
const commandLine = split === -1 ? "" : trimmed.slice(split).trim();
|
|
150
|
+
const elapsedMs = parsePsElapsedMs(elapsedText);
|
|
151
|
+
if (elapsedMs == null) return null;
|
|
152
|
+
return {
|
|
153
|
+
creationTimeMs: nowMs - elapsedMs,
|
|
154
|
+
commandLine: commandLine || null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Windows CIM query: one round trip returns creation time (converted to Unix
|
|
160
|
+
* milliseconds in PowerShell so the parent needs no date parsing) and the
|
|
161
|
+
* command line. `-ErrorAction SilentlyContinue` keeps a vanished pid from
|
|
162
|
+
* turning into a non-zero exit; the empty result is handled as "unavailable".
|
|
163
|
+
* @param {number} pid
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
function windowsProbeScript(pid) {
|
|
167
|
+
return "$p = Get-CimInstance Win32_Process -Filter \"ProcessId=" + pid + "\" -ErrorAction SilentlyContinue; "
|
|
168
|
+
+ "if ($p) { [pscustomobject]@{creation=[long]([datetimeoffset]$p.CreationDate).ToUnixTimeMilliseconds(); cmd=$p.CommandLine} | ConvertTo-Json -Compress }";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve the platform-specific probe command. Platforms without a probe
|
|
173
|
+
* (`linux` and everything else) and invalid pids return `null`, so callers
|
|
174
|
+
* never spawn anything for them.
|
|
175
|
+
* @param {string} platform
|
|
176
|
+
* @param {number} pid
|
|
177
|
+
* @returns {{command:string, args:string[]}|null}
|
|
178
|
+
*/
|
|
179
|
+
export function resolveProbeCommand(platform, pid) {
|
|
180
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
181
|
+
if (platform === "win32") {
|
|
182
|
+
return {
|
|
183
|
+
command: "powershell.exe",
|
|
184
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", windowsProbeScript(pid)],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (platform === "darwin") {
|
|
188
|
+
return { command: "ps", args: ["-p", String(pid), "-o", "etime=,command="] };
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Read `{ creationTimeMs, commandLine }` for `pid`, or `null` when the process
|
|
195
|
+
* cannot be observed (unsupported platform, invalid pid, non-zero exit,
|
|
196
|
+
* timeout, or empty output). `null` means "unverifiable", never "foreign".
|
|
197
|
+
*
|
|
198
|
+
* `execFileFn` is injectable for tests; the default is Node's async
|
|
199
|
+
* `execFile`. `windowsHide` mirrors `launchRun`'s detached spawn (issue #49):
|
|
200
|
+
* without it a console window flashes on Windows 11 25H2.
|
|
201
|
+
*
|
|
202
|
+
* @param {number} pid
|
|
203
|
+
* @param {{ platform?: string, execFileFn?: typeof execFile, timeoutMs?: number, nowMs?: () => number }} [opts]
|
|
204
|
+
* @returns {Promise<{creationTimeMs:number|null, commandLine:string|null}|null>}
|
|
205
|
+
*/
|
|
206
|
+
export async function readProcessIdentityProbe(pid, {
|
|
207
|
+
platform = process.platform,
|
|
208
|
+
execFileFn = execFile,
|
|
209
|
+
timeoutMs = PROBE_TIMEOUT_MS,
|
|
210
|
+
nowMs = Date.now,
|
|
211
|
+
} = {}) {
|
|
212
|
+
const resolved = resolveProbeCommand(platform, pid);
|
|
213
|
+
if (!resolved) return null;
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
execFileFn(resolved.command, resolved.args, { timeout: timeoutMs, windowsHide: true, maxBuffer: PROBE_MAX_BUFFER }, (error, stdout) => {
|
|
216
|
+
if (error) {
|
|
217
|
+
resolve(null);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const text = String(stdout ?? "").trim();
|
|
221
|
+
if (!text) {
|
|
222
|
+
resolve(null);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
resolve(platform === "win32" ? parseProbeJson(text) : parsePsProbeLine(text, nowMs()));
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Helpers for safely delivering prompts to Pi child processes. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Protect a prompt that is delivered as a positional CLI argument.
|
|
5
|
+
* Pi parses argv before it knows which tokens are prompt text, so a leading `-`
|
|
6
|
+
* would be treated as another flag and abort the run.
|
|
7
|
+
* @param {string} text
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function encodePromptForCliArg(text) {
|
|
11
|
+
const raw = String(text ?? "");
|
|
12
|
+
return /^\s*-/.test(raw) ? ` ${raw}` : raw;
|
|
13
|
+
}
|