libretto 0.6.10 → 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.
Files changed (119) hide show
  1. package/README.md +4 -0
  2. package/README.template.md +4 -0
  3. package/dist/cli/cli.js +4 -3
  4. package/dist/cli/commands/ai.js +3 -2
  5. package/dist/cli/commands/browser.js +17 -17
  6. package/dist/cli/commands/execution.js +254 -234
  7. package/dist/cli/commands/experiments.js +100 -0
  8. package/dist/cli/commands/setup.js +20 -34
  9. package/dist/cli/commands/shared.js +10 -0
  10. package/dist/cli/commands/snapshot.js +81 -9
  11. package/dist/cli/commands/status.js +5 -4
  12. package/dist/cli/core/ai-model.js +6 -3
  13. package/dist/cli/core/browser.js +300 -121
  14. package/dist/cli/core/config.js +4 -2
  15. package/dist/cli/core/context.js +4 -0
  16. package/dist/cli/core/daemon/config.js +0 -6
  17. package/dist/cli/core/daemon/daemon.js +535 -89
  18. package/dist/cli/core/daemon/ipc.js +170 -129
  19. package/dist/cli/core/daemon/snapshot.js +72 -6
  20. package/dist/cli/core/experiments.js +66 -0
  21. package/dist/cli/core/session.js +5 -4
  22. package/dist/cli/core/skill-version.js +2 -1
  23. package/dist/cli/core/snapshot-analyzer.js +4 -3
  24. package/dist/cli/core/workflow-runner/runner.js +147 -0
  25. package/dist/cli/core/workflow-runtime.js +60 -0
  26. package/dist/cli/router.js +4 -1
  27. package/dist/shared/debug/pause-handler.d.ts +9 -0
  28. package/dist/shared/debug/pause-handler.js +15 -0
  29. package/dist/shared/debug/pause.d.ts +1 -2
  30. package/dist/shared/debug/pause.js +13 -36
  31. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  32. package/dist/shared/ipc/child-process-transport.js +60 -0
  33. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  34. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  35. package/dist/shared/ipc/ipc.d.ts +46 -0
  36. package/dist/shared/ipc/ipc.js +165 -0
  37. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  38. package/dist/shared/ipc/ipc.spec.js +114 -0
  39. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  40. package/dist/shared/ipc/socket-transport.js +143 -0
  41. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  42. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  43. package/dist/shared/package-manager.d.ts +7 -0
  44. package/dist/shared/package-manager.js +60 -0
  45. package/dist/shared/paths/paths.d.ts +1 -8
  46. package/dist/shared/paths/paths.js +1 -49
  47. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  48. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  49. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  50. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  51. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  52. package/dist/shared/snapshot/render-snapshot.js +651 -0
  53. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  54. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  55. package/dist/shared/snapshot/types.d.ts +40 -0
  56. package/dist/shared/snapshot/types.js +0 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  58. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  59. package/dist/shared/state/session-state.d.ts +1 -0
  60. package/dist/shared/state/session-state.js +1 -0
  61. package/docs/experiments.md +67 -0
  62. package/package.json +4 -2
  63. package/skills/libretto/SKILL.md +3 -1
  64. package/skills/libretto-readonly/SKILL.md +1 -1
  65. package/src/cli/AGENTS.md +7 -0
  66. package/src/cli/cli.ts +4 -3
  67. package/src/cli/commands/ai.ts +3 -2
  68. package/src/cli/commands/browser.ts +13 -11
  69. package/src/cli/commands/execution.ts +303 -271
  70. package/src/cli/commands/experiments.ts +120 -0
  71. package/src/cli/commands/setup.ts +18 -36
  72. package/src/cli/commands/shared.ts +20 -0
  73. package/src/cli/commands/snapshot.ts +99 -11
  74. package/src/cli/commands/status.ts +5 -4
  75. package/src/cli/core/ai-model.ts +6 -3
  76. package/src/cli/core/browser.ts +369 -147
  77. package/src/cli/core/config.ts +3 -1
  78. package/src/cli/core/context.ts +4 -0
  79. package/src/cli/core/daemon/config.ts +35 -19
  80. package/src/cli/core/daemon/daemon.ts +686 -106
  81. package/src/cli/core/daemon/ipc.ts +330 -214
  82. package/src/cli/core/daemon/snapshot.ts +106 -8
  83. package/src/cli/core/experiments.ts +85 -0
  84. package/src/cli/core/session.ts +5 -4
  85. package/src/cli/core/skill-version.ts +2 -1
  86. package/src/cli/core/snapshot-analyzer.ts +4 -3
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +85 -0
  89. package/src/cli/router.ts +4 -1
  90. package/src/shared/debug/pause-handler.ts +20 -0
  91. package/src/shared/debug/pause.ts +14 -48
  92. package/src/shared/ipc/AGENTS.md +24 -0
  93. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  94. package/src/shared/ipc/child-process-transport.ts +96 -0
  95. package/src/shared/ipc/ipc.spec.ts +161 -0
  96. package/src/shared/ipc/ipc.ts +288 -0
  97. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  98. package/src/shared/ipc/socket-transport.ts +189 -0
  99. package/src/shared/package-manager.ts +76 -0
  100. package/src/shared/paths/paths.ts +0 -72
  101. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  102. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  103. package/src/shared/snapshot/render-snapshot.ts +962 -0
  104. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  105. package/src/shared/snapshot/types.ts +43 -0
  106. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  107. package/src/shared/state/session-state.ts +1 -0
  108. package/dist/cli/core/daemon/index.js +0 -16
  109. package/dist/cli/core/daemon/spawn.js +0 -90
  110. package/dist/cli/core/pause-signals.js +0 -29
  111. package/dist/cli/workers/run-integration-runtime.js +0 -235
  112. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  113. package/dist/cli/workers/run-integration-worker.js +0 -64
  114. package/src/cli/core/daemon/index.ts +0 -24
  115. package/src/cli/core/daemon/spawn.ts +0 -171
  116. package/src/cli/core/pause-signals.ts +0 -35
  117. package/src/cli/workers/run-integration-runtime.ts +0 -326
  118. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  119. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -1,41 +1,94 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createServer, connect as netConnect, type Server } from "node:net";
3
- import { unlink } from "node:fs/promises";
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 ErrorWithOutput = Error & { output?: DaemonExecOutput };
18
+ export type DaemonPageSummary = { id: string; url: string; active: boolean };
9
19
 
10
- // ---------------------------------------------------------------------------
11
- // Request types — one shape per daemon command
12
- // ---------------------------------------------------------------------------
20
+ export type DaemonExecArgs = {
21
+ code: string;
22
+ pageId?: string;
23
+ visualize?: boolean;
24
+ };
13
25
 
14
- export type DaemonRequest =
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
- // Response types success or error, keyed by the originating request id
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 DaemonResponse =
32
- | { id: string; type: "result"; data: unknown }
33
- | {
34
- id: string;
35
- type: "error";
36
- message: string;
37
- output?: DaemonExecOutput;
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 DaemonCommandResult<T> =
51
- | { ok: true; data: T }
52
- | { ok: false; message: string; output?: DaemonExecOutput };
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: Array<{ id: string; url: string; active: boolean }>;
166
- exec: { result: unknown; output?: DaemonExecOutput };
167
- "readonly-exec": {
168
- result: unknown;
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 — connects to UDS, sends NDJSON request, reads response
183
+ // DaemonClient — typed IPC wrapper over the daemon socket
182
184
  // ---------------------------------------------------------------------------
183
185
 
184
186
  export class DaemonClient {
185
- constructor(private readonly socketPath: string) {}
186
-
187
- private async send(request: DaemonRequest): Promise<DaemonResponse> {
188
- return new Promise<DaemonResponse>((resolve, reject) => {
189
- const socket = netConnect(this.socketPath);
190
- let buffer = "";
191
-
192
- socket.on("connect", () => {
193
- socket.write(JSON.stringify(request) + "\n");
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
- socket.on("error", (err) => {
214
- reject(err);
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
- private generateId(): string {
220
- return Math.random().toString(36).slice(2, 10);
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
- private async sendOrThrow<C extends DaemonRequest["command"]>(
224
- request: DaemonRequest & { command: C },
225
- ): Promise<DaemonResultMap[C]> {
226
- const response = await this.send(request);
227
- if (response.type === "error") {
228
- throw new DaemonClientError(response.message, response.output);
229
- }
230
- return response.data as DaemonResultMap[C];
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
- private async sendResult<C extends DaemonRequest["command"]>(
234
- request: DaemonRequest & { command: C },
235
- ): Promise<DaemonCommandResult<DaemonResultMap[C]>> {
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
- return { ok: true, data: response.data as DaemonResultMap[C] };
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.sendOrThrow({ id: this.generateId(), command: "ping" });
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.sendOrThrow({ id: this.generateId(), command: "pages" });
378
+ return this.ipc.call.pages();
258
379
  }
259
380
 
260
- async exec(args: {
261
- code: string;
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
- code: string;
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.sendOrThrow({
289
- id: this.generateId(),
290
- command: "snapshot",
291
- ...args,
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
  }