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.
Files changed (130) hide show
  1. package/README.md +7 -8
  2. package/README.template.md +7 -8
  3. package/dist/cli/cli.js +0 -22
  4. package/dist/cli/commands/browser.js +18 -24
  5. package/dist/cli/commands/execution.js +254 -234
  6. package/dist/cli/commands/experiments.js +100 -0
  7. package/dist/cli/commands/setup.js +3 -310
  8. package/dist/cli/commands/shared.js +10 -0
  9. package/dist/cli/commands/snapshot.js +46 -64
  10. package/dist/cli/commands/status.js +1 -40
  11. package/dist/cli/core/browser.js +303 -124
  12. package/dist/cli/core/config.js +5 -6
  13. package/dist/cli/core/context.js +4 -0
  14. package/dist/cli/core/daemon/config.js +0 -6
  15. package/dist/cli/core/daemon/daemon.js +497 -90
  16. package/dist/cli/core/daemon/ipc.js +170 -129
  17. package/dist/cli/core/daemon/snapshot.js +48 -9
  18. package/dist/cli/core/experiments.js +39 -0
  19. package/dist/cli/core/session.js +5 -4
  20. package/dist/cli/core/skill-version.js +2 -1
  21. package/dist/cli/core/workflow-runner/runner.js +147 -0
  22. package/dist/cli/core/workflow-runtime.js +60 -0
  23. package/dist/cli/index.js +0 -2
  24. package/dist/cli/router.js +4 -3
  25. package/dist/shared/debug/pause-handler.d.ts +9 -0
  26. package/dist/shared/debug/pause-handler.js +15 -0
  27. package/dist/shared/debug/pause.d.ts +1 -2
  28. package/dist/shared/debug/pause.js +13 -36
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  31. package/dist/shared/ipc/child-process-transport.js +60 -0
  32. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  33. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  34. package/dist/shared/ipc/ipc.d.ts +46 -0
  35. package/dist/shared/ipc/ipc.js +165 -0
  36. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  37. package/dist/shared/ipc/ipc.spec.js +114 -0
  38. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  39. package/dist/shared/ipc/socket-transport.js +143 -0
  40. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  41. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  42. package/dist/shared/package-manager.d.ts +7 -0
  43. package/dist/shared/package-manager.js +60 -0
  44. package/dist/shared/paths/paths.d.ts +1 -8
  45. package/dist/shared/paths/paths.js +1 -49
  46. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  47. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  48. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  49. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  50. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  51. package/dist/shared/snapshot/render-snapshot.js +651 -0
  52. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  53. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  54. package/dist/shared/snapshot/types.d.ts +40 -0
  55. package/dist/shared/snapshot/types.js +0 -0
  56. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  58. package/dist/shared/state/session-state.d.ts +1 -0
  59. package/dist/shared/state/session-state.js +1 -0
  60. package/docs/experiments.md +67 -0
  61. package/docs/releasing.md +8 -6
  62. package/package.json +5 -2
  63. package/skills/libretto/SKILL.md +19 -19
  64. package/skills/libretto/references/configuration-file-reference.md +6 -12
  65. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  66. package/skills/libretto-readonly/SKILL.md +2 -9
  67. package/src/cli/AGENTS.md +7 -0
  68. package/src/cli/cli.ts +0 -23
  69. package/src/cli/commands/browser.ts +14 -18
  70. package/src/cli/commands/execution.ts +303 -271
  71. package/src/cli/commands/experiments.ts +120 -0
  72. package/src/cli/commands/setup.ts +3 -400
  73. package/src/cli/commands/shared.ts +20 -0
  74. package/src/cli/commands/snapshot.ts +54 -94
  75. package/src/cli/commands/status.ts +1 -48
  76. package/src/cli/core/browser.ts +372 -150
  77. package/src/cli/core/config.ts +4 -5
  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 +645 -107
  81. package/src/cli/core/daemon/ipc.ts +319 -214
  82. package/src/cli/core/daemon/snapshot.ts +71 -15
  83. package/src/cli/core/experiments.ts +56 -0
  84. package/src/cli/core/resolve-model.ts +5 -0
  85. package/src/cli/core/session.ts +5 -4
  86. package/src/cli/core/skill-version.ts +2 -1
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +86 -0
  89. package/src/cli/index.ts +0 -1
  90. package/src/cli/router.ts +4 -3
  91. package/src/shared/debug/pause-handler.ts +20 -0
  92. package/src/shared/debug/pause.ts +14 -48
  93. package/src/shared/instrumentation/instrument.ts +4 -4
  94. package/src/shared/ipc/AGENTS.md +24 -0
  95. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  96. package/src/shared/ipc/child-process-transport.ts +96 -0
  97. package/src/shared/ipc/ipc.spec.ts +161 -0
  98. package/src/shared/ipc/ipc.ts +288 -0
  99. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  100. package/src/shared/ipc/socket-transport.ts +189 -0
  101. package/src/shared/package-manager.ts +76 -0
  102. package/src/shared/paths/paths.ts +0 -72
  103. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  104. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  105. package/src/shared/snapshot/render-snapshot.ts +962 -0
  106. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  107. package/src/shared/snapshot/types.ts +43 -0
  108. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  109. package/src/shared/state/session-state.ts +1 -0
  110. package/dist/cli/commands/ai.js +0 -109
  111. package/dist/cli/core/ai-model.js +0 -192
  112. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  113. package/dist/cli/core/daemon/index.js +0 -16
  114. package/dist/cli/core/daemon/spawn.js +0 -90
  115. package/dist/cli/core/pause-signals.js +0 -29
  116. package/dist/cli/core/snapshot-analyzer.js +0 -666
  117. package/dist/cli/workers/run-integration-runtime.js +0 -235
  118. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  119. package/dist/cli/workers/run-integration-worker.js +0 -64
  120. package/scripts/summarize-evals.mjs +0 -135
  121. package/src/cli/commands/ai.ts +0 -143
  122. package/src/cli/core/ai-model.ts +0 -298
  123. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  124. package/src/cli/core/daemon/index.ts +0 -24
  125. package/src/cli/core/daemon/spawn.ts +0 -171
  126. package/src/cli/core/pause-signals.ts +0 -35
  127. package/src/cli/core/snapshot-analyzer.ts +0 -855
  128. package/src/cli/workers/run-integration-runtime.ts +0 -326
  129. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  130. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -1,41 +1,83 @@
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
+ 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 DaemonResponse =
32
- | { id: string; type: "result"; data: unknown }
33
- | {
34
- id: string;
35
- type: "error";
36
- message: string;
37
- output?: DaemonExecOutput;
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 DaemonCommandResult<T> =
51
- | { ok: true; data: T }
52
- | { ok: false; message: string; output?: DaemonExecOutput };
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: 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
- };
165
+ pages: DaemonPageSummary[];
166
+ exec: DaemonExecSuccess;
167
+ "readonly-exec": DaemonExecSuccess;
168
+ snapshot: DaemonSnapshotResult;
178
169
  };
179
170
 
180
171
  // ---------------------------------------------------------------------------
181
- // DaemonClient — connects to UDS, sends NDJSON request, reads response
172
+ // DaemonClient — typed IPC wrapper over the daemon socket
182
173
  // ---------------------------------------------------------------------------
183
174
 
184
175
  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
- });
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
- socket.on("error", (err) => {
214
- reject(err);
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
- private generateId(): string {
220
- return Math.random().toString(36).slice(2, 10);
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
- 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
- }
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
- 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,
318
+ const fail = (error: Error): void => {
319
+ cleanup();
320
+ reject(error);
242
321
  };
243
- }
244
- return { ok: true, data: response.data as DaemonResultMap[C] };
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.sendOrThrow({ id: this.generateId(), command: "ping" });
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.sendOrThrow({ id: this.generateId(), command: "pages" });
367
+ return this.ipc.call.pages();
258
368
  }
259
369
 
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
- });
370
+ async exec(args: DaemonExecArgs): Promise<DaemonExecResult> {
371
+ return this.ipc.call.exec(args);
270
372
  }
271
373
 
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
- });
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.sendOrThrow({
289
- id: this.generateId(),
290
- command: "snapshot",
291
- ...args,
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
  }