libretto 0.6.13 → 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/dist/cli/commands/auth.js +24 -33
- package/dist/cli/commands/billing.js +3 -5
- package/dist/cli/commands/browser.js +3 -6
- 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 +1 -1
- package/dist/cli/commands/shared.js +1 -1
- package/dist/cli/commands/snapshot.js +1 -1
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/auth-fetch.js +11 -6
- package/dist/cli/core/browser.js +10 -5
- package/dist/cli/core/daemon/daemon.js +63 -10
- 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/exec-compiler.js +8 -3
- package/dist/cli/core/providers/index.js +13 -4
- package/dist/cli/core/providers/libretto-cloud.js +178 -26
- package/dist/cli/router.js +9 -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/package.json +2 -2
- package/skills/libretto/SKILL.md +33 -29
- package/skills/libretto/references/code-generation-rules.md +6 -0
- package/skills/libretto/references/configuration-file-reference.md +8 -0
- package/skills/libretto/references/site-security-review.md +6 -6
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/auth.ts +24 -33
- package/src/cli/commands/billing.ts +3 -5
- package/src/cli/commands/browser.ts +5 -9
- 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 +1 -1
- package/src/cli/commands/shared.ts +1 -1
- package/src/cli/commands/snapshot.ts +1 -1
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/core/auth-fetch.ts +9 -4
- package/src/cli/core/browser.ts +12 -5
- package/src/cli/core/daemon/daemon.ts +81 -9
- 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 +76 -7
- package/src/cli/core/exec-compiler.ts +8 -3
- package/src/cli/core/providers/index.ts +17 -4
- package/src/cli/core/providers/libretto-cloud.ts +224 -36
- package/src/cli/router.ts +9 -4
- package/src/shared/ipc/socket-transport.spec.ts +6 -0
- package/src/shared/ipc/socket-transport.ts +20 -5
- package/dist/cli/framework/simple-cli.js +0 -880
- package/src/cli/framework/simple-cli.ts +0 -1459
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../browser.js";
|
|
32
32
|
import { handlePages } from "./pages.js";
|
|
33
33
|
import { handleExec, handleReadonlyExec } from "./exec.js";
|
|
34
|
+
import { DaemonExecRepl } from "./exec-repl.js";
|
|
34
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";
|
|
@@ -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);
|
|
@@ -233,6 +239,7 @@ class BrowserDaemon {
|
|
|
233
239
|
});
|
|
234
240
|
});
|
|
235
241
|
await listenOnIpcSocket(ipcServer, socketPath);
|
|
242
|
+
beforeReady?.();
|
|
236
243
|
process.send?.({ type: "ready", socketPath, provider: readyProvider });
|
|
237
244
|
daemon.logger.info("ipc-server-listening", { socketPath });
|
|
238
245
|
browser.on("disconnected", () => {
|
|
@@ -315,8 +322,13 @@ class BrowserDaemon {
|
|
|
315
322
|
static async connectToProvider(args) {
|
|
316
323
|
const { session, browser: config } = args;
|
|
317
324
|
const provider = getCloudProviderApi(config.providerName);
|
|
318
|
-
|
|
325
|
+
let providerSession;
|
|
326
|
+
const startupCleanup = createProviderStartupCleanup({
|
|
327
|
+
provider,
|
|
328
|
+
getProviderSession: () => providerSession
|
|
329
|
+
});
|
|
319
330
|
try {
|
|
331
|
+
providerSession = await provider.createSession();
|
|
320
332
|
const browser = await chromium.connectOverCDP(
|
|
321
333
|
providerSession.cdpEndpoint
|
|
322
334
|
);
|
|
@@ -343,7 +355,8 @@ class BrowserDaemon {
|
|
|
343
355
|
provider,
|
|
344
356
|
name: config.providerName,
|
|
345
357
|
sessionId: providerSession.sessionId
|
|
346
|
-
}
|
|
358
|
+
},
|
|
359
|
+
beforeReady: startupCleanup.dispose
|
|
347
360
|
});
|
|
348
361
|
daemon.logger.info("child-provider-connected", {
|
|
349
362
|
provider: config.providerName,
|
|
@@ -354,7 +367,10 @@ class BrowserDaemon {
|
|
|
354
367
|
});
|
|
355
368
|
return daemon;
|
|
356
369
|
} catch (error) {
|
|
357
|
-
|
|
370
|
+
startupCleanup.dispose();
|
|
371
|
+
if (providerSession) {
|
|
372
|
+
await provider.closeSession(providerSession.sessionId);
|
|
373
|
+
}
|
|
358
374
|
throw error;
|
|
359
375
|
}
|
|
360
376
|
}
|
|
@@ -504,10 +520,7 @@ class BrowserDaemon {
|
|
|
504
520
|
const result = await handleExec(
|
|
505
521
|
page,
|
|
506
522
|
args.code,
|
|
507
|
-
this.
|
|
508
|
-
this.browser,
|
|
509
|
-
this.execState,
|
|
510
|
-
this.session,
|
|
523
|
+
this.execRepl,
|
|
511
524
|
args.visualize
|
|
512
525
|
);
|
|
513
526
|
try {
|
|
@@ -620,6 +633,44 @@ class BrowserDaemon {
|
|
|
620
633
|
}
|
|
621
634
|
}
|
|
622
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
|
+
}
|
|
623
674
|
async function main() {
|
|
624
675
|
const config = JSON.parse(process.argv[2]);
|
|
625
676
|
const headed = config.browser.kind === "launch" ? config.browser.headed : false;
|
|
@@ -696,7 +747,9 @@ function reportStartupError(error) {
|
|
|
696
747
|
message: error.message
|
|
697
748
|
});
|
|
698
749
|
}
|
|
699
|
-
process.exit(
|
|
750
|
+
process.exit(
|
|
751
|
+
process.exitCode && process.exitCode !== 0 ? process.exitCode : 1
|
|
752
|
+
);
|
|
700
753
|
}
|
|
701
754
|
try {
|
|
702
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,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
|
};
|
|
@@ -2,7 +2,14 @@ import { readLibrettoConfig } from "../config.js";
|
|
|
2
2
|
import { createBrowserbaseProvider } from "./browserbase.js";
|
|
3
3
|
import { createKernelProvider } from "./kernel.js";
|
|
4
4
|
import { createLibrettoCloudProvider } from "./libretto-cloud.js";
|
|
5
|
-
const VALID_PROVIDERS = /* @__PURE__ */ new Set([
|
|
5
|
+
const VALID_PROVIDERS = /* @__PURE__ */ new Set([
|
|
6
|
+
"local",
|
|
7
|
+
"kernel",
|
|
8
|
+
"browserbase",
|
|
9
|
+
"libretto-cloud"
|
|
10
|
+
]);
|
|
11
|
+
const DEFAULT_PROVIDER_STARTUP_TIMEOUT_MS = 6e4;
|
|
12
|
+
const LIBRETTO_CLOUD_STARTUP_TIMEOUT_MS = 10 * 6e4;
|
|
6
13
|
function assertValidProviderName(value, source) {
|
|
7
14
|
if (!VALID_PROVIDERS.has(value)) {
|
|
8
15
|
throw new Error(
|
|
@@ -32,9 +39,7 @@ function getCloudProviderApi(name) {
|
|
|
32
39
|
case "browserbase":
|
|
33
40
|
return createBrowserbaseProvider();
|
|
34
41
|
case "libretto-cloud":
|
|
35
|
-
console.warn(
|
|
36
|
-
"Note: The libretto-cloud provider is in alpha."
|
|
37
|
-
);
|
|
42
|
+
console.warn("Note: The libretto-cloud provider is in alpha.");
|
|
38
43
|
return createLibrettoCloudProvider();
|
|
39
44
|
default:
|
|
40
45
|
throw new Error(
|
|
@@ -42,7 +47,11 @@ function getCloudProviderApi(name) {
|
|
|
42
47
|
);
|
|
43
48
|
}
|
|
44
49
|
}
|
|
50
|
+
function getProviderStartupTimeoutMs(providerName) {
|
|
51
|
+
return providerName === "libretto-cloud" ? LIBRETTO_CLOUD_STARTUP_TIMEOUT_MS : DEFAULT_PROVIDER_STARTUP_TIMEOUT_MS;
|
|
52
|
+
}
|
|
45
53
|
export {
|
|
46
54
|
getCloudProviderApi,
|
|
55
|
+
getProviderStartupTimeoutMs,
|
|
47
56
|
resolveProviderName
|
|
48
57
|
};
|