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.
- package/README.md +3 -8
- package/README.template.md +3 -8
- package/dist/cli/cli.js +0 -23
- package/dist/cli/commands/auth.js +24 -33
- package/dist/cli/commands/billing.js +3 -5
- package/dist/cli/commands/browser.js +4 -13
- package/dist/cli/commands/deploy.js +54 -45
- package/dist/cli/commands/execution.js +6 -3
- package/dist/cli/commands/experiments.js +1 -1
- package/dist/cli/commands/setup.js +2 -295
- package/dist/cli/commands/shared.js +1 -1
- package/dist/cli/commands/snapshot.js +10 -100
- package/dist/cli/commands/status.js +2 -42
- package/dist/cli/core/auth-fetch.js +11 -6
- package/dist/cli/core/browser.js +13 -8
- package/dist/cli/core/config.js +3 -6
- package/dist/cli/core/daemon/daemon.js +88 -74
- package/dist/cli/core/daemon/exec-repl.js +133 -0
- package/dist/cli/core/daemon/exec.js +6 -21
- package/dist/cli/core/daemon/ipc.js +47 -4
- package/dist/cli/core/daemon/ipc.spec.js +21 -0
- package/dist/cli/core/daemon/snapshot.js +2 -29
- package/dist/cli/core/exec-compiler.js +8 -3
- package/dist/cli/core/experiments.js +1 -28
- package/dist/cli/core/providers/index.js +13 -4
- package/dist/cli/core/providers/libretto-cloud.js +178 -26
- package/dist/cli/index.js +0 -2
- package/dist/cli/router.js +9 -6
- package/dist/shared/instrumentation/instrument.js +4 -4
- package/dist/shared/ipc/socket-transport.d.ts +2 -1
- package/dist/shared/ipc/socket-transport.js +16 -5
- package/dist/shared/ipc/socket-transport.spec.js +5 -0
- package/docs/releasing.md +8 -6
- package/package.json +3 -2
- package/skills/libretto/SKILL.md +49 -47
- package/skills/libretto/references/code-generation-rules.md +6 -0
- package/skills/libretto/references/configuration-file-reference.md +14 -12
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +6 -6
- package/skills/libretto-readonly/SKILL.md +2 -9
- package/src/cli/cli.ts +0 -24
- package/src/cli/commands/auth.ts +24 -33
- package/src/cli/commands/billing.ts +3 -5
- package/src/cli/commands/browser.ts +6 -16
- package/src/cli/commands/deploy.ts +55 -49
- package/src/cli/commands/execution.ts +6 -3
- package/src/cli/commands/experiments.ts +1 -1
- package/src/cli/commands/setup.ts +2 -381
- package/src/cli/commands/shared.ts +1 -1
- package/src/cli/commands/snapshot.ts +9 -137
- package/src/cli/commands/status.ts +2 -50
- package/src/cli/core/auth-fetch.ts +9 -4
- package/src/cli/core/browser.ts +15 -8
- package/src/cli/core/config.ts +3 -6
- package/src/cli/core/daemon/daemon.ts +106 -76
- package/src/cli/core/daemon/exec-repl.ts +189 -0
- package/src/cli/core/daemon/exec.ts +8 -43
- package/src/cli/core/daemon/ipc.spec.ts +27 -0
- package/src/cli/core/daemon/ipc.ts +81 -23
- package/src/cli/core/daemon/snapshot.ts +1 -43
- package/src/cli/core/exec-compiler.ts +8 -3
- package/src/cli/core/experiments.ts +9 -38
- package/src/cli/core/providers/index.ts +17 -4
- package/src/cli/core/providers/libretto-cloud.ts +224 -36
- package/src/cli/core/resolve-model.ts +5 -0
- package/src/cli/core/workflow-runtime.ts +1 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/router.ts +9 -6
- package/src/shared/instrumentation/instrument.ts +4 -4
- package/src/shared/ipc/socket-transport.spec.ts +6 -0
- package/src/shared/ipc/socket-transport.ts +20 -5
- package/dist/cli/commands/ai.js +0 -110
- package/dist/cli/core/ai-model.js +0 -195
- package/dist/cli/core/api-snapshot-analyzer.js +0 -86
- package/dist/cli/core/snapshot-analyzer.js +0 -667
- package/dist/cli/framework/simple-cli.js +0 -880
- package/scripts/summarize-evals.mjs +0 -135
- package/src/cli/commands/ai.ts +0 -144
- package/src/cli/core/ai-model.ts +0 -301
- package/src/cli/core/api-snapshot-analyzer.ts +0 -110
- package/src/cli/core/snapshot-analyzer.ts +0 -856
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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,
|
|
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
|
-
|
|
47
|
-
browser,
|
|
48
|
-
state: execState,
|
|
49
|
-
console: buffered.console,
|
|
50
|
-
networkLog,
|
|
51
|
-
actionLog
|
|
42
|
+
frame: targetPage.mainFrame()
|
|
52
43
|
};
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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 =
|
|
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
|
};
|