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