libretto 0.6.11 → 0.6.13
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/README.md +7 -8
- package/README.template.md +7 -8
- package/dist/cli/cli.js +0 -22
- package/dist/cli/commands/browser.js +18 -24
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +3 -310
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +46 -64
- package/dist/cli/commands/status.js +1 -40
- package/dist/cli/core/browser.js +303 -124
- package/dist/cli/core/config.js +5 -6
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +497 -90
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +48 -9
- package/dist/cli/core/experiments.js +39 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/index.js +0 -2
- package/dist/cli/router.js +4 -3
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/instrumentation/instrument.js +4 -4
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/docs/releasing.md +8 -6
- package/package.json +5 -2
- package/skills/libretto/SKILL.md +19 -19
- package/skills/libretto/references/configuration-file-reference.md +6 -12
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto-readonly/SKILL.md +2 -9
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +0 -23
- package/src/cli/commands/browser.ts +14 -18
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +3 -400
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +54 -94
- package/src/cli/commands/status.ts +1 -48
- package/src/cli/core/browser.ts +372 -150
- package/src/cli/core/config.ts +4 -5
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +645 -107
- package/src/cli/core/daemon/ipc.ts +319 -214
- package/src/cli/core/daemon/snapshot.ts +71 -15
- package/src/cli/core/experiments.ts +56 -0
- package/src/cli/core/resolve-model.ts +5 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +86 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/router.ts +4 -3
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/instrumentation/instrument.ts +4 -4
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/commands/ai.js +0 -109
- package/dist/cli/core/ai-model.js +0 -192
- package/dist/cli/core/api-snapshot-analyzer.js +0 -86
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/core/snapshot-analyzer.js +0 -666
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/scripts/summarize-evals.mjs +0 -135
- package/src/cli/commands/ai.ts +0 -143
- package/src/cli/core/ai-model.ts +0 -298
- package/src/cli/core/api-snapshot-analyzer.ts +0 -110
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/core/snapshot-analyzer.ts +0 -855
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
|
@@ -1,41 +1,83 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { ChildProcess } from "node:child_process";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { openSync, closeSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createIpcPeer, type IpcPeer } from "../../../shared/ipc/ipc.js";
|
|
8
|
+
import { connectToIpcSocket } from "../../../shared/ipc/socket-transport.js";
|
|
9
|
+
import type { LoggerApi } from "../../../shared/logger/index.js";
|
|
10
|
+
import type { SnapshotDiff } from "../../../shared/snapshot/diff-snapshots.js";
|
|
11
|
+
import type { Snapshot } from "../../../shared/snapshot/types.js";
|
|
4
12
|
import { REPO_ROOT } from "../context.js";
|
|
13
|
+
import type { WorkflowStatus } from "../workflow-runner/runner.js";
|
|
14
|
+
import type { DaemonConfig } from "./config.js";
|
|
5
15
|
|
|
6
16
|
export type DaemonExecOutput = { stdout: string; stderr: string };
|
|
7
17
|
|
|
8
|
-
type
|
|
18
|
+
export type DaemonPageSummary = { id: string; url: string; active: boolean };
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
export type DaemonExecArgs = {
|
|
21
|
+
code: string;
|
|
22
|
+
pageId?: string;
|
|
23
|
+
visualize?: boolean;
|
|
24
|
+
};
|
|
13
25
|
|
|
14
|
-
export type
|
|
15
|
-
| { id: string; command: "ping" }
|
|
16
|
-
| { id: string; command: "pages" }
|
|
17
|
-
| { id: string; command: "snapshot"; pageId?: string }
|
|
18
|
-
| {
|
|
19
|
-
id: string;
|
|
20
|
-
command: "exec";
|
|
21
|
-
code: string;
|
|
22
|
-
pageId?: string;
|
|
23
|
-
visualize?: boolean;
|
|
24
|
-
}
|
|
25
|
-
| { id: string; command: "readonly-exec"; code: string; pageId?: string };
|
|
26
|
+
export type DaemonReadonlyExecArgs = { code: string; pageId?: string };
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
export type DaemonSnapshotArgs = {
|
|
29
|
+
pageId?: string;
|
|
30
|
+
useCachedSnapshot?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type DaemonExecSuccess = {
|
|
34
|
+
result: unknown;
|
|
35
|
+
output?: DaemonExecOutput;
|
|
36
|
+
snapshotDiff?: SnapshotDiff;
|
|
37
|
+
};
|
|
30
38
|
|
|
31
|
-
export type
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
export type DaemonSnapshotResult = {
|
|
40
|
+
mode: "compact";
|
|
41
|
+
pngPath: string;
|
|
42
|
+
snapshot: Snapshot;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type DaemonCloseResult = { replayUrl?: string };
|
|
46
|
+
|
|
47
|
+
export type DaemonCommandResult<T> =
|
|
48
|
+
| { ok: true; data: T }
|
|
49
|
+
| { ok: false; message: string; output?: DaemonExecOutput };
|
|
50
|
+
|
|
51
|
+
export type DaemonExecResult = DaemonCommandResult<DaemonExecSuccess>;
|
|
52
|
+
|
|
53
|
+
export type CliToDaemonApi = {
|
|
54
|
+
ping(): { protocolVersion: number };
|
|
55
|
+
pages(): DaemonPageSummary[];
|
|
56
|
+
exec(args: DaemonExecArgs): DaemonExecResult;
|
|
57
|
+
readonlyExec(args: DaemonReadonlyExecArgs): DaemonExecResult;
|
|
58
|
+
snapshot(args: DaemonSnapshotArgs): DaemonSnapshotResult;
|
|
59
|
+
getWorkflowStatus(): WorkflowStatus;
|
|
60
|
+
resumeWorkflow(): void;
|
|
61
|
+
close(): DaemonCloseResult;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type DaemonToCliApi = {
|
|
65
|
+
workflowOutput(args: { stream: "stdout" | "stderr"; text: string }): void;
|
|
66
|
+
workflowPaused(args: { pausedAt: string; url?: string }): void;
|
|
67
|
+
workflowFinished(
|
|
68
|
+
args:
|
|
69
|
+
| { result: "completed"; completedAt: string }
|
|
70
|
+
| { result: "failed"; message: string; phase: "setup" | "workflow" },
|
|
71
|
+
): void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function createNoopDaemonToCliHandlers(): DaemonToCliApi {
|
|
75
|
+
return {
|
|
76
|
+
workflowOutput: () => {},
|
|
77
|
+
workflowPaused: () => {},
|
|
78
|
+
workflowFinished: () => {},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
39
81
|
|
|
40
82
|
export class DaemonClientError extends Error {
|
|
41
83
|
constructor(
|
|
@@ -47,9 +89,53 @@ export class DaemonClientError extends Error {
|
|
|
47
89
|
}
|
|
48
90
|
}
|
|
49
91
|
|
|
50
|
-
export type
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
export type DaemonReadyMessage = {
|
|
93
|
+
type: "ready";
|
|
94
|
+
socketPath: string;
|
|
95
|
+
provider?: {
|
|
96
|
+
name: string;
|
|
97
|
+
sessionId: string;
|
|
98
|
+
cdpEndpoint: string;
|
|
99
|
+
liveViewUrl?: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type DaemonStartupErrorMessage = {
|
|
104
|
+
type: "startup-error";
|
|
105
|
+
message: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function isDaemonReadyMessage(message: unknown): message is DaemonReadyMessage {
|
|
109
|
+
if (typeof message !== "object" || message === null) return false;
|
|
110
|
+
const candidate = message as { type?: unknown; socketPath?: unknown };
|
|
111
|
+
return candidate.type === "ready" && typeof candidate.socketPath === "string";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isDaemonStartupErrorMessage(
|
|
115
|
+
message: unknown,
|
|
116
|
+
): message is DaemonStartupErrorMessage {
|
|
117
|
+
if (typeof message !== "object" || message === null) return false;
|
|
118
|
+
const candidate = message as { type?: unknown; message?: unknown };
|
|
119
|
+
return (
|
|
120
|
+
candidate.type === "startup-error" && typeof candidate.message === "string"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type DaemonClientSpawnOptions = {
|
|
125
|
+
config: DaemonConfig;
|
|
126
|
+
logger: LoggerApi;
|
|
127
|
+
logPath: string;
|
|
128
|
+
startupTimeoutMs: number;
|
|
129
|
+
handlers?: DaemonToCliApi;
|
|
130
|
+
onFailure?: () => Promise<unknown>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type DaemonClientSpawnResult = {
|
|
134
|
+
pid: number;
|
|
135
|
+
socketPath: string;
|
|
136
|
+
provider?: DaemonReadyMessage["provider"];
|
|
137
|
+
client: DaemonClient;
|
|
138
|
+
};
|
|
53
139
|
|
|
54
140
|
// ---------------------------------------------------------------------------
|
|
55
141
|
// Socket path resolution
|
|
@@ -70,183 +156,207 @@ export function getDaemonSocketPath(session: string): string {
|
|
|
70
156
|
return `/tmp/libretto-${process.getuid!()}-${hash}.sock`;
|
|
71
157
|
}
|
|
72
158
|
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// DaemonServer — Unix domain socket server, NDJSON, one request per connection
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
export type RequestHandler = (request: DaemonRequest) => Promise<unknown>;
|
|
78
|
-
|
|
79
|
-
export class DaemonServer {
|
|
80
|
-
private server: Server | null = null;
|
|
81
|
-
|
|
82
|
-
constructor(
|
|
83
|
-
private readonly socketPath: string,
|
|
84
|
-
private readonly handler: RequestHandler,
|
|
85
|
-
) {}
|
|
86
|
-
|
|
87
|
-
async listen(): Promise<void> {
|
|
88
|
-
// Remove stale socket file if present.
|
|
89
|
-
try {
|
|
90
|
-
await unlink(this.socketPath);
|
|
91
|
-
} catch (err) {
|
|
92
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const server = createServer((socket) => {
|
|
96
|
-
let buffer = "";
|
|
97
|
-
socket.on("data", (chunk) => {
|
|
98
|
-
buffer += chunk.toString();
|
|
99
|
-
const newlineIndex = buffer.indexOf("\n");
|
|
100
|
-
if (newlineIndex === -1) return;
|
|
101
|
-
|
|
102
|
-
const line = buffer.slice(0, newlineIndex);
|
|
103
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
104
|
-
|
|
105
|
-
void (async () => {
|
|
106
|
-
let response: DaemonResponse;
|
|
107
|
-
try {
|
|
108
|
-
const request = JSON.parse(line) as DaemonRequest;
|
|
109
|
-
const data = await this.handler(request);
|
|
110
|
-
response = { id: request.id, type: "result", data };
|
|
111
|
-
} catch (err) {
|
|
112
|
-
const id = (() => {
|
|
113
|
-
try {
|
|
114
|
-
return (JSON.parse(line) as { id?: string }).id ?? "unknown";
|
|
115
|
-
} catch {
|
|
116
|
-
return "unknown";
|
|
117
|
-
}
|
|
118
|
-
})();
|
|
119
|
-
response = {
|
|
120
|
-
id,
|
|
121
|
-
type: "error",
|
|
122
|
-
message: err instanceof Error ? err.message : String(err),
|
|
123
|
-
output:
|
|
124
|
-
err instanceof Error
|
|
125
|
-
? (err as ErrorWithOutput).output
|
|
126
|
-
: undefined,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
socket.end(JSON.stringify(response) + "\n");
|
|
130
|
-
})();
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
this.server = server;
|
|
135
|
-
|
|
136
|
-
await new Promise<void>((resolve, reject) => {
|
|
137
|
-
server.on("error", reject);
|
|
138
|
-
server.listen(this.socketPath, () => resolve());
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async close(): Promise<void> {
|
|
143
|
-
const server = this.server;
|
|
144
|
-
if (!server) return;
|
|
145
|
-
this.server = null;
|
|
146
|
-
|
|
147
|
-
await new Promise<void>((resolve, reject) => {
|
|
148
|
-
server.close((err) => (err ? reject(err) : resolve()));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
await unlink(this.socketPath);
|
|
153
|
-
} catch (err) {
|
|
154
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
159
|
// ---------------------------------------------------------------------------
|
|
160
160
|
// Response data types — maps command name to the shape returned on success
|
|
161
161
|
// ---------------------------------------------------------------------------
|
|
162
162
|
|
|
163
163
|
export type DaemonResultMap = {
|
|
164
164
|
ping: { protocolVersion: number };
|
|
165
|
-
pages:
|
|
166
|
-
exec:
|
|
167
|
-
"readonly-exec":
|
|
168
|
-
|
|
169
|
-
output?: DaemonExecOutput;
|
|
170
|
-
};
|
|
171
|
-
snapshot: {
|
|
172
|
-
pngPath: string;
|
|
173
|
-
htmlPath: string;
|
|
174
|
-
snapshotRunId: string;
|
|
175
|
-
pageUrl: string;
|
|
176
|
-
title: string;
|
|
177
|
-
};
|
|
165
|
+
pages: DaemonPageSummary[];
|
|
166
|
+
exec: DaemonExecSuccess;
|
|
167
|
+
"readonly-exec": DaemonExecSuccess;
|
|
168
|
+
snapshot: DaemonSnapshotResult;
|
|
178
169
|
};
|
|
179
170
|
|
|
180
171
|
// ---------------------------------------------------------------------------
|
|
181
|
-
// DaemonClient —
|
|
172
|
+
// DaemonClient — typed IPC wrapper over the daemon socket
|
|
182
173
|
// ---------------------------------------------------------------------------
|
|
183
174
|
|
|
184
175
|
export class DaemonClient {
|
|
185
|
-
constructor(private readonly
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
socket.on("data", (chunk) => {
|
|
197
|
-
buffer += chunk.toString();
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
socket.on("end", () => {
|
|
201
|
-
try {
|
|
202
|
-
const response = JSON.parse(buffer.trim()) as DaemonResponse;
|
|
203
|
-
resolve(response);
|
|
204
|
-
} catch (err) {
|
|
205
|
-
reject(
|
|
206
|
-
new Error(
|
|
207
|
-
`Failed to parse daemon response: ${err instanceof Error ? err.message : String(err)}`,
|
|
208
|
-
),
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
176
|
+
private constructor(private readonly ipc: IpcPeer<CliToDaemonApi>) {}
|
|
177
|
+
|
|
178
|
+
static async connect(
|
|
179
|
+
socketPath: string,
|
|
180
|
+
handlers: DaemonToCliApi = createNoopDaemonToCliHandlers(),
|
|
181
|
+
): Promise<DaemonClient> {
|
|
182
|
+
const transport = await connectToIpcSocket(socketPath);
|
|
183
|
+
return new DaemonClient(
|
|
184
|
+
createIpcPeer<CliToDaemonApi, DaemonToCliApi>(transport, handlers),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
212
187
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
188
|
+
static async spawn(
|
|
189
|
+
options: DaemonClientSpawnOptions,
|
|
190
|
+
): Promise<DaemonClientSpawnResult> {
|
|
191
|
+
const { config, logger, logPath, startupTimeoutMs, handlers, onFailure } =
|
|
192
|
+
options;
|
|
193
|
+
const { session } = config;
|
|
194
|
+
|
|
195
|
+
const daemonEntryPath = fileURLToPath(
|
|
196
|
+
new URL("./daemon.js", import.meta.url),
|
|
197
|
+
);
|
|
198
|
+
const require = createRequire(import.meta.url);
|
|
199
|
+
const tsxCliPath = require.resolve("tsx/cli");
|
|
200
|
+
|
|
201
|
+
const childStderrFd = openSync(logPath, "a");
|
|
202
|
+
const child = spawn(
|
|
203
|
+
process.execPath,
|
|
204
|
+
[
|
|
205
|
+
tsxCliPath,
|
|
206
|
+
...(config.workflow?.tsconfigPath
|
|
207
|
+
? ["--tsconfig", config.workflow.tsconfigPath]
|
|
208
|
+
: []),
|
|
209
|
+
daemonEntryPath,
|
|
210
|
+
JSON.stringify(config),
|
|
211
|
+
],
|
|
212
|
+
{
|
|
213
|
+
detached: true,
|
|
214
|
+
stdio: ["ignore", "ignore", childStderrFd, "ipc"],
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
closeSync(childStderrFd);
|
|
218
|
+
|
|
219
|
+
const pid = child.pid!;
|
|
220
|
+
logger.info("daemon-spawned", { pid, session });
|
|
221
|
+
|
|
222
|
+
const readyMessage = await DaemonClient.waitForReadyMessage({
|
|
223
|
+
child,
|
|
224
|
+
timeoutMs: startupTimeoutMs,
|
|
225
|
+
formatTimeoutError: () =>
|
|
226
|
+
new Error(
|
|
227
|
+
`Daemon failed to start within ${Math.ceil(startupTimeoutMs / 1000)}s. Check logs: ${logPath}`,
|
|
228
|
+
),
|
|
229
|
+
formatSpawnError: (error) => {
|
|
230
|
+
const errWithCode = error as Error & { code?: string };
|
|
231
|
+
const hint =
|
|
232
|
+
errWithCode.code === "ENOENT"
|
|
233
|
+
? " Ensure Node.js is available in PATH for child processes."
|
|
234
|
+
: "";
|
|
235
|
+
return new Error(
|
|
236
|
+
`Failed to spawn daemon: ${error.message}.${hint} Check logs: ${logPath}`,
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
formatExitError: (code, signal) => {
|
|
240
|
+
const status = code ?? signal ?? "unknown";
|
|
241
|
+
return new Error(
|
|
242
|
+
`Daemon exited before startup (status: ${status}). Check logs: ${logPath}`,
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
onReady: (message) => {
|
|
246
|
+
logger.info("daemon-ready", {
|
|
247
|
+
session,
|
|
248
|
+
socketPath: message.socketPath,
|
|
249
|
+
pid,
|
|
250
|
+
});
|
|
251
|
+
child.disconnect();
|
|
252
|
+
child.unref();
|
|
253
|
+
},
|
|
254
|
+
onSpawnError: (error) => {
|
|
255
|
+
logger.error("daemon-spawn-error", { error, session });
|
|
256
|
+
},
|
|
257
|
+
onExit: (code, signal, ready) => {
|
|
258
|
+
logger.warn("daemon-exit", { code, signal, session, pid, ready });
|
|
259
|
+
},
|
|
260
|
+
}).catch(async (error: unknown) => {
|
|
261
|
+
try {
|
|
262
|
+
process.kill(pid, "SIGTERM");
|
|
263
|
+
} catch {
|
|
264
|
+
// Process may have already exited.
|
|
265
|
+
}
|
|
266
|
+
await onFailure?.();
|
|
267
|
+
throw error;
|
|
216
268
|
});
|
|
217
|
-
}
|
|
218
269
|
|
|
219
|
-
|
|
220
|
-
|
|
270
|
+
const client = await DaemonClient.connect(
|
|
271
|
+
readyMessage.socketPath,
|
|
272
|
+
handlers,
|
|
273
|
+
);
|
|
274
|
+
const socketPath = readyMessage.socketPath;
|
|
275
|
+
logger.info("daemon-ipc-ready", { session, socketPath });
|
|
276
|
+
return { pid, socketPath, provider: readyMessage.provider, client };
|
|
221
277
|
}
|
|
222
278
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
279
|
+
static async waitForReadyMessage(args: {
|
|
280
|
+
child: ChildProcess;
|
|
281
|
+
timeoutMs: number;
|
|
282
|
+
formatTimeoutError: () => Error;
|
|
283
|
+
formatSpawnError: (error: Error) => Error;
|
|
284
|
+
formatExitError: (
|
|
285
|
+
code: number | null,
|
|
286
|
+
signal: NodeJS.Signals | null,
|
|
287
|
+
) => Error;
|
|
288
|
+
onReady?: (message: DaemonReadyMessage) => void;
|
|
289
|
+
onSpawnError?: (error: Error) => void;
|
|
290
|
+
onExit?: (
|
|
291
|
+
code: number | null,
|
|
292
|
+
signal: NodeJS.Signals | null,
|
|
293
|
+
ready: boolean,
|
|
294
|
+
) => void;
|
|
295
|
+
}): Promise<DaemonReadyMessage> {
|
|
296
|
+
const {
|
|
297
|
+
child,
|
|
298
|
+
timeoutMs,
|
|
299
|
+
formatTimeoutError,
|
|
300
|
+
formatSpawnError,
|
|
301
|
+
formatExitError,
|
|
302
|
+
onReady,
|
|
303
|
+
onSpawnError,
|
|
304
|
+
onExit,
|
|
305
|
+
} = args;
|
|
306
|
+
|
|
307
|
+
return new Promise<DaemonReadyMessage>((resolve, reject) => {
|
|
308
|
+
let ready = false;
|
|
309
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
310
|
+
|
|
311
|
+
const cleanup = (): void => {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
child.off("message", onMessage);
|
|
314
|
+
child.off("error", onError);
|
|
315
|
+
child.off("exit", onChildExit);
|
|
316
|
+
};
|
|
232
317
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const response = await this.send(request);
|
|
237
|
-
if (response.type === "error") {
|
|
238
|
-
return {
|
|
239
|
-
ok: false,
|
|
240
|
-
message: response.message,
|
|
241
|
-
output: response.output,
|
|
318
|
+
const fail = (error: Error): void => {
|
|
319
|
+
cleanup();
|
|
320
|
+
reject(error);
|
|
242
321
|
};
|
|
243
|
-
|
|
244
|
-
|
|
322
|
+
|
|
323
|
+
timeout = setTimeout(() => fail(formatTimeoutError()), timeoutMs);
|
|
324
|
+
|
|
325
|
+
const onMessage = (message: unknown): void => {
|
|
326
|
+
if (isDaemonStartupErrorMessage(message)) {
|
|
327
|
+
fail(new Error(message.message));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!isDaemonReadyMessage(message)) return;
|
|
331
|
+
ready = true;
|
|
332
|
+
cleanup();
|
|
333
|
+
onReady?.(message);
|
|
334
|
+
resolve(message);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const onError = (error: Error): void => {
|
|
338
|
+
onSpawnError?.(error);
|
|
339
|
+
fail(formatSpawnError(error));
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const onChildExit = (
|
|
343
|
+
code: number | null,
|
|
344
|
+
signal: NodeJS.Signals | null,
|
|
345
|
+
): void => {
|
|
346
|
+
onExit?.(code, signal, ready);
|
|
347
|
+
if (ready) return;
|
|
348
|
+
fail(formatExitError(code, signal));
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
child.on("message", onMessage);
|
|
352
|
+
child.on("error", onError);
|
|
353
|
+
child.on("exit", onChildExit);
|
|
354
|
+
});
|
|
245
355
|
}
|
|
246
356
|
|
|
247
357
|
async ping(): Promise<boolean> {
|
|
248
358
|
try {
|
|
249
|
-
await this.
|
|
359
|
+
await this.ipc.call.ping();
|
|
250
360
|
return true;
|
|
251
361
|
} catch {
|
|
252
362
|
return false;
|
|
@@ -254,41 +364,36 @@ export class DaemonClient {
|
|
|
254
364
|
}
|
|
255
365
|
|
|
256
366
|
async pages(): Promise<DaemonResultMap["pages"]> {
|
|
257
|
-
return this.
|
|
367
|
+
return this.ipc.call.pages();
|
|
258
368
|
}
|
|
259
369
|
|
|
260
|
-
async exec(args: {
|
|
261
|
-
|
|
262
|
-
pageId?: string;
|
|
263
|
-
visualize?: boolean;
|
|
264
|
-
}): Promise<DaemonCommandResult<DaemonResultMap["exec"]>> {
|
|
265
|
-
return this.sendResult({
|
|
266
|
-
id: this.generateId(),
|
|
267
|
-
command: "exec",
|
|
268
|
-
...args,
|
|
269
|
-
});
|
|
370
|
+
async exec(args: DaemonExecArgs): Promise<DaemonExecResult> {
|
|
371
|
+
return this.ipc.call.exec(args);
|
|
270
372
|
}
|
|
271
373
|
|
|
272
|
-
async readonlyExec(args: {
|
|
273
|
-
|
|
274
|
-
pageId?: string;
|
|
275
|
-
}): Promise<DaemonCommandResult<DaemonResultMap["readonly-exec"]>> {
|
|
276
|
-
return this.sendResult({
|
|
277
|
-
id: this.generateId(),
|
|
278
|
-
command: "readonly-exec",
|
|
279
|
-
...args,
|
|
280
|
-
});
|
|
374
|
+
async readonlyExec(args: DaemonReadonlyExecArgs): Promise<DaemonExecResult> {
|
|
375
|
+
return this.ipc.call.readonlyExec(args);
|
|
281
376
|
}
|
|
282
377
|
|
|
283
378
|
async snapshot(
|
|
284
|
-
args: {
|
|
285
|
-
pageId?: string;
|
|
286
|
-
} = {},
|
|
379
|
+
args: DaemonSnapshotArgs = {},
|
|
287
380
|
): Promise<DaemonResultMap["snapshot"]> {
|
|
288
|
-
return this.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
381
|
+
return this.ipc.call.snapshot(args);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async getWorkflowStatus(): Promise<WorkflowStatus> {
|
|
385
|
+
return this.ipc.call.getWorkflowStatus();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async resumeWorkflow(): Promise<void> {
|
|
389
|
+
await this.ipc.call.resumeWorkflow();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async close(): Promise<DaemonCloseResult> {
|
|
393
|
+
return this.ipc.call.close();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
destroy(): void {
|
|
397
|
+
this.ipc.destroy();
|
|
293
398
|
}
|
|
294
399
|
}
|