libretto 0.6.12 → 0.6.14

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 (82) hide show
  1. package/README.md +3 -8
  2. package/README.template.md +3 -8
  3. package/dist/cli/cli.js +0 -23
  4. package/dist/cli/commands/auth.js +24 -33
  5. package/dist/cli/commands/billing.js +3 -5
  6. package/dist/cli/commands/browser.js +4 -13
  7. package/dist/cli/commands/deploy.js +54 -45
  8. package/dist/cli/commands/execution.js +6 -3
  9. package/dist/cli/commands/experiments.js +1 -1
  10. package/dist/cli/commands/setup.js +2 -295
  11. package/dist/cli/commands/shared.js +1 -1
  12. package/dist/cli/commands/snapshot.js +10 -100
  13. package/dist/cli/commands/status.js +2 -42
  14. package/dist/cli/core/auth-fetch.js +11 -6
  15. package/dist/cli/core/browser.js +13 -8
  16. package/dist/cli/core/config.js +3 -6
  17. package/dist/cli/core/daemon/daemon.js +88 -74
  18. package/dist/cli/core/daemon/exec-repl.js +133 -0
  19. package/dist/cli/core/daemon/exec.js +6 -21
  20. package/dist/cli/core/daemon/ipc.js +47 -4
  21. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  22. package/dist/cli/core/daemon/snapshot.js +2 -29
  23. package/dist/cli/core/exec-compiler.js +8 -3
  24. package/dist/cli/core/experiments.js +1 -28
  25. package/dist/cli/core/providers/index.js +13 -4
  26. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  27. package/dist/cli/index.js +0 -2
  28. package/dist/cli/router.js +9 -6
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  31. package/dist/shared/ipc/socket-transport.js +16 -5
  32. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  33. package/docs/releasing.md +8 -6
  34. package/package.json +3 -2
  35. package/skills/libretto/SKILL.md +49 -47
  36. package/skills/libretto/references/code-generation-rules.md +6 -0
  37. package/skills/libretto/references/configuration-file-reference.md +14 -12
  38. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  39. package/skills/libretto/references/site-security-review.md +6 -6
  40. package/skills/libretto-readonly/SKILL.md +2 -9
  41. package/src/cli/cli.ts +0 -24
  42. package/src/cli/commands/auth.ts +24 -33
  43. package/src/cli/commands/billing.ts +3 -5
  44. package/src/cli/commands/browser.ts +6 -16
  45. package/src/cli/commands/deploy.ts +55 -49
  46. package/src/cli/commands/execution.ts +6 -3
  47. package/src/cli/commands/experiments.ts +1 -1
  48. package/src/cli/commands/setup.ts +2 -381
  49. package/src/cli/commands/shared.ts +1 -1
  50. package/src/cli/commands/snapshot.ts +9 -137
  51. package/src/cli/commands/status.ts +2 -50
  52. package/src/cli/core/auth-fetch.ts +9 -4
  53. package/src/cli/core/browser.ts +15 -8
  54. package/src/cli/core/config.ts +3 -6
  55. package/src/cli/core/daemon/daemon.ts +106 -76
  56. package/src/cli/core/daemon/exec-repl.ts +189 -0
  57. package/src/cli/core/daemon/exec.ts +8 -43
  58. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  59. package/src/cli/core/daemon/ipc.ts +81 -23
  60. package/src/cli/core/daemon/snapshot.ts +1 -43
  61. package/src/cli/core/exec-compiler.ts +8 -3
  62. package/src/cli/core/experiments.ts +9 -38
  63. package/src/cli/core/providers/index.ts +17 -4
  64. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  65. package/src/cli/core/resolve-model.ts +5 -0
  66. package/src/cli/core/workflow-runtime.ts +1 -0
  67. package/src/cli/index.ts +0 -1
  68. package/src/cli/router.ts +9 -6
  69. package/src/shared/instrumentation/instrument.ts +4 -4
  70. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  71. package/src/shared/ipc/socket-transport.ts +20 -5
  72. package/dist/cli/commands/ai.js +0 -110
  73. package/dist/cli/core/ai-model.js +0 -195
  74. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  75. package/dist/cli/core/snapshot-analyzer.js +0 -667
  76. package/dist/cli/framework/simple-cli.js +0 -880
  77. package/scripts/summarize-evals.mjs +0 -135
  78. package/src/cli/commands/ai.ts +0 -144
  79. package/src/cli/core/ai-model.ts +0 -301
  80. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  81. package/src/cli/core/snapshot-analyzer.ts +0 -856
  82. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -31,7 +31,8 @@ import {
31
31
  } from "../browser.js";
32
32
  import { handlePages } from "./pages.js";
33
33
  import { handleExec, handleReadonlyExec } from "./exec.js";
34
- import { handleCompactSnapshot, handleSnapshot } from "./snapshot.js";
34
+ import { DaemonExecRepl } from "./exec-repl.js";
35
+ import { handleCompactSnapshot } from "./snapshot.js";
35
36
  import { librettoCommand } from "../../../shared/package-manager.js";
36
37
  import { snapshot } from "../../../shared/snapshot/capture-snapshot.js";
37
38
  import { diffSnapshots } from "../../../shared/snapshot/diff-snapshots.js";
@@ -105,9 +106,13 @@ class BrowserDaemon {
105
106
  this.page = page;
106
107
  this.providerSession = providerSession;
107
108
  this.logger = logger.withScope("child");
109
+ this.execRepl = new DaemonExecRepl({
110
+ browser: this.browser,
111
+ context: this.context
112
+ });
108
113
  }
109
114
  logger;
110
- execState = {};
115
+ execRepl;
111
116
  pageById = /* @__PURE__ */ new Map();
112
117
  shutdownHandlers = [];
113
118
  connectedClis = /* @__PURE__ */ new Set();
@@ -146,7 +151,8 @@ class BrowserDaemon {
146
151
  initialPages,
147
152
  navigateUrl,
148
153
  readyProvider,
149
- providerSession
154
+ providerSession,
155
+ beforeReady
150
156
  } = args;
151
157
  await mkdir(getSessionDir(session), { recursive: true });
152
158
  const networkLogFile = getSessionNetworkLogPath(session);
@@ -170,9 +176,7 @@ class BrowserDaemon {
170
176
  error: err instanceof Error ? err.message : String(err)
171
177
  });
172
178
  }
173
- if (experiments["compact-snapshot-format"]) {
174
- await context.addInitScript(installPageStabilityWaiter);
175
- }
179
+ await context.addInitScript(installPageStabilityWaiter);
176
180
  const socketPath = getDaemonSocketPath(session);
177
181
  const daemon = new BrowserDaemon(
178
182
  session,
@@ -188,19 +192,15 @@ class BrowserDaemon {
188
192
  wrapPageForActionLogging(p, session);
189
193
  daemon.trackPage(p);
190
194
  }
191
- if (experiments["compact-snapshot-format"]) {
192
- await Promise.all(
193
- initialPages.map(
194
- (initialPage) => daemon.installCompactSnapshotWaiter(initialPage)
195
- )
196
- );
197
- }
195
+ await Promise.all(
196
+ initialPages.map(
197
+ (initialPage) => daemon.installCompactSnapshotWaiter(initialPage)
198
+ )
199
+ );
198
200
  context.on("page", (newPage) => {
199
201
  wrapPageForActionLogging(newPage, session);
200
202
  daemon.trackPage(newPage);
201
- if (experiments["compact-snapshot-format"]) {
202
- void daemon.installCompactSnapshotWaiter(newPage);
203
- }
203
+ void daemon.installCompactSnapshotWaiter(newPage);
204
204
  });
205
205
  if (navigateUrl) {
206
206
  await page.goto(navigateUrl);
@@ -239,6 +239,7 @@ class BrowserDaemon {
239
239
  });
240
240
  });
241
241
  await listenOnIpcSocket(ipcServer, socketPath);
242
+ beforeReady?.();
242
243
  process.send?.({ type: "ready", socketPath, provider: readyProvider });
243
244
  daemon.logger.info("ipc-server-listening", { socketPath });
244
245
  browser.on("disconnected", () => {
@@ -321,8 +322,13 @@ class BrowserDaemon {
321
322
  static async connectToProvider(args) {
322
323
  const { session, browser: config } = args;
323
324
  const provider = getCloudProviderApi(config.providerName);
324
- const providerSession = await provider.createSession();
325
+ let providerSession;
326
+ const startupCleanup = createProviderStartupCleanup({
327
+ provider,
328
+ getProviderSession: () => providerSession
329
+ });
325
330
  try {
331
+ providerSession = await provider.createSession();
326
332
  const browser = await chromium.connectOverCDP(
327
333
  providerSession.cdpEndpoint
328
334
  );
@@ -349,7 +355,8 @@ class BrowserDaemon {
349
355
  provider,
350
356
  name: config.providerName,
351
357
  sessionId: providerSession.sessionId
352
- }
358
+ },
359
+ beforeReady: startupCleanup.dispose
353
360
  });
354
361
  daemon.logger.info("child-provider-connected", {
355
362
  provider: config.providerName,
@@ -360,7 +367,10 @@ class BrowserDaemon {
360
367
  });
361
368
  return daemon;
362
369
  } catch (error) {
363
- await provider.closeSession(providerSession.sessionId);
370
+ startupCleanup.dispose();
371
+ if (providerSession) {
372
+ await provider.closeSession(providerSession.sessionId);
373
+ }
364
374
  throw error;
365
375
  }
366
376
  }
@@ -465,38 +475,23 @@ class BrowserDaemon {
465
475
  };
466
476
  }
467
477
  async runSnapshot(args) {
468
- if (args.mode === "compact") {
469
- if (!this.experiments["compact-snapshot-format"]) {
470
- throw new Error(
471
- `The compact-snapshot-format experiment is not enabled for session "${this.session}". Close and reopen the session after running ${librettoCommand("experiments enable compact-snapshot-format")}.`
472
- );
473
- }
474
- const targetPage = this.resolveTargetPage(args.pageId);
475
- const result = await this.withRequestTimeout(
476
- () => handleCompactSnapshot(
477
- targetPage,
478
- this.session,
479
- this.logger,
480
- {
481
- pageId: args.pageId,
482
- cachedSnapshot: this.latestCompactSnapshotByPage.get(targetPage),
483
- useCachedSnapshot: args.useCachedSnapshot
484
- }
485
- )
486
- );
487
- if (!args.useCachedSnapshot) {
488
- this.latestCompactSnapshotByPage.set(targetPage, result.snapshot);
489
- }
490
- return result;
491
- }
492
- return this.withRequestTimeout(
493
- () => handleSnapshot(
494
- this.resolveTargetPage(args.pageId),
478
+ const targetPage = this.resolveTargetPage(args.pageId);
479
+ const result = await this.withRequestTimeout(
480
+ () => handleCompactSnapshot(
481
+ targetPage,
495
482
  this.session,
496
483
  this.logger,
497
- args.pageId
484
+ {
485
+ pageId: args.pageId,
486
+ cachedSnapshot: this.latestCompactSnapshotByPage.get(targetPage),
487
+ useCachedSnapshot: args.useCachedSnapshot
488
+ }
498
489
  )
499
490
  );
491
+ if (!args.useCachedSnapshot) {
492
+ this.latestCompactSnapshotByPage.set(targetPage, result.snapshot);
493
+ }
494
+ return result;
500
495
  }
501
496
  async withRequestTimeout(operation) {
502
497
  let timerId;
@@ -513,25 +508,7 @@ class BrowserDaemon {
513
508
  }
514
509
  }
515
510
  async runExec(args) {
516
- if (this.experiments["compact-snapshot-format"]) {
517
- return this.runCompactExec(args);
518
- }
519
- try {
520
- const data = await this.withRequestTimeout(
521
- () => handleExec(
522
- this.resolveTargetPage(args.pageId),
523
- args.code,
524
- this.context,
525
- this.browser,
526
- this.execState,
527
- this.session,
528
- args.visualize
529
- )
530
- );
531
- return { ok: true, data };
532
- } catch (error) {
533
- return this.createExecErrorResult(error);
534
- }
511
+ return this.runCompactExec(args);
535
512
  }
536
513
  async runCompactExec(args) {
537
514
  let targetPage;
@@ -543,10 +520,7 @@ class BrowserDaemon {
543
520
  const result = await handleExec(
544
521
  page,
545
522
  args.code,
546
- this.context,
547
- this.browser,
548
- this.execState,
549
- this.session,
523
+ this.execRepl,
550
524
  args.visualize
551
525
  );
552
526
  try {
@@ -606,17 +580,17 @@ class BrowserDaemon {
606
580
  context: this.context,
607
581
  logger: this.logger,
608
582
  onLog: (event) => {
609
- this.broadcast("workflowOutput", event);
583
+ void this.broadcast("workflowOutput", event);
610
584
  },
611
585
  onOutcome: (outcome) => {
612
586
  if (outcome.state === "paused") {
613
- this.broadcast("workflowPaused", {
587
+ void this.broadcast("workflowPaused", {
614
588
  pausedAt: outcome.pausedAt,
615
589
  url: outcome.url
616
590
  });
617
591
  return;
618
592
  }
619
- this.broadcast(
593
+ void this.broadcast(
620
594
  "workflowFinished",
621
595
  outcome.result === "completed" ? { result: "completed", completedAt: outcome.completedAt } : {
622
596
  result: "failed",
@@ -659,6 +633,44 @@ class BrowserDaemon {
659
633
  }
660
634
  }
661
635
  }
636
+ function createProviderStartupCleanup(args) {
637
+ let disposed = false;
638
+ let fallbackExit;
639
+ const requestClose = (reason) => {
640
+ if (disposed) return;
641
+ disposed = true;
642
+ process.exitCode = reason === "received SIGINT" ? 130 : 1;
643
+ const providerSession = args.getProviderSession();
644
+ if (!providerSession) {
645
+ fallbackExit = setTimeout(() => {
646
+ process.exit(process.exitCode);
647
+ }, 5e3);
648
+ fallbackExit.unref();
649
+ return;
650
+ }
651
+ void args.provider.closeSession(providerSession.sessionId).catch(() => {
652
+ }).finally(() => {
653
+ process.exit(reason === "received SIGINT" ? 130 : 1);
654
+ });
655
+ };
656
+ const onDisconnect = () => requestClose("parent command disconnected");
657
+ const onSigint = () => requestClose("received SIGINT");
658
+ const onSigterm = () => requestClose("received SIGTERM");
659
+ if (typeof process.send === "function") {
660
+ process.once("disconnect", onDisconnect);
661
+ }
662
+ process.once("SIGINT", onSigint);
663
+ process.once("SIGTERM", onSigterm);
664
+ return {
665
+ dispose: () => {
666
+ disposed = true;
667
+ if (fallbackExit) clearTimeout(fallbackExit);
668
+ process.off("disconnect", onDisconnect);
669
+ process.off("SIGINT", onSigint);
670
+ process.off("SIGTERM", onSigterm);
671
+ }
672
+ };
673
+ }
662
674
  async function main() {
663
675
  const config = JSON.parse(process.argv[2]);
664
676
  const headed = config.browser.kind === "launch" ? config.browser.headed : false;
@@ -735,7 +747,9 @@ function reportStartupError(error) {
735
747
  message: error.message
736
748
  });
737
749
  }
738
- process.exit(1);
750
+ process.exit(
751
+ process.exitCode && process.exitCode !== 0 ? process.exitCode : 1
752
+ );
739
753
  }
740
754
  try {
741
755
  await main();
@@ -0,0 +1,133 @@
1
+ import * as repl from "node:repl";
2
+ import { PassThrough } from "node:stream";
3
+ import { format, formatWithOptions } from "node:util";
4
+ import { stripTypeScriptExecCode } from "../exec-compiler.js";
5
+ const PROMPT = "__LIBRETTO_EXEC_REPL_READY__";
6
+ const TOP_LEVEL_RETURN_HINT = "Hint: top-level return isn't supported because exec is a REPL-style environment. Use the expression value instead, for example: await page.title()";
7
+ const NO_RESULT = /* @__PURE__ */ Symbol("NO_RESULT");
8
+ function getErrorMessage(error) {
9
+ if (error instanceof Error) return error.message;
10
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
11
+ return error.message;
12
+ }
13
+ return String(error);
14
+ }
15
+ function isTopLevelReturnError(error) {
16
+ const message = getErrorMessage(error);
17
+ return message.includes("Illegal return statement") || message.includes("Return statement is not allowed here");
18
+ }
19
+ function isErrorLike(value) {
20
+ return value instanceof Error || typeof value === "object" && value !== null && "name" in value && "message" in value && typeof value.name === "string" && typeof value.message === "string";
21
+ }
22
+ function toError(value) {
23
+ return value instanceof Error ? value : new Error(getErrorMessage(value));
24
+ }
25
+ function appendTopLevelReturnHint(error) {
26
+ const message = getErrorMessage(error);
27
+ if (message.includes(TOP_LEVEL_RETURN_HINT)) {
28
+ return error instanceof Error ? error : new Error(message);
29
+ }
30
+ return new SyntaxError(`${message}
31
+
32
+ ${TOP_LEVEL_RETURN_HINT}`);
33
+ }
34
+ function createBufferedConsole() {
35
+ const output = { stdout: "", stderr: "" };
36
+ const writeStdout = (...args) => {
37
+ output.stdout += `${format(...args)}
38
+ `;
39
+ };
40
+ const writeStderr = (...args) => {
41
+ output.stderr += `${format(...args)}
42
+ `;
43
+ };
44
+ const bufferedConsole = {
45
+ ...globalThis.console,
46
+ log: writeStdout,
47
+ info: writeStdout,
48
+ debug: writeStdout,
49
+ dir: (value, options) => {
50
+ output.stdout += `${formatWithOptions(options ?? {}, value)}
51
+ `;
52
+ },
53
+ warn: writeStderr,
54
+ error: writeStderr
55
+ };
56
+ return { console: bufferedConsole, output };
57
+ }
58
+ class DaemonExecRepl {
59
+ replServer;
60
+ input = new PassThrough();
61
+ output = new PassThrough();
62
+ readyResolve;
63
+ ready;
64
+ activeEval;
65
+ lastResult = NO_RESULT;
66
+ constructor(globals) {
67
+ this.ready = new Promise((resolve) => {
68
+ this.readyResolve = resolve;
69
+ });
70
+ this.output.on("data", (chunk) => {
71
+ this.handleOutput(String(chunk));
72
+ });
73
+ this.replServer = repl.start({
74
+ prompt: PROMPT,
75
+ input: this.input,
76
+ output: this.output,
77
+ terminal: true,
78
+ useGlobal: false,
79
+ writer: (value) => {
80
+ this.lastResult = value;
81
+ return "";
82
+ }
83
+ });
84
+ Object.assign(this.replServer.context, globals);
85
+ }
86
+ async run(code, globals) {
87
+ Object.assign(this.replServer.context, globals);
88
+ let jsCode;
89
+ try {
90
+ jsCode = stripTypeScriptExecCode(code);
91
+ } catch (error) {
92
+ return {
93
+ ok: false,
94
+ error: isTopLevelReturnError(error) ? appendTopLevelReturnHint(error) : toError(error),
95
+ output: { stdout: "", stderr: "" }
96
+ };
97
+ }
98
+ await this.ready;
99
+ const buffered = createBufferedConsole();
100
+ Object.assign(this.replServer.context, { console: buffered.console });
101
+ return await new Promise((resolve) => {
102
+ this.activeEval = { output: "", consoleOutput: buffered.output, resolve };
103
+ this.lastResult = NO_RESULT;
104
+ this.input.write(`.editor
105
+ ${jsCode}
106
+ `);
107
+ });
108
+ }
109
+ handleOutput(chunk) {
110
+ const active = this.activeEval;
111
+ if (!active) {
112
+ if (chunk.includes(PROMPT)) {
113
+ this.readyResolve?.();
114
+ this.readyResolve = void 0;
115
+ }
116
+ return;
117
+ }
118
+ active.output += chunk;
119
+ if (!active.output.includes(PROMPT)) return;
120
+ this.activeEval = void 0;
121
+ const result = this.lastResult === NO_RESULT ? void 0 : this.lastResult;
122
+ const output = active.consoleOutput;
123
+ if (isErrorLike(result)) {
124
+ const error = isTopLevelReturnError(result) ? appendTopLevelReturnHint(result) : toError(result);
125
+ active.resolve({ ok: false, error, output });
126
+ return;
127
+ }
128
+ active.resolve({ ok: true, result, output });
129
+ }
130
+ }
131
+ export {
132
+ DaemonExecRepl
133
+ };
@@ -2,7 +2,6 @@ import { format, formatWithOptions } from "node:util";
2
2
  import { installInstrumentation } from "../../../shared/instrumentation/index.js";
3
3
  import { compileExecFunction } from "../exec-compiler.js";
4
4
  import { createReadonlyExecHelpers } from "../readonly-exec.js";
5
- import { readNetworkLog, readActionLog } from "../telemetry.js";
6
5
  class DaemonExecError extends Error {
7
6
  constructor(message, output) {
8
7
  super(message);
@@ -34,33 +33,19 @@ function createBufferedConsole() {
34
33
  };
35
34
  return { console: bufferedConsole, output };
36
35
  }
37
- async function handleExec(targetPage, code, context, browser, execState, session, visualize) {
38
- const buffered = createBufferedConsole();
36
+ async function handleExec(targetPage, code, execRepl, visualize) {
39
37
  if (visualize) {
40
38
  await installInstrumentation(targetPage, { visualize: true });
41
39
  }
42
- const networkLog = (opts = {}) => readNetworkLog(session, opts);
43
- const actionLog = (opts = {}) => readActionLog(session, opts);
44
40
  const helpers = {
45
41
  page: targetPage,
46
- context,
47
- browser,
48
- state: execState,
49
- console: buffered.console,
50
- networkLog,
51
- actionLog
42
+ frame: targetPage.mainFrame()
52
43
  };
53
- const helperNames = Object.keys(helpers);
54
- const fn = compileExecFunction(code, helperNames);
55
- try {
56
- const result = await fn(...Object.values(helpers));
57
- return { result, output: buffered.output };
58
- } catch (error) {
59
- throw new DaemonExecError(
60
- error instanceof Error ? error.message : String(error),
61
- buffered.output
62
- );
44
+ const result = await execRepl.run(code, helpers);
45
+ if (!result.ok) {
46
+ throw new DaemonExecError(result.error.message, result.output);
63
47
  }
48
+ return { result: result.result, output: result.output };
64
49
  }
65
50
  async function handleReadonlyExec(targetPage, code) {
66
51
  const buffered = createBufferedConsole();
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { openSync, closeSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
+ import { homedir, userInfo } from "node:os";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { createIpcPeer } from "../../../shared/ipc/ipc.js";
7
8
  import { connectToIpcSocket } from "../../../shared/ipc/socket-transport.js";
@@ -33,9 +34,27 @@ function isDaemonStartupErrorMessage(message) {
33
34
  const candidate = message;
34
35
  return candidate.type === "startup-error" && typeof candidate.message === "string";
35
36
  }
36
- function getDaemonSocketPath(session) {
37
- const hash = createHash("sha256").update(`${REPO_ROOT}:${session}`).digest("hex").slice(0, 12);
38
- return `/tmp/libretto-${process.getuid()}-${hash}.sock`;
37
+ function isDaemonStartupStatusMessage(message) {
38
+ if (typeof message !== "object" || message === null) return false;
39
+ const candidate = message;
40
+ return candidate.type === "startup-status" && typeof candidate.message === "string";
41
+ }
42
+ function getDaemonSocketPath(session, platform = process.platform) {
43
+ const userKey = getDaemonUserKey();
44
+ const hash = createHash("sha256").update(`${REPO_ROOT}:${session}:${userKey}`).digest("hex").slice(0, 12);
45
+ if (platform === "win32") {
46
+ return `\\\\.\\pipe\\libretto-${hash}`;
47
+ }
48
+ return `/tmp/libretto-${userKey}-${hash}.sock`;
49
+ }
50
+ function getDaemonUserKey() {
51
+ if (typeof process.getuid === "function") return String(process.getuid());
52
+ try {
53
+ const info = userInfo();
54
+ if (info.username) return info.username;
55
+ } catch {
56
+ }
57
+ return createHash("sha256").update(homedir()).digest("hex").slice(0, 12);
39
58
  }
40
59
  class DaemonClient {
41
60
  constructor(ipc) {
@@ -105,6 +124,13 @@ class DaemonClient {
105
124
  },
106
125
  onExit: (code, signal, ready) => {
107
126
  logger.warn("daemon-exit", { code, signal, session, pid, ready });
127
+ },
128
+ onStatus: (message) => {
129
+ logger.info("daemon-startup-status", {
130
+ session,
131
+ message: message.message
132
+ });
133
+ console.log(message.message);
108
134
  }
109
135
  }).catch(async (error) => {
110
136
  try {
@@ -131,7 +157,8 @@ class DaemonClient {
131
157
  formatExitError,
132
158
  onReady,
133
159
  onSpawnError,
134
- onExit
160
+ onExit,
161
+ onStatus
135
162
  } = args;
136
163
  return new Promise((resolve, reject) => {
137
164
  let ready = false;
@@ -141,6 +168,8 @@ class DaemonClient {
141
168
  child.off("message", onMessage);
142
169
  child.off("error", onError);
143
170
  child.off("exit", onChildExit);
171
+ process.off("SIGINT", onParentSigint);
172
+ process.off("SIGTERM", onParentSigterm);
144
173
  };
145
174
  const fail = (error) => {
146
175
  cleanup();
@@ -152,6 +181,10 @@ class DaemonClient {
152
181
  fail(new Error(message.message));
153
182
  return;
154
183
  }
184
+ if (isDaemonStartupStatusMessage(message)) {
185
+ onStatus?.(message);
186
+ return;
187
+ }
155
188
  if (!isDaemonReadyMessage(message)) return;
156
189
  ready = true;
157
190
  cleanup();
@@ -167,9 +200,19 @@ class DaemonClient {
167
200
  if (ready) return;
168
201
  fail(formatExitError(code, signal));
169
202
  };
203
+ const forwardSignalToChild = (signal) => {
204
+ try {
205
+ child.kill(signal);
206
+ } catch {
207
+ }
208
+ };
209
+ const onParentSigint = () => forwardSignalToChild("SIGINT");
210
+ const onParentSigterm = () => forwardSignalToChild("SIGTERM");
170
211
  child.on("message", onMessage);
171
212
  child.on("error", onError);
172
213
  child.on("exit", onChildExit);
214
+ process.once("SIGINT", onParentSigint);
215
+ process.once("SIGTERM", onParentSigterm);
173
216
  });
174
217
  }
175
218
  async ping() {
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { getDaemonSocketPath } from "./ipc.js";
3
+ describe("daemon IPC endpoint paths", () => {
4
+ test("uses a Windows named pipe path on Windows", () => {
5
+ const socketPath = getDaemonSocketPath("windows-session", "win32");
6
+ expect(socketPath).toMatch(/^\\\\\.\\pipe\\libretto-[a-f0-9]{12}$/);
7
+ expect(socketPath).not.toContain("/tmp/");
8
+ expect(socketPath).not.toContain(".sock");
9
+ });
10
+ test("uses a short Unix socket path on Unix-like platforms", () => {
11
+ const socketPath = getDaemonSocketPath("unix-session", "linux");
12
+ expect(socketPath).toMatch(/^\/tmp\/libretto-.+-[a-f0-9]{12}\.sock$/);
13
+ });
14
+ test("keeps daemon IPC endpoints deterministic per session", () => {
15
+ const firstPath = getDaemonSocketPath("stable-session", "linux");
16
+ const secondPath = getDaemonSocketPath("stable-session", "linux");
17
+ const otherPath = getDaemonSocketPath("other-session", "linux");
18
+ expect(secondPath).toBe(firstPath);
19
+ expect(otherPath).not.toBe(firstPath);
20
+ });
21
+ });
@@ -1,4 +1,4 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync } from "node:fs";
2
2
  import { getSessionSnapshotRunDir } from "../context.js";
3
3
  import {
4
4
  snapshot
@@ -13,32 +13,6 @@ import {
13
13
  forceSnapshotViewport
14
14
  } from "../../commands/snapshot.js";
15
15
  const RENDER_SETTLE_TIMEOUT_MS = 1e4;
16
- async function handleSnapshot(targetPage, session, logger, pageId) {
17
- const screenshot = await captureSnapshotScreenshot(
18
- targetPage,
19
- session,
20
- logger,
21
- pageId
22
- );
23
- const htmlPath = `${getSessionSnapshotRunDir(
24
- session,
25
- screenshot.snapshotRunId
26
- )}/page.html`;
27
- const htmlContent = await targetPage.content();
28
- writeFileSync(htmlPath, htmlContent);
29
- logger.info("screenshot-success", {
30
- session,
31
- pageUrl: screenshot.pageUrl,
32
- title: screenshot.title,
33
- pngPath: screenshot.pngPath,
34
- htmlPath,
35
- snapshotRunId: screenshot.snapshotRunId
36
- });
37
- return {
38
- ...screenshot,
39
- htmlPath
40
- };
41
- }
42
16
  async function handleCompactSnapshot(targetPage, session, logger, options = {}) {
43
17
  if (options.useCachedSnapshot) {
44
18
  if (!options.cachedSnapshot) {
@@ -147,6 +121,5 @@ async function captureSnapshotScreenshot(targetPage, session, logger, pageId) {
147
121
  };
148
122
  }
149
123
  export {
150
- handleCompactSnapshot,
151
- handleSnapshot
124
+ handleCompactSnapshot
152
125
  };
@@ -1,5 +1,11 @@
1
1
  import * as moduleBuiltin from "node:module";
2
2
  const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
3
+ function stripTypeScriptExecCode(code) {
4
+ if (!stripTypeScriptTypes) return code;
5
+ return withSuppressedStripTypeScriptWarning(
6
+ () => stripTypeScriptTypes(code, { mode: "strip" })
7
+ );
8
+ }
3
9
  function withSuppressedStripTypeScriptWarning(action) {
4
10
  const mutableProcess = process;
5
11
  const originalEmitWarning = mutableProcess.emitWarning;
@@ -24,9 +30,7 @@ function compileTypeScriptExecFunction(code, helperNames) {
24
30
  const wrappedSource = `(async function __librettoExec(${helperNames.join(", ")}) {
25
31
  ${code}
26
32
  })`;
27
- const jsSource = withSuppressedStripTypeScriptWarning(
28
- () => stripTypeScriptTypes(wrappedSource, { mode: "strip" })
29
- );
33
+ const jsSource = stripTypeScriptExecCode(wrappedSource);
30
34
  const createFunction = new Function(
31
35
  `return ${jsSource}`
32
36
  );
@@ -107,5 +111,6 @@ export {
107
111
  compileExecFunction,
108
112
  compileTypeScriptExecFunction,
109
113
  stripEmptyCatchHandlers,
114
+ stripTypeScriptExecCode,
110
115
  withSuppressedStripTypeScriptWarning
111
116
  };