@jmoyers/harness 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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/native/ptyd/Cargo.lock +16 -0
- package/native/ptyd/Cargo.toml +7 -0
- package/native/ptyd/src/main.rs +257 -0
- package/package.json +90 -0
- package/scripts/build-ptyd.sh +73 -0
- package/scripts/control-plane-daemon.ts +277 -0
- package/scripts/cursor-hook-relay.ts +82 -0
- package/scripts/harness-animate.ts +469 -0
- package/scripts/harness-bin.js +77 -0
- package/scripts/harness-core.ts +1 -0
- package/scripts/harness-inspector.ts +439 -0
- package/scripts/harness.ts +2493 -0
- package/src/adapters/agent-session-state.ts +390 -0
- package/src/cli/gateway-record.ts +173 -0
- package/src/codex/live-session.ts +872 -0
- package/src/config/config-core.ts +1359 -0
- package/src/config/secrets-core.ts +170 -0
- package/src/control-plane/agent-realtime-api.ts +2441 -0
- package/src/control-plane/codex-session-stream.ts +392 -0
- package/src/control-plane/codex-telemetry.ts +1325 -0
- package/src/control-plane/lifecycle-hooks.ts +706 -0
- package/src/control-plane/session-summary.ts +380 -0
- package/src/control-plane/status/agent-status-reducer.ts +21 -0
- package/src/control-plane/status/reducer-base.ts +170 -0
- package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
- package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
- package/src/control-plane/status/session-status-engine.ts +76 -0
- package/src/control-plane/stream-client.ts +396 -0
- package/src/control-plane/stream-command-parser.ts +1673 -0
- package/src/control-plane/stream-protocol.ts +1808 -0
- package/src/control-plane/stream-server-background.ts +486 -0
- package/src/control-plane/stream-server-command.ts +2557 -0
- package/src/control-plane/stream-server-connection.ts +234 -0
- package/src/control-plane/stream-server-observed-filter.ts +112 -0
- package/src/control-plane/stream-server-session-runtime.ts +566 -0
- package/src/control-plane/stream-server-state-store.ts +15 -0
- package/src/control-plane/stream-server.ts +3192 -0
- package/src/cursor/managed-hooks.ts +282 -0
- package/src/domain/conversations.ts +414 -0
- package/src/domain/directories.ts +78 -0
- package/src/domain/repositories.ts +123 -0
- package/src/domain/tasks.ts +148 -0
- package/src/domain/workspace.ts +156 -0
- package/src/events/normalized-events.ts +124 -0
- package/src/mux/ansi-integrity.ts +103 -0
- package/src/mux/control-plane-op-queue.ts +212 -0
- package/src/mux/conversation-rail.ts +339 -0
- package/src/mux/double-click.ts +78 -0
- package/src/mux/dual-pane-core.ts +435 -0
- package/src/mux/harness-core-ui.ts +817 -0
- package/src/mux/input-shortcuts.ts +667 -0
- package/src/mux/live-mux/actions-conversation.ts +344 -0
- package/src/mux/live-mux/actions-repository.ts +246 -0
- package/src/mux/live-mux/actions-task.ts +115 -0
- package/src/mux/live-mux/args.ts +142 -0
- package/src/mux/live-mux/command-menu.ts +298 -0
- package/src/mux/live-mux/control-plane-records.ts +546 -0
- package/src/mux/live-mux/conversation-state.ts +188 -0
- package/src/mux/live-mux/directory-resolution.ts +34 -0
- package/src/mux/live-mux/event-mapping.ts +96 -0
- package/src/mux/live-mux/gateway-profiler.ts +152 -0
- package/src/mux/live-mux/gateway-render-trace.ts +177 -0
- package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
- package/src/mux/live-mux/git-parsing.ts +131 -0
- package/src/mux/live-mux/git-snapshot.ts +263 -0
- package/src/mux/live-mux/git-state.ts +136 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
- package/src/mux/live-mux/home-pane-actions.ts +58 -0
- package/src/mux/live-mux/home-pane-drop.ts +44 -0
- package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
- package/src/mux/live-mux/home-pane-pointer.ts +96 -0
- package/src/mux/live-mux/input-forwarding.ts +112 -0
- package/src/mux/live-mux/layout.ts +30 -0
- package/src/mux/live-mux/left-nav-activation.ts +103 -0
- package/src/mux/live-mux/left-nav.ts +85 -0
- package/src/mux/live-mux/left-rail-actions.ts +118 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
- package/src/mux/live-mux/left-rail-pointer.ts +74 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
- package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
- package/src/mux/live-mux/modal-input-reducers.ts +94 -0
- package/src/mux/live-mux/modal-overlays.ts +287 -0
- package/src/mux/live-mux/modal-pointer.ts +70 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
- package/src/mux/live-mux/observed-stream.ts +87 -0
- package/src/mux/live-mux/palette-parsing.ts +128 -0
- package/src/mux/live-mux/pointer-routing.ts +108 -0
- package/src/mux/live-mux/process-usage.ts +53 -0
- package/src/mux/live-mux/project-pane-pointer.ts +44 -0
- package/src/mux/live-mux/rail-layout.ts +244 -0
- package/src/mux/live-mux/render-trace-analysis.ts +213 -0
- package/src/mux/live-mux/render-trace-state.ts +84 -0
- package/src/mux/live-mux/repository-folding.ts +207 -0
- package/src/mux/live-mux/runtime-shutdown.ts +51 -0
- package/src/mux/live-mux/selection.ts +411 -0
- package/src/mux/live-mux/startup-utils.ts +187 -0
- package/src/mux/live-mux/status-timeline-state.ts +82 -0
- package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
- package/src/mux/live-mux/terminal-palette.ts +79 -0
- package/src/mux/new-thread-prompt.ts +165 -0
- package/src/mux/project-tree.ts +295 -0
- package/src/mux/render-frame.ts +113 -0
- package/src/mux/runtime-wiring.ts +185 -0
- package/src/mux/selector-index.ts +160 -0
- package/src/mux/startup-sequencer.ts +238 -0
- package/src/mux/task-composer.ts +289 -0
- package/src/mux/task-focused-pane.ts +417 -0
- package/src/mux/task-screen-keybindings.ts +539 -0
- package/src/mux/terminal-input-modes.ts +35 -0
- package/src/mux/workspace-path.ts +55 -0
- package/src/mux/workspace-rail-model.ts +701 -0
- package/src/mux/workspace-rail.ts +247 -0
- package/src/perf/perf-core.ts +307 -0
- package/src/pty/pty_host.ts +217 -0
- package/src/pty/session-broker.ts +158 -0
- package/src/recording/terminal-recording.ts +383 -0
- package/src/services/control-plane.ts +567 -0
- package/src/services/conversation-lifecycle.ts +176 -0
- package/src/services/conversation-startup-hydration.ts +47 -0
- package/src/services/directory-hydration.ts +49 -0
- package/src/services/event-persistence.ts +104 -0
- package/src/services/mux-ui-state-persistence.ts +82 -0
- package/src/services/output-load-sampler.ts +231 -0
- package/src/services/process-usage-refresh.ts +88 -0
- package/src/services/recording.ts +75 -0
- package/src/services/render-trace-recorder.ts +177 -0
- package/src/services/runtime-control-actions.ts +123 -0
- package/src/services/runtime-control-plane-ops.ts +131 -0
- package/src/services/runtime-conversation-actions.ts +113 -0
- package/src/services/runtime-conversation-activation.ts +78 -0
- package/src/services/runtime-conversation-starter.ts +171 -0
- package/src/services/runtime-conversation-title-edit.ts +149 -0
- package/src/services/runtime-directory-actions.ts +164 -0
- package/src/services/runtime-envelope-handler.ts +198 -0
- package/src/services/runtime-git-state.ts +92 -0
- package/src/services/runtime-input-pipeline.ts +50 -0
- package/src/services/runtime-input-router.ts +202 -0
- package/src/services/runtime-layout-resize.ts +236 -0
- package/src/services/runtime-left-rail-render.ts +159 -0
- package/src/services/runtime-main-pane-input.ts +230 -0
- package/src/services/runtime-modal-input.ts +119 -0
- package/src/services/runtime-navigation-input.ts +207 -0
- package/src/services/runtime-process-wiring.ts +68 -0
- package/src/services/runtime-rail-input.ts +287 -0
- package/src/services/runtime-render-flush.ts +146 -0
- package/src/services/runtime-render-lifecycle.ts +104 -0
- package/src/services/runtime-render-orchestrator.ts +108 -0
- package/src/services/runtime-render-pipeline.ts +167 -0
- package/src/services/runtime-render-state.ts +72 -0
- package/src/services/runtime-repository-actions.ts +197 -0
- package/src/services/runtime-right-pane-render.ts +132 -0
- package/src/services/runtime-shutdown.ts +79 -0
- package/src/services/runtime-stream-subscriptions.ts +56 -0
- package/src/services/runtime-task-composer-persistence.ts +139 -0
- package/src/services/runtime-task-editor-actions.ts +83 -0
- package/src/services/runtime-task-pane-actions.ts +198 -0
- package/src/services/runtime-task-pane-shortcuts.ts +189 -0
- package/src/services/runtime-task-pane.ts +62 -0
- package/src/services/runtime-workspace-actions.ts +153 -0
- package/src/services/runtime-workspace-observed-events.ts +190 -0
- package/src/services/session-projection-instrumentation.ts +190 -0
- package/src/services/startup-background-probe.ts +91 -0
- package/src/services/startup-background-resume.ts +65 -0
- package/src/services/startup-orchestrator.ts +166 -0
- package/src/services/startup-output-tracker.ts +54 -0
- package/src/services/startup-paint-tracker.ts +115 -0
- package/src/services/startup-persisted-conversation-queue.ts +45 -0
- package/src/services/startup-settled-gate.ts +67 -0
- package/src/services/startup-shutdown.ts +53 -0
- package/src/services/startup-span-tracker.ts +77 -0
- package/src/services/startup-state-hydration.ts +94 -0
- package/src/services/startup-visibility.ts +35 -0
- package/src/services/status-timeline-recorder.ts +144 -0
- package/src/services/task-pane-selection-actions.ts +153 -0
- package/src/services/task-planning-hydration.ts +58 -0
- package/src/services/task-planning-observed-events.ts +89 -0
- package/src/services/workspace-observed-events.ts +113 -0
- package/src/store/control-plane-store-normalize.ts +760 -0
- package/src/store/control-plane-store-types.ts +224 -0
- package/src/store/control-plane-store.ts +2951 -0
- package/src/store/event-store.ts +253 -0
- package/src/store/sqlite.ts +81 -0
- package/src/terminal/compat-matrix.ts +345 -0
- package/src/terminal/differential-checkpoints.ts +132 -0
- package/src/terminal/parity-suite.ts +441 -0
- package/src/terminal/snapshot-oracle.ts +1840 -0
- package/src/ui/conversation-input-forwarder.ts +114 -0
- package/src/ui/conversation-selection-input.ts +103 -0
- package/src/ui/debug-footer-notice.ts +39 -0
- package/src/ui/global-shortcut-input.ts +126 -0
- package/src/ui/input-preflight.ts +68 -0
- package/src/ui/input-token-router.ts +312 -0
- package/src/ui/input.ts +238 -0
- package/src/ui/kit.ts +509 -0
- package/src/ui/left-nav-input.ts +80 -0
- package/src/ui/left-rail-pointer-input.ts +148 -0
- package/src/ui/main-pane-pointer-input.ts +150 -0
- package/src/ui/modals/manager.ts +192 -0
- package/src/ui/mux-theme.ts +529 -0
- package/src/ui/panes/conversation.ts +19 -0
- package/src/ui/panes/home-gridfire.ts +302 -0
- package/src/ui/panes/home.ts +109 -0
- package/src/ui/panes/left-rail.ts +12 -0
- package/src/ui/panes/project.ts +44 -0
- package/src/ui/pointer-routing-input.ts +158 -0
- package/src/ui/repository-fold-input.ts +91 -0
- package/src/ui/screen.ts +210 -0
- package/src/ui/surface.ts +224 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { loadHarnessConfig } from '../src/config/config-core.ts';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PROFILE_INSPECT_TIMEOUT_MS = 5000;
|
|
5
|
+
const PROFILE_RUNTIME_STATE_KEY = '__HARNESS_GATEWAY_CPU_PROFILE_STATE__';
|
|
6
|
+
|
|
7
|
+
interface InspectorPendingCommand {
|
|
8
|
+
method: string;
|
|
9
|
+
resolve: (result: Record<string, unknown>) => void;
|
|
10
|
+
reject: (error: Error) => void;
|
|
11
|
+
timeoutHandle: ReturnType<typeof setTimeout>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class InspectorWebSocketClient {
|
|
15
|
+
private readonly pending = new Map<number, InspectorPendingCommand>();
|
|
16
|
+
private nextId = 1;
|
|
17
|
+
private closed = false;
|
|
18
|
+
|
|
19
|
+
private constructor(
|
|
20
|
+
private readonly socket: WebSocket,
|
|
21
|
+
private readonly endpoint: string,
|
|
22
|
+
) {
|
|
23
|
+
socket.addEventListener('message', (event) => {
|
|
24
|
+
const payload = this.parseMessagePayload(event.data);
|
|
25
|
+
if (payload === null) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const idValue = payload['id'];
|
|
29
|
+
if (typeof idValue !== 'number') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const pending = this.pending.get(idValue);
|
|
33
|
+
if (pending === undefined) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.pending.delete(idValue);
|
|
37
|
+
clearTimeout(pending.timeoutHandle);
|
|
38
|
+
const errorValue = payload['error'];
|
|
39
|
+
if (typeof errorValue === 'object' && errorValue !== null) {
|
|
40
|
+
const code = (errorValue as Record<string, unknown>)['code'];
|
|
41
|
+
const message = (errorValue as Record<string, unknown>)['message'];
|
|
42
|
+
const codeText = typeof code === 'number' ? String(code) : 'unknown';
|
|
43
|
+
const messageText = typeof message === 'string' ? message : 'unknown inspector error';
|
|
44
|
+
pending.reject(new Error(`${pending.method} failed (${codeText}): ${messageText}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const resultValue = payload['result'];
|
|
48
|
+
if (typeof resultValue !== 'object' || resultValue === null) {
|
|
49
|
+
pending.resolve({});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
pending.resolve(resultValue as Record<string, unknown>);
|
|
53
|
+
});
|
|
54
|
+
socket.addEventListener('error', () => {
|
|
55
|
+
this.closeWithError(new Error(`inspector websocket error (${this.endpoint})`));
|
|
56
|
+
});
|
|
57
|
+
socket.addEventListener('close', () => {
|
|
58
|
+
this.closeWithError(new Error(`inspector websocket closed (${this.endpoint})`));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static async connect(endpoint: string, timeoutMs: number): Promise<InspectorWebSocketClient> {
|
|
63
|
+
return await new Promise<InspectorWebSocketClient>((resolveClient, rejectClient) => {
|
|
64
|
+
let settled = false;
|
|
65
|
+
const socket = new WebSocket(endpoint);
|
|
66
|
+
const timeoutHandle = setTimeout(() => {
|
|
67
|
+
if (settled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
settled = true;
|
|
71
|
+
try {
|
|
72
|
+
socket.close();
|
|
73
|
+
} catch {
|
|
74
|
+
// Best-effort cleanup only.
|
|
75
|
+
}
|
|
76
|
+
rejectClient(new Error(`inspector websocket connect timeout (${endpoint})`));
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
|
|
79
|
+
const onOpen = (): void => {
|
|
80
|
+
if (settled) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
settled = true;
|
|
84
|
+
clearTimeout(timeoutHandle);
|
|
85
|
+
resolveClient(new InspectorWebSocketClient(socket, endpoint));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const onError = (): void => {
|
|
89
|
+
if (settled) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
settled = true;
|
|
93
|
+
clearTimeout(timeoutHandle);
|
|
94
|
+
rejectClient(new Error(`inspector websocket connect failed (${endpoint})`));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
socket.addEventListener('open', onOpen, { once: true });
|
|
98
|
+
socket.addEventListener('error', onError, { once: true });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async sendCommand(
|
|
103
|
+
method: string,
|
|
104
|
+
params: Record<string, unknown> = {},
|
|
105
|
+
timeoutMs = DEFAULT_PROFILE_INSPECT_TIMEOUT_MS,
|
|
106
|
+
): Promise<Record<string, unknown>> {
|
|
107
|
+
if (this.closed) {
|
|
108
|
+
throw new Error(`inspector websocket is closed (${this.endpoint})`);
|
|
109
|
+
}
|
|
110
|
+
const id = this.nextId;
|
|
111
|
+
this.nextId += 1;
|
|
112
|
+
return await new Promise<Record<string, unknown>>((resolveCommand, rejectCommand) => {
|
|
113
|
+
const timeoutHandle = setTimeout(() => {
|
|
114
|
+
this.pending.delete(id);
|
|
115
|
+
rejectCommand(
|
|
116
|
+
new Error(`${method} timed out after ${String(timeoutMs)}ms (${this.endpoint})`),
|
|
117
|
+
);
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
this.pending.set(id, {
|
|
120
|
+
method,
|
|
121
|
+
resolve: resolveCommand,
|
|
122
|
+
reject: rejectCommand,
|
|
123
|
+
timeoutHandle,
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
127
|
+
} catch (error: unknown) {
|
|
128
|
+
this.pending.delete(id);
|
|
129
|
+
clearTimeout(timeoutHandle);
|
|
130
|
+
rejectCommand(error instanceof Error ? error : new Error(String(error)));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
close(): void {
|
|
136
|
+
if (this.closed) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.closed = true;
|
|
140
|
+
try {
|
|
141
|
+
this.socket.close();
|
|
142
|
+
} catch {
|
|
143
|
+
// Best-effort cleanup only.
|
|
144
|
+
}
|
|
145
|
+
this.closeWithError(new Error(`inspector websocket closed (${this.endpoint})`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private closeWithError(error: Error): void {
|
|
149
|
+
if (this.closed && this.pending.size === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.closed = true;
|
|
153
|
+
for (const pending of this.pending.values()) {
|
|
154
|
+
clearTimeout(pending.timeoutHandle);
|
|
155
|
+
pending.reject(error);
|
|
156
|
+
}
|
|
157
|
+
this.pending.clear();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private parseMessagePayload(rawData: unknown): Record<string, unknown> | null {
|
|
161
|
+
const rawText = typeof rawData === 'string' ? rawData : String(rawData);
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(rawText) as unknown;
|
|
164
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return parsed as Record<string, unknown>;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface InspectorProfileState {
|
|
175
|
+
status: string;
|
|
176
|
+
error: string | null;
|
|
177
|
+
written: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function buildInspectorProfileStartExpression(): string {
|
|
181
|
+
return `(() => {
|
|
182
|
+
const key = ${JSON.stringify(PROFILE_RUNTIME_STATE_KEY)};
|
|
183
|
+
const current = globalThis[key];
|
|
184
|
+
if (current !== undefined && current !== null) {
|
|
185
|
+
const status = typeof current.status === 'string' ? current.status : 'unknown';
|
|
186
|
+
if (status === 'starting' || status === 'running' || status === 'stopping') {
|
|
187
|
+
return JSON.stringify({ ok: false, reason: status });
|
|
188
|
+
}
|
|
189
|
+
if (current.session) {
|
|
190
|
+
try { current.session.disconnect(); } catch {}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const state = {
|
|
194
|
+
status: 'starting',
|
|
195
|
+
error: null,
|
|
196
|
+
written: false,
|
|
197
|
+
session: null,
|
|
198
|
+
};
|
|
199
|
+
globalThis[key] = state;
|
|
200
|
+
import('node:inspector').then((inspectorMod) => {
|
|
201
|
+
const inspector = inspectorMod.default ?? inspectorMod;
|
|
202
|
+
const session = new inspector.Session();
|
|
203
|
+
state.session = session;
|
|
204
|
+
session.connect();
|
|
205
|
+
session.post('Profiler.enable', (enableError) => {
|
|
206
|
+
if (enableError) {
|
|
207
|
+
state.status = 'failed';
|
|
208
|
+
state.error = String(enableError);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
session.post('Profiler.start', (startError) => {
|
|
212
|
+
if (startError) {
|
|
213
|
+
state.status = 'failed';
|
|
214
|
+
state.error = String(startError);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
state.status = 'running';
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}).catch((error) => {
|
|
221
|
+
state.status = 'failed';
|
|
222
|
+
state.error = String(error);
|
|
223
|
+
});
|
|
224
|
+
return JSON.stringify({ ok: true });
|
|
225
|
+
})()`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildInspectorProfileStopExpression(
|
|
229
|
+
gatewayProfilePath: string,
|
|
230
|
+
gatewayProfileDir: string,
|
|
231
|
+
): string {
|
|
232
|
+
return `(() => {
|
|
233
|
+
const key = ${JSON.stringify(PROFILE_RUNTIME_STATE_KEY)};
|
|
234
|
+
const targetPath = ${JSON.stringify(gatewayProfilePath)};
|
|
235
|
+
const targetDir = ${JSON.stringify(gatewayProfileDir)};
|
|
236
|
+
const state = globalThis[key];
|
|
237
|
+
if (state === undefined || state === null) {
|
|
238
|
+
return JSON.stringify({ ok: false, reason: 'missing' });
|
|
239
|
+
}
|
|
240
|
+
if (state.status !== 'running' || !state.session) {
|
|
241
|
+
return JSON.stringify({ ok: false, reason: String(state.status ?? 'unknown') });
|
|
242
|
+
}
|
|
243
|
+
state.status = 'stopping';
|
|
244
|
+
state.error = null;
|
|
245
|
+
state.written = false;
|
|
246
|
+
state.session.post('Profiler.stop', (stopError, stopResult) => {
|
|
247
|
+
if (stopError) {
|
|
248
|
+
state.status = 'failed';
|
|
249
|
+
state.error = String(stopError);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const profile = stopResult?.profile;
|
|
253
|
+
if (profile === undefined) {
|
|
254
|
+
state.status = 'failed';
|
|
255
|
+
state.error = 'missing profile payload';
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
import('node:fs').then((fs) => {
|
|
259
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
260
|
+
fs.writeFileSync(targetPath, JSON.stringify(profile) + '\\n', 'utf8');
|
|
261
|
+
state.written = true;
|
|
262
|
+
state.status = 'stopped';
|
|
263
|
+
try { state.session.disconnect(); } catch {}
|
|
264
|
+
state.session = null;
|
|
265
|
+
}).catch((error) => {
|
|
266
|
+
state.status = 'failed';
|
|
267
|
+
state.error = String(error);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return JSON.stringify({ ok: true });
|
|
271
|
+
})()`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildInspectorProfileStatusExpression(): string {
|
|
275
|
+
return `(() => {
|
|
276
|
+
const key = ${JSON.stringify(PROFILE_RUNTIME_STATE_KEY)};
|
|
277
|
+
const state = globalThis[key];
|
|
278
|
+
if (state === undefined || state === null) {
|
|
279
|
+
return JSON.stringify(null);
|
|
280
|
+
}
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
status: typeof state.status === 'string' ? state.status : 'unknown',
|
|
283
|
+
error: typeof state.error === 'string' ? state.error : null,
|
|
284
|
+
written: state.written === true,
|
|
285
|
+
});
|
|
286
|
+
})()`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeInspectorProfileState(rawValue: unknown): InspectorProfileState | null {
|
|
290
|
+
if (typeof rawValue !== 'string') {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
let parsed: unknown;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(rawValue) as unknown;
|
|
296
|
+
} catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const candidate = parsed as Record<string, unknown>;
|
|
303
|
+
const status = candidate['status'];
|
|
304
|
+
const error = candidate['error'];
|
|
305
|
+
const written = candidate['written'];
|
|
306
|
+
if (typeof status !== 'string') {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
if (error !== null && typeof error !== 'string') {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (typeof written !== 'boolean') {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
status,
|
|
317
|
+
error,
|
|
318
|
+
written,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function evaluateInspectorExpression(
|
|
323
|
+
client: InspectorWebSocketClient,
|
|
324
|
+
expression: string,
|
|
325
|
+
timeoutMs: number,
|
|
326
|
+
): Promise<unknown> {
|
|
327
|
+
const result = await client.sendCommand(
|
|
328
|
+
'Runtime.evaluate',
|
|
329
|
+
{
|
|
330
|
+
expression,
|
|
331
|
+
returnByValue: true,
|
|
332
|
+
},
|
|
333
|
+
timeoutMs,
|
|
334
|
+
);
|
|
335
|
+
const wasThrown = result['wasThrown'];
|
|
336
|
+
if (wasThrown === true) {
|
|
337
|
+
const exceptionDetails = result['exceptionDetails'];
|
|
338
|
+
if (typeof exceptionDetails === 'object' && exceptionDetails !== null) {
|
|
339
|
+
const text = (exceptionDetails as Record<string, unknown>)['text'];
|
|
340
|
+
if (typeof text === 'string' && text.length > 0) {
|
|
341
|
+
throw new Error(`inspector runtime evaluate failed: ${text}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
throw new Error('inspector runtime evaluate failed');
|
|
345
|
+
}
|
|
346
|
+
const remoteValue = result['result'];
|
|
347
|
+
if (typeof remoteValue !== 'object' || remoteValue === null) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return (remoteValue as Record<string, unknown>)['value'];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function readInspectorProfileState(
|
|
354
|
+
client: InspectorWebSocketClient,
|
|
355
|
+
timeoutMs: number,
|
|
356
|
+
): Promise<InspectorProfileState | null> {
|
|
357
|
+
const rawState = await evaluateInspectorExpression(
|
|
358
|
+
client,
|
|
359
|
+
buildInspectorProfileStatusExpression(),
|
|
360
|
+
timeoutMs,
|
|
361
|
+
);
|
|
362
|
+
return normalizeInspectorProfileState(rawState);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function parseInspectorWebSocketUrlsFromGatewayLog(logPath: string): readonly string[] {
|
|
366
|
+
if (!existsSync(logPath)) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
let logText = '';
|
|
370
|
+
try {
|
|
371
|
+
logText = readFileSync(logPath, 'utf8');
|
|
372
|
+
} catch {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
const matches = logText.match(/ws:\/\/[^\s]+/gu) ?? [];
|
|
376
|
+
const urls: string[] = [];
|
|
377
|
+
for (let index = matches.length - 1; index >= 0; index -= 1) {
|
|
378
|
+
const rawUrl = matches[index]!;
|
|
379
|
+
try {
|
|
380
|
+
const parsed = new URL(rawUrl);
|
|
381
|
+
if (parsed.protocol !== 'ws:') {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!urls.includes(rawUrl)) {
|
|
388
|
+
urls.push(rawUrl);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return urls;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function resolveInspectorWebSocketCandidates(
|
|
395
|
+
invocationDirectory: string,
|
|
396
|
+
logPath: string,
|
|
397
|
+
): readonly string[] {
|
|
398
|
+
const urls = [...parseInspectorWebSocketUrlsFromGatewayLog(logPath)];
|
|
399
|
+
const loadedConfig = loadHarnessConfig({ cwd: invocationDirectory });
|
|
400
|
+
const debugConfig = loadedConfig.config.debug;
|
|
401
|
+
if (debugConfig.enabled && debugConfig.inspect.enabled) {
|
|
402
|
+
const configuredUrl = `ws://localhost:${String(debugConfig.inspect.gatewayPort)}/harness-gateway`;
|
|
403
|
+
if (!urls.includes(configuredUrl)) {
|
|
404
|
+
urls.push(configuredUrl);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return urls;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function connectGatewayInspector(
|
|
411
|
+
invocationDirectory: string,
|
|
412
|
+
logPath: string,
|
|
413
|
+
timeoutMs: number,
|
|
414
|
+
): Promise<{ client: InspectorWebSocketClient; endpoint: string }> {
|
|
415
|
+
const candidates = resolveInspectorWebSocketCandidates(invocationDirectory, logPath);
|
|
416
|
+
if (candidates.length === 0) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
'gateway inspector endpoint unavailable; enable debug.inspect, restart gateway, then retry `harness profile start`',
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
let lastError: string | null = null;
|
|
422
|
+
for (const candidate of candidates) {
|
|
423
|
+
let client: InspectorWebSocketClient | null = null;
|
|
424
|
+
try {
|
|
425
|
+
client = await InspectorWebSocketClient.connect(candidate, timeoutMs);
|
|
426
|
+
await client.sendCommand('Runtime.enable', {}, timeoutMs);
|
|
427
|
+
return {
|
|
428
|
+
client,
|
|
429
|
+
endpoint: candidate,
|
|
430
|
+
};
|
|
431
|
+
} catch (error: unknown) {
|
|
432
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
433
|
+
client?.close();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw new Error(
|
|
437
|
+
`gateway inspector endpoint unavailable; enable debug.inspect, restart gateway, then retry \`harness profile start\` (${lastError ?? 'unknown error'})`,
|
|
438
|
+
);
|
|
439
|
+
}
|