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
@@ -0,0 +1,60 @@
1
+ import { existsSync } from "node:fs";
2
+ import { cwd } from "node:process";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { instrumentContext } from "../../index.js";
6
+ import {
7
+ getDefaultWorkflowFromModuleExports,
8
+ getWorkflowsFromModuleExports
9
+ } from "../../shared/workflow/workflow.js";
10
+ const TSCONFIG_HINT = "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
11
+ function isTsxCompileError(error) {
12
+ return error instanceof Error && (error.name === "TransformError" || error.message.startsWith("Cannot resolve tsconfig at path:"));
13
+ }
14
+ function getAbsoluteIntegrationPath(integrationPath) {
15
+ const absolutePath = isAbsolute(integrationPath) ? integrationPath : resolve(cwd(), integrationPath);
16
+ if (!existsSync(absolutePath)) {
17
+ throw new Error(`Integration file does not exist: ${absolutePath}`);
18
+ }
19
+ return absolutePath;
20
+ }
21
+ async function loadDefaultWorkflow(absolutePath) {
22
+ let loadedModule;
23
+ try {
24
+ loadedModule = await import(pathToFileURL(absolutePath).href);
25
+ } catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ const compileHint = isTsxCompileError(error) ? `
28
+ ${TSCONFIG_HINT}` : absolutePath.endsWith(".ts") || absolutePath.endsWith(".tsx") ? `
29
+ ${TSCONFIG_HINT}` : "";
30
+ throw new Error(
31
+ `Failed to import integration module at ${absolutePath}: ${message}${compileHint}`
32
+ );
33
+ }
34
+ const defaultWorkflow = getDefaultWorkflowFromModuleExports(loadedModule);
35
+ if (defaultWorkflow) {
36
+ return defaultWorkflow;
37
+ }
38
+ const availableWorkflowNames = getWorkflowsFromModuleExports(loadedModule).map(
39
+ (candidate) => candidate.name
40
+ );
41
+ if (availableWorkflowNames.length === 0) {
42
+ throw new Error(
43
+ `No default-exported workflow found in ${absolutePath}. Export the workflow with \`export default workflow("name", handler)\`.`
44
+ );
45
+ }
46
+ throw new Error(
47
+ `No default-exported workflow found in ${absolutePath}. libretto run only uses the file's default export. Available named workflows: ${availableWorkflowNames.join(", ")}`
48
+ );
49
+ }
50
+ async function installHeadedWorkflowVisualization(args) {
51
+ await (args.instrument ?? instrumentContext)(args.context, {
52
+ visualize: true,
53
+ logger: args.logger
54
+ });
55
+ }
56
+ export {
57
+ getAbsoluteIntegrationPath,
58
+ installHeadedWorkflowVisualization,
59
+ loadDefaultWorkflow
60
+ };
package/dist/cli/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { runLibrettoCLI } from "./cli.js";
3
- import { runClose } from "./commands/browser.js";
4
3
  void runLibrettoCLI();
5
4
  export {
6
- runClose,
7
5
  runLibrettoCLI
8
6
  };
@@ -1,18 +1,19 @@
1
- import { aiCommands } from "./commands/ai.js";
2
1
  import { authCommands } from "./commands/auth.js";
3
2
  import { billingCommands } from "./commands/billing.js";
4
3
  import { browserCommands } from "./commands/browser.js";
5
4
  import { deployCommand } from "./commands/deploy.js";
6
5
  import { executionCommands } from "./commands/execution.js";
6
+ import { experimentsCommand } from "./commands/experiments.js";
7
7
  import { setupCommand } from "./commands/setup.js";
8
8
  import { statusCommand } from "./commands/status.js";
9
9
  import { snapshotCommand } from "./commands/snapshot.js";
10
+ import { librettoCommand } from "../shared/package-manager.js";
10
11
  import { SimpleCLI } from "./framework/simple-cli.js";
11
12
  const cliRoutes = {
12
13
  ...browserCommands,
13
14
  deploy: deployCommand,
15
+ experiments: experimentsCommand,
14
16
  ...executionCommands,
15
- ai: aiCommands,
16
17
  auth: authCommands,
17
18
  billing: billingCommands,
18
19
  setup: setupCommand,
@@ -20,7 +21,7 @@ const cliRoutes = {
20
21
  snapshot: snapshotCommand
21
22
  };
22
23
  function createCLIApp() {
23
- return SimpleCLI.define("libretto", cliRoutes);
24
+ return SimpleCLI.define(librettoCommand(), cliRoutes);
24
25
  }
25
26
  export {
26
27
  cliRoutes,
@@ -0,0 +1,9 @@
1
+ type ActivePauseHandler = (args: {
2
+ session: string;
3
+ pausedAt: string;
4
+ url?: string;
5
+ }) => Promise<void>;
6
+ declare function installPauseHandler(handler: ActivePauseHandler): () => void;
7
+ declare function getActivePauseHandler(): ActivePauseHandler | undefined;
8
+
9
+ export { type ActivePauseHandler, getActivePauseHandler, installPauseHandler };
@@ -0,0 +1,15 @@
1
+ let activePauseHandler;
2
+ function installPauseHandler(handler) {
3
+ const previousHandler = activePauseHandler;
4
+ activePauseHandler = handler;
5
+ return () => {
6
+ activePauseHandler = previousHandler;
7
+ };
8
+ }
9
+ function getActivePauseHandler() {
10
+ return activePauseHandler;
11
+ }
12
+ export {
13
+ getActivePauseHandler,
14
+ installPauseHandler
15
+ };
@@ -2,8 +2,7 @@
2
2
  * Standalone pause function.
3
3
  *
4
4
  * - In production (`NODE_ENV === "production"`), returns immediately (no-op).
5
- * - Otherwise, writes a `.paused` signal file and polls for a `.resume` signal,
6
- * using the same file-based mechanism as the CLI runner.
5
+ * - Otherwise, delegates to the active Libretto workflow runtime pause handler.
7
6
  *
8
7
  * Import directly: `import { pause } from "libretto";`
9
8
  */
@@ -1,21 +1,9 @@
1
- import { existsSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import { getSessionDir } from "../../cli/core/context.js";
4
- import {
5
- getPauseSignalPaths,
6
- removeSignalIfExists
7
- } from "../../cli/core/pause-signals.js";
8
- import { listRunningSessions } from "../../cli/core/session.js";
1
+ import { getActivePauseHandler } from "./pause-handler.js";
2
+ import { librettoCommand } from "../package-manager.js";
9
3
  function throwMissingSessionError() {
10
- const runningSessions = listRunningSessions();
11
- const lines = ["pause(session) requires a non-empty session ID."];
12
- if (runningSessions.length > 0) {
13
- lines.push("", "Running sessions:");
14
- for (const s of runningSessions) {
15
- lines.push(` ${s.session}`);
16
- }
17
- }
18
- throw new Error(lines.join("\n"));
4
+ throw new Error(
5
+ `pause(session) requires a non-empty session ID. Pass ctx.session from inside your workflow: await pause(ctx.session). To list running sessions, run: ${librettoCommand("status")}.`
6
+ );
19
7
  }
20
8
  async function pause(session) {
21
9
  if (process.env.NODE_ENV === "production") {
@@ -24,27 +12,16 @@ async function pause(session) {
24
12
  if (typeof session !== "string" || session.trim().length === 0) {
25
13
  throwMissingSessionError();
26
14
  }
27
- const signalPaths = getPauseSignalPaths(session);
28
- const { pausedSignalPath, resumeSignalPath } = signalPaths;
29
- await mkdir(getSessionDir(session), { recursive: true });
30
- await removeSignalIfExists(resumeSignalPath);
31
- const details = {
32
- sessionName: session,
33
- pausedAt: (/* @__PURE__ */ new Date()).toISOString(),
34
- url: "unknown"
35
- };
36
- await writeFile(pausedSignalPath, JSON.stringify(details, null, 2), "utf8");
37
- console.log(`[pause] Paused (session: ${session})`);
38
- console.log("[pause] Waiting for resume signal...");
39
- const RESUME_POLL_INTERVAL_MS = 250;
40
- while (!existsSync(resumeSignalPath)) {
41
- await new Promise(
42
- (resolve) => setTimeout(resolve, RESUME_POLL_INTERVAL_MS)
15
+ const handler = getActivePauseHandler();
16
+ if (!handler) {
17
+ throw new Error(
18
+ `pause(session) can only suspend an active Libretto workflow. Run the workflow with ${librettoCommand("run <integrationFile>")} and call pause(ctx.session) from inside the workflow.`
43
19
  );
44
20
  }
45
- await removeSignalIfExists(resumeSignalPath);
46
- await removeSignalIfExists(pausedSignalPath);
47
- console.log("[pause] Resume signal received. Continuing workflow...");
21
+ await handler({
22
+ session,
23
+ pausedAt: (/* @__PURE__ */ new Date()).toISOString()
24
+ });
48
25
  }
49
26
  export {
50
27
  pause
@@ -82,12 +82,12 @@ function wrapLocatorActions(locator, page, opts) {
82
82
  try {
83
83
  const result = await orig(...args);
84
84
  if (opts.visualize) {
85
- enqueue(page, () => visualizeAfterAction(page));
85
+ void enqueue(page, () => visualizeAfterAction(page));
86
86
  }
87
87
  return result;
88
88
  } catch (err) {
89
89
  if (opts.visualize) {
90
- enqueue(page, () => visualizeAfterAction(page));
90
+ void enqueue(page, () => visualizeAfterAction(page));
91
91
  }
92
92
  if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
93
93
  await enrichTimeoutError(err, locator, page);
@@ -226,12 +226,12 @@ async function installInstrumentation(page, options) {
226
226
  try {
227
227
  const result = await orig(...args);
228
228
  if (visualize) {
229
- enqueue(page, () => visualizeAfterAction(page));
229
+ void enqueue(page, () => visualizeAfterAction(page));
230
230
  }
231
231
  return result;
232
232
  } catch (err) {
233
233
  if (visualize) {
234
- enqueue(page, () => visualizeAfterAction(page));
234
+ void enqueue(page, () => visualizeAfterAction(page));
235
235
  }
236
236
  if (POINTER_ACTIONS.has(method) && isTimeoutError(err) && typeof args[0] === "string") {
237
237
  await enrichTimeoutError(err, page.locator(args[0]), page);
@@ -0,0 +1,7 @@
1
+ import { ChildProcess } from 'node:child_process';
2
+ import { IpcTransport, IpcProtocolMessage } from './ipc.js';
3
+
4
+ declare function createChildProcessIpcTransport(child: ChildProcess): IpcTransport<IpcProtocolMessage>;
5
+ declare function createParentProcessIpcTransport(): IpcTransport<IpcProtocolMessage>;
6
+
7
+ export { createChildProcessIpcTransport, createParentProcessIpcTransport };
@@ -0,0 +1,60 @@
1
+ function createChildProcessIpcTransport(child) {
2
+ return createProcessIpcTransport(child, () => {
3
+ if (child.connected) child.disconnect();
4
+ });
5
+ }
6
+ function createParentProcessIpcTransport() {
7
+ if (!process.send) {
8
+ throw new Error(
9
+ "Cannot create child-process IPC transport: process.send is not available. Start the process with an IPC channel."
10
+ );
11
+ }
12
+ return createProcessIpcTransport(process, () => {
13
+ if (process.connected) process.disconnect?.();
14
+ });
15
+ }
16
+ function createProcessIpcTransport(target, close) {
17
+ return {
18
+ send(message) {
19
+ if (!target.send) {
20
+ throw new Error(
21
+ "Cannot send IPC message: process IPC channel is closed."
22
+ );
23
+ }
24
+ target.send(message);
25
+ },
26
+ listen(callback) {
27
+ const onMessage = (message) => {
28
+ if (isIpcProtocolMessage(message)) callback(message);
29
+ };
30
+ target.on("message", onMessage);
31
+ return () => target.off("message", onMessage);
32
+ },
33
+ onClose(callback) {
34
+ const onDisconnect = () => callback();
35
+ target.on("disconnect", onDisconnect);
36
+ return () => target.off("disconnect", onDisconnect);
37
+ },
38
+ close
39
+ };
40
+ }
41
+ function isIpcProtocolMessage(message) {
42
+ if (!isRecord(message)) return false;
43
+ if (message.type === "ipc-request") {
44
+ return typeof message.id === "string" && typeof message.method === "string" && Array.isArray(message.args);
45
+ }
46
+ if (message.type === "ipc-response") {
47
+ return typeof message.id === "string" && typeof message.method === "string" && (message.error === void 0 || isSerializedError(message.error));
48
+ }
49
+ return false;
50
+ }
51
+ function isSerializedError(value) {
52
+ return isRecord(value) && typeof value.name === "string" && typeof value.message === "string" && (value.stack === void 0 || typeof value.stack === "string");
53
+ }
54
+ function isRecord(value) {
55
+ return typeof value === "object" && value !== null;
56
+ }
57
+ export {
58
+ createChildProcessIpcTransport,
59
+ createParentProcessIpcTransport
60
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,68 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { fileURLToPath } from "node:url";
4
+ import { expect, test } from "vitest";
5
+ import { createIpcPeer } from "./ipc.js";
6
+ import { createChildProcessIpcTransport } from "./child-process-transport.js";
7
+ const execFileAsync = promisify(execFile);
8
+ test("throws a clear error without a child-process IPC channel", async () => {
9
+ const modulePath = fileURLToPath(
10
+ new URL("./child-process-transport.ts", import.meta.url)
11
+ );
12
+ await expect(
13
+ execFileAsync(
14
+ process.execPath,
15
+ [
16
+ "--import",
17
+ "tsx",
18
+ "-e",
19
+ `import { createParentProcessIpcTransport } from ${JSON.stringify(modulePath)}; createParentProcessIpcTransport();`
20
+ ],
21
+ { cwd: process.cwd() }
22
+ )
23
+ ).rejects.toMatchObject({
24
+ stderr: expect.stringContaining("process.send is not available")
25
+ });
26
+ });
27
+ test("lets parent and child process call each other", async () => {
28
+ const child = spawn(
29
+ process.execPath,
30
+ ["--import", "tsx", "--input-type=module", "-e", getChildFixtureSource()],
31
+ { stdio: ["ignore", "ignore", "ignore", "ipc"] }
32
+ );
33
+ child.send({ type: "not-an-ipc-protocol-message" });
34
+ const peer = createIpcPeer(
35
+ createChildProcessIpcTransport(child),
36
+ {
37
+ greet(name) {
38
+ return `hello ${name}`;
39
+ }
40
+ }
41
+ );
42
+ await expect(peer.call.double(21)).resolves.toBe(42);
43
+ await expect(peer.call.askParent("Ada")).resolves.toBe("hello Ada");
44
+ peer.destroy();
45
+ await new Promise((resolve) => child.once("exit", () => resolve()));
46
+ });
47
+ function getChildFixtureSource() {
48
+ const ipcModule = new URL("./ipc.ts", import.meta.url).href;
49
+ const transportModule = new URL(
50
+ "./child-process-transport.ts",
51
+ import.meta.url
52
+ ).href;
53
+ return `
54
+ import { createIpcPeer } from ${JSON.stringify(ipcModule)};
55
+ import { createParentProcessIpcTransport } from ${JSON.stringify(transportModule)};
56
+
57
+ const peer = createIpcPeer(createParentProcessIpcTransport(), {
58
+ double(value) {
59
+ return value * 2;
60
+ },
61
+ async askParent(name) {
62
+ return peer.call.greet(name);
63
+ },
64
+ });
65
+
66
+ process.on("disconnect", () => peer.destroy());
67
+ `;
68
+ }
@@ -0,0 +1,46 @@
1
+ type IpcTransport<T = unknown> = {
2
+ send(message: T): void | Promise<void>;
3
+ listen(callback: (message: T) => void): () => void;
4
+ onClose?(callback: (error?: Error) => void): () => void;
5
+ close?(): void;
6
+ };
7
+ type FunctionMap<T> = {
8
+ [K in keyof T]: (...args: never[]) => unknown;
9
+ };
10
+ type UnwrapPromise<T> = T extends Promise<infer Result> ? Result : T;
11
+ type MaybeAsync<T extends (...args: never[]) => unknown> = T extends (...args: infer Args) => infer Result ? (...args: Args) => UnwrapPromise<Result> | Promise<UnwrapPromise<Result>> : never;
12
+ type IpcPeerHandlers<Local extends FunctionMap<Local>> = {
13
+ [K in keyof Local]: MaybeAsync<Local[K]>;
14
+ };
15
+ type IpcPeerCalls<Remote extends FunctionMap<Remote>> = {
16
+ [K in keyof Remote]: Remote[K] extends (...args: infer Args) => infer Result ? (...args: Args) => Promise<UnwrapPromise<Result>> : never;
17
+ };
18
+ type IpcPeer<Remote extends FunctionMap<Remote>> = {
19
+ call: IpcPeerCalls<Remote>;
20
+ destroy(): void;
21
+ };
22
+ type IpcRequestMessage = {
23
+ type: "ipc-request";
24
+ id: string;
25
+ method: string;
26
+ args: unknown[];
27
+ };
28
+ type SerializedError = {
29
+ name: string;
30
+ message: string;
31
+ stack?: string;
32
+ code?: string | number;
33
+ cause?: SerializedError;
34
+ errors?: SerializedError[];
35
+ };
36
+ type IpcResponseMessage = {
37
+ type: "ipc-response";
38
+ id: string;
39
+ method: string;
40
+ data?: unknown;
41
+ error?: SerializedError;
42
+ };
43
+ type IpcProtocolMessage = IpcRequestMessage | IpcResponseMessage;
44
+ declare function createIpcPeer<Remote extends FunctionMap<Remote>, Local extends FunctionMap<Local>>(transport: IpcTransport<IpcProtocolMessage>, handlers: IpcPeerHandlers<Local>): IpcPeer<Remote>;
45
+
46
+ export { type IpcPeer, type IpcPeerCalls, type IpcPeerHandlers, type IpcProtocolMessage, type IpcTransport, createIpcPeer };
@@ -0,0 +1,165 @@
1
+ import { randomUUID } from "node:crypto";
2
+ function createIpcPeer(transport, handlers) {
3
+ const pending = /* @__PURE__ */ new Map();
4
+ let destroyed = false;
5
+ const stopListening = transport.listen((message) => {
6
+ if (message.type === "ipc-request") {
7
+ void handleRequest(message);
8
+ return;
9
+ }
10
+ handleResponse(message);
11
+ });
12
+ const stopCloseListener = transport.onClose?.((error) => {
13
+ destroy(error ?? new Error("IPC transport closed"));
14
+ });
15
+ async function handleRequest(message) {
16
+ if (destroyed) return;
17
+ const handler = handlers[message.method];
18
+ if (!handler) {
19
+ await sendResponse({
20
+ type: "ipc-response",
21
+ id: message.id,
22
+ method: message.method,
23
+ error: serializeError(
24
+ new Error(`No handler registered for method: ${message.method}`)
25
+ )
26
+ });
27
+ return;
28
+ }
29
+ try {
30
+ const data = await Promise.resolve(handler(...message.args));
31
+ await sendResponse({
32
+ type: "ipc-response",
33
+ id: message.id,
34
+ method: message.method,
35
+ data
36
+ });
37
+ } catch (error) {
38
+ await sendResponse({
39
+ type: "ipc-response",
40
+ id: message.id,
41
+ method: message.method,
42
+ error: serializeError(error)
43
+ });
44
+ }
45
+ }
46
+ async function sendResponse(message) {
47
+ try {
48
+ await transport.send(message);
49
+ } catch {
50
+ }
51
+ }
52
+ function handleResponse(message) {
53
+ const request = pending.get(message.id);
54
+ if (!request) return;
55
+ pending.delete(message.id);
56
+ if (message.error) {
57
+ request.reject(deserializeRemoteError(request.method, message.error));
58
+ return;
59
+ }
60
+ request.resolve(message.data);
61
+ }
62
+ const call = new Proxy({}, {
63
+ get: (_target, method) => {
64
+ if (typeof method !== "string") return void 0;
65
+ return (...args) => {
66
+ if (destroyed) {
67
+ return Promise.reject(new Error("IPC peer destroyed"));
68
+ }
69
+ const id = `${method}-${randomUUID()}`;
70
+ const promise = new Promise((resolve, reject) => {
71
+ pending.set(id, { method, resolve, reject });
72
+ });
73
+ void Promise.resolve(
74
+ transport.send({
75
+ type: "ipc-request",
76
+ id,
77
+ method,
78
+ args
79
+ })
80
+ ).catch((error) => {
81
+ const request = pending.get(id);
82
+ if (!request) return;
83
+ pending.delete(id);
84
+ request.reject(error);
85
+ });
86
+ return promise;
87
+ };
88
+ }
89
+ });
90
+ function destroy(error = new Error("IPC peer destroyed")) {
91
+ if (destroyed) return;
92
+ destroyed = true;
93
+ stopListening();
94
+ stopCloseListener?.();
95
+ transport.close?.();
96
+ for (const request of pending.values()) {
97
+ request.reject(error);
98
+ }
99
+ pending.clear();
100
+ }
101
+ return { call, destroy };
102
+ }
103
+ function serializeError(error, seen = /* @__PURE__ */ new WeakSet()) {
104
+ if (typeof error === "object" && error !== null) {
105
+ if (seen.has(error)) {
106
+ return {
107
+ name: "Error",
108
+ message: "[Circular]"
109
+ };
110
+ }
111
+ seen.add(error);
112
+ }
113
+ if (error instanceof Error) {
114
+ const serialized = {
115
+ name: error.name,
116
+ message: error.message,
117
+ stack: error.stack
118
+ };
119
+ const errorWithCode = error;
120
+ if (typeof errorWithCode.code === "string" || typeof errorWithCode.code === "number") {
121
+ serialized.code = errorWithCode.code;
122
+ }
123
+ if (error.cause !== void 0) {
124
+ serialized.cause = serializeError(error.cause, seen);
125
+ }
126
+ if (error instanceof AggregateError) {
127
+ serialized.errors = error.errors.map(
128
+ (aggregateError) => serializeError(aggregateError, seen)
129
+ );
130
+ }
131
+ return serialized;
132
+ }
133
+ return {
134
+ name: "NonError",
135
+ message: String(error)
136
+ };
137
+ }
138
+ function deserializeRemoteError(method, remoteError) {
139
+ const error = deserializeSerializedError(
140
+ remoteError,
141
+ `${method} > ${remoteError.message}`
142
+ );
143
+ error.stack = [new Error(method).stack, remoteError.stack].filter((stack) => typeof stack === "string").join("\n");
144
+ return error;
145
+ }
146
+ function deserializeSerializedError(serialized, message = serialized.message) {
147
+ const cause = serialized.cause ? deserializeSerializedError(serialized.cause) : void 0;
148
+ const error = serialized.name === "AggregateError" && serialized.errors ? new AggregateError(
149
+ serialized.errors.map(
150
+ (aggregateError) => deserializeSerializedError(aggregateError)
151
+ ),
152
+ message,
153
+ { cause }
154
+ ) : new Error(message, { cause });
155
+ error.name = serialized.name;
156
+ error.stack = serialized.stack;
157
+ const errorWithCode = error;
158
+ if (serialized.code !== void 0) {
159
+ errorWithCode.code = serialized.code;
160
+ }
161
+ return error;
162
+ }
163
+ export {
164
+ createIpcPeer
165
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }