libretto 0.6.8 → 0.6.10
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/cli.js +2 -0
- package/dist/cli/commands/auth.js +535 -0
- package/dist/cli/commands/billing.js +74 -0
- package/dist/cli/commands/browser.js +8 -3
- package/dist/cli/commands/deploy.js +2 -7
- package/dist/cli/commands/execution.js +112 -137
- package/dist/cli/commands/snapshot.js +38 -126
- package/dist/cli/core/ai-model.js +0 -3
- package/dist/cli/core/auth-fetch.js +195 -0
- package/dist/cli/core/auth-storage.js +52 -0
- package/dist/cli/core/browser.js +151 -206
- package/dist/cli/core/daemon/config.js +6 -0
- package/dist/cli/core/daemon/daemon.js +298 -0
- package/dist/cli/core/daemon/exec.js +86 -0
- package/dist/cli/core/daemon/index.js +16 -0
- package/dist/cli/core/daemon/ipc.js +171 -0
- package/dist/cli/core/daemon/pages.js +15 -0
- package/dist/cli/core/daemon/snapshot.js +86 -0
- package/dist/cli/core/daemon/spawn.js +90 -0
- package/dist/cli/core/exec-compiler.js +111 -0
- package/dist/cli/core/prompt.js +72 -0
- package/dist/cli/core/providers/browserbase.js +1 -0
- package/dist/cli/core/providers/kernel.js +1 -0
- package/dist/cli/core/providers/libretto-cloud.js +6 -7
- package/dist/cli/core/readonly-exec.js +1 -1
- package/dist/cli/router.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +0 -5
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +2 -1
- package/docs/browser-automation-approaches.md +435 -0
- package/docs/releasing.md +117 -0
- package/package.json +4 -3
- package/skills/libretto/SKILL.md +14 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +2 -0
- package/src/cli/commands/auth.ts +787 -0
- package/src/cli/commands/billing.ts +133 -0
- package/src/cli/commands/browser.ts +8 -2
- package/src/cli/commands/deploy.ts +2 -7
- package/src/cli/commands/execution.ts +139 -187
- package/src/cli/commands/snapshot.ts +46 -143
- package/src/cli/core/ai-model.ts +4 -5
- package/src/cli/core/auth-fetch.ts +283 -0
- package/src/cli/core/auth-storage.ts +102 -0
- package/src/cli/core/browser.ts +182 -245
- package/src/cli/core/daemon/config.ts +46 -0
- package/src/cli/core/daemon/daemon.ts +429 -0
- package/src/cli/core/daemon/exec.ts +128 -0
- package/src/cli/core/daemon/index.ts +24 -0
- package/src/cli/core/daemon/ipc.ts +294 -0
- package/src/cli/core/daemon/pages.ts +21 -0
- package/src/cli/core/daemon/snapshot.ts +114 -0
- package/src/cli/core/daemon/spawn.ts +171 -0
- package/src/cli/core/exec-compiler.ts +169 -0
- package/src/cli/core/prompt.ts +94 -0
- package/src/cli/core/providers/browserbase.ts +1 -0
- package/src/cli/core/providers/kernel.ts +1 -0
- package/src/cli/core/providers/libretto-cloud.ts +13 -7
- package/src/cli/core/providers/types.ts +12 -1
- package/src/cli/core/readonly-exec.ts +2 -1
- package/src/cli/router.ts +4 -0
- package/src/cli/workers/run-integration-runtime.ts +0 -6
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/browser-daemon.js +0 -122
- package/src/cli/core/browser-daemon.ts +0 -198
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared exec compilation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the daemon (daemon/exec.ts) and the connect-based
|
|
5
|
+
* session path (execution.ts) to compile user-provided code strings
|
|
6
|
+
* into callable async functions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as moduleBuiltin from "node:module";
|
|
10
|
+
|
|
11
|
+
export type ExecFunction = (...args: unknown[]) => Promise<unknown>;
|
|
12
|
+
|
|
13
|
+
type StripTypeScriptTypesFn = (
|
|
14
|
+
code: string,
|
|
15
|
+
options?: { mode?: "strip" | "transform" },
|
|
16
|
+
) => string;
|
|
17
|
+
|
|
18
|
+
const stripTypeScriptTypes = (
|
|
19
|
+
moduleBuiltin as { stripTypeScriptTypes?: StripTypeScriptTypesFn }
|
|
20
|
+
).stripTypeScriptTypes;
|
|
21
|
+
|
|
22
|
+
export function withSuppressedStripTypeScriptWarning<T>(action: () => T): T {
|
|
23
|
+
type EmitWarningFn = (...args: unknown[]) => void;
|
|
24
|
+
const mutableProcess = process as unknown as { emitWarning: EmitWarningFn };
|
|
25
|
+
const originalEmitWarning = mutableProcess.emitWarning;
|
|
26
|
+
|
|
27
|
+
mutableProcess.emitWarning = (...args: unknown[]) => {
|
|
28
|
+
const warning = args[0];
|
|
29
|
+
const typeOrOptions = args[1];
|
|
30
|
+
const warningMessage =
|
|
31
|
+
typeof warning === "string"
|
|
32
|
+
? warning
|
|
33
|
+
: warning instanceof Error
|
|
34
|
+
? warning.message
|
|
35
|
+
: "";
|
|
36
|
+
const warningType =
|
|
37
|
+
typeof typeOrOptions === "string"
|
|
38
|
+
? typeOrOptions
|
|
39
|
+
: typeof typeOrOptions === "object" &&
|
|
40
|
+
typeOrOptions !== null &&
|
|
41
|
+
"type" in typeOrOptions &&
|
|
42
|
+
typeof (typeOrOptions as { type?: unknown }).type === "string"
|
|
43
|
+
? ((typeOrOptions as { type?: string }).type ?? "")
|
|
44
|
+
: "";
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
warningType === "ExperimentalWarning" &&
|
|
48
|
+
warningMessage.includes("stripTypeScriptTypes")
|
|
49
|
+
) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
originalEmitWarning(...args);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
return action();
|
|
57
|
+
} finally {
|
|
58
|
+
mutableProcess.emitWarning = originalEmitWarning;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function compileTypeScriptExecFunction(
|
|
63
|
+
code: string,
|
|
64
|
+
helperNames: string[],
|
|
65
|
+
): ExecFunction | null {
|
|
66
|
+
if (!stripTypeScriptTypes) return null;
|
|
67
|
+
|
|
68
|
+
const wrappedSource = `(async function __librettoExec(${helperNames.join(", ")}) {\n${code}\n})`;
|
|
69
|
+
const jsSource = withSuppressedStripTypeScriptWarning(() =>
|
|
70
|
+
stripTypeScriptTypes(wrappedSource, { mode: "strip" }),
|
|
71
|
+
);
|
|
72
|
+
const createFunction = new Function(
|
|
73
|
+
`return ${jsSource}`,
|
|
74
|
+
) as () => ExecFunction;
|
|
75
|
+
return createFunction();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function compileExecFunction(
|
|
79
|
+
code: string,
|
|
80
|
+
helperNames: string[],
|
|
81
|
+
): ExecFunction {
|
|
82
|
+
const typeStripped = compileTypeScriptExecFunction(code, helperNames);
|
|
83
|
+
if (typeStripped) return typeStripped;
|
|
84
|
+
|
|
85
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {})
|
|
86
|
+
.constructor as new (...args: string[]) => ExecFunction;
|
|
87
|
+
return new AsyncFunction(...helperNames, code);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Strip `.catch(() => {})` / `?.catch(() => {})` from executable code,
|
|
92
|
+
* skipping occurrences inside string literals (single, double, backtick)
|
|
93
|
+
* and single-line / multi-line comments so we never corrupt non-code text.
|
|
94
|
+
*/
|
|
95
|
+
export function stripEmptyCatchHandlers(code: string): {
|
|
96
|
+
cleaned: string;
|
|
97
|
+
strippedCount: number;
|
|
98
|
+
} {
|
|
99
|
+
const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
|
|
100
|
+
let strippedCount = 0;
|
|
101
|
+
let result = "";
|
|
102
|
+
let i = 0;
|
|
103
|
+
|
|
104
|
+
while (i < code.length) {
|
|
105
|
+
// Single-line comment
|
|
106
|
+
if (code[i] === "/" && code[i + 1] === "/") {
|
|
107
|
+
const end = code.indexOf("\n", i);
|
|
108
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
|
|
109
|
+
result += slice;
|
|
110
|
+
i += slice.length;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Multi-line comment
|
|
114
|
+
if (code[i] === "/" && code[i + 1] === "*") {
|
|
115
|
+
const end = code.indexOf("*/", i + 2);
|
|
116
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
|
|
117
|
+
result += slice;
|
|
118
|
+
i += slice.length;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// String literals
|
|
122
|
+
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
123
|
+
const quote = code[i];
|
|
124
|
+
let j = i + 1;
|
|
125
|
+
while (j < code.length) {
|
|
126
|
+
if (code[j] === "\\" && quote !== "`") {
|
|
127
|
+
j += 2;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (code[j] === "\\" && quote === "`") {
|
|
131
|
+
j += 2;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (code[j] === quote) {
|
|
135
|
+
j++;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
// Template literal interpolation — skip nested braces
|
|
139
|
+
if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
|
|
140
|
+
let depth = 1;
|
|
141
|
+
j += 2;
|
|
142
|
+
while (j < code.length && depth > 0) {
|
|
143
|
+
if (code[j] === "{") depth++;
|
|
144
|
+
else if (code[j] === "}") depth--;
|
|
145
|
+
j++;
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
j++;
|
|
150
|
+
}
|
|
151
|
+
result += code.slice(i, j);
|
|
152
|
+
i = j;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
// Try to match the catch pattern at the current position
|
|
156
|
+
catchRe.lastIndex = i;
|
|
157
|
+
const match = catchRe.exec(code);
|
|
158
|
+
if (match && match.index === i) {
|
|
159
|
+
strippedCount++;
|
|
160
|
+
i += match[0].length;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Regular character
|
|
164
|
+
result += code[i];
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { cleaned: result, strippedCount };
|
|
169
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal interactive prompts for the auth CLI commands. Uses node:readline
|
|
3
|
+
* for normal input and a raw-mode terminal for password entry so the
|
|
4
|
+
* password is masked as the user types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
|
|
10
|
+
export async function prompt(
|
|
11
|
+
question: string,
|
|
12
|
+
opts: { defaultValue?: string } = {},
|
|
13
|
+
): Promise<string> {
|
|
14
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
15
|
+
try {
|
|
16
|
+
const display = opts.defaultValue
|
|
17
|
+
? `${question} (${opts.defaultValue}) `
|
|
18
|
+
: `${question} `;
|
|
19
|
+
const answer = (await rl.question(display)).trim();
|
|
20
|
+
if (answer.length === 0 && opts.defaultValue !== undefined) {
|
|
21
|
+
return opts.defaultValue;
|
|
22
|
+
}
|
|
23
|
+
return answer;
|
|
24
|
+
} finally {
|
|
25
|
+
rl.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CTRL_C = "";
|
|
30
|
+
const CR = "\r";
|
|
31
|
+
const LF = "\n";
|
|
32
|
+
const BACKSPACE = "";
|
|
33
|
+
const DELETE = "";
|
|
34
|
+
|
|
35
|
+
export async function promptPassword(question: string): Promise<string> {
|
|
36
|
+
if (!stdin.isTTY) {
|
|
37
|
+
// Non-interactive (piped input) — fall back to plain readline so
|
|
38
|
+
// scripted tests still work; the password just won't be masked.
|
|
39
|
+
return prompt(question);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stdout.write(`${question} `);
|
|
43
|
+
|
|
44
|
+
return new Promise<string>((resolve, reject) => {
|
|
45
|
+
let buffer = "";
|
|
46
|
+
const wasRaw = stdin.isRaw;
|
|
47
|
+
stdin.setRawMode(true);
|
|
48
|
+
stdin.resume();
|
|
49
|
+
stdin.setEncoding("utf8");
|
|
50
|
+
|
|
51
|
+
const cleanup = (): void => {
|
|
52
|
+
stdin.setRawMode(wasRaw);
|
|
53
|
+
stdin.pause();
|
|
54
|
+
stdin.removeListener("data", onData);
|
|
55
|
+
process.stdout.write("\n");
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const onData = (chunk: string): void => {
|
|
59
|
+
for (const ch of chunk) {
|
|
60
|
+
if (ch === LF || ch === CR) {
|
|
61
|
+
cleanup();
|
|
62
|
+
resolve(buffer);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (ch === CTRL_C) {
|
|
66
|
+
cleanup();
|
|
67
|
+
reject(new Error("Aborted."));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (ch === BACKSPACE || ch === DELETE) {
|
|
71
|
+
if (buffer.length > 0) {
|
|
72
|
+
buffer = buffer.slice(0, -1);
|
|
73
|
+
process.stdout.write("\b \b");
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Skip other control bytes (escape sequences from arrow keys, etc).
|
|
78
|
+
if (ch.charCodeAt(0) < 0x20) continue;
|
|
79
|
+
buffer += ch;
|
|
80
|
+
process.stdout.write("*");
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
stdin.on("data", onData);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function slugify(name: string): string {
|
|
89
|
+
return name
|
|
90
|
+
.trim()
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
93
|
+
.replace(/^-+|-+$/g, "");
|
|
94
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HOSTED_API_URL } from "../auth-fetch.js";
|
|
1
2
|
import type { ProviderApi } from "./types.js";
|
|
2
3
|
|
|
3
4
|
export function createLibrettoCloudProvider(): ProviderApi {
|
|
@@ -6,12 +7,7 @@ export function createLibrettoCloudProvider(): ProviderApi {
|
|
|
6
7
|
throw new Error(
|
|
7
8
|
"LIBRETTO_API_KEY is required for the Libretto Cloud provider.",
|
|
8
9
|
);
|
|
9
|
-
const
|
|
10
|
-
if (!apiUrl)
|
|
11
|
-
throw new Error(
|
|
12
|
-
"LIBRETTO_API_URL is required for the Libretto Cloud provider.",
|
|
13
|
-
);
|
|
14
|
-
const endpoint = apiUrl.replace(/\/$/, "");
|
|
10
|
+
const endpoint = HOSTED_API_URL;
|
|
15
11
|
|
|
16
12
|
// The Libretto Cloud API is an oRPC RPCHandler, not plain REST, so inputs
|
|
17
13
|
// must be wrapped as { json: ... } and outputs arrive the same way.
|
|
@@ -37,11 +33,17 @@ export function createLibrettoCloudProvider(): ProviderApi {
|
|
|
37
33
|
);
|
|
38
34
|
}
|
|
39
35
|
const { json } = (await resp.json()) as {
|
|
40
|
-
json: {
|
|
36
|
+
json: {
|
|
37
|
+
session_id: string;
|
|
38
|
+
cdp_url: string;
|
|
39
|
+
live_view_url: string | null;
|
|
40
|
+
recording_url: string | null;
|
|
41
|
+
};
|
|
41
42
|
};
|
|
42
43
|
return {
|
|
43
44
|
sessionId: json.session_id,
|
|
44
45
|
cdpEndpoint: json.cdp_url,
|
|
46
|
+
liveViewUrl: json.live_view_url ?? undefined,
|
|
45
47
|
};
|
|
46
48
|
},
|
|
47
49
|
async closeSession(sessionId) {
|
|
@@ -59,6 +61,10 @@ export function createLibrettoCloudProvider(): ProviderApi {
|
|
|
59
61
|
`Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`,
|
|
60
62
|
);
|
|
61
63
|
}
|
|
64
|
+
const { json } = (await resp.json()) as {
|
|
65
|
+
json: { replay_url: string | null };
|
|
66
|
+
};
|
|
67
|
+
return { replayUrl: json.replay_url ?? undefined };
|
|
62
68
|
},
|
|
63
69
|
};
|
|
64
70
|
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
export type ProviderSession = {
|
|
2
2
|
sessionId: string; // remote session id for cleanup
|
|
3
3
|
cdpEndpoint: string; // CDP WebSocket URL
|
|
4
|
+
// Provider-hosted URL for watching the session live while it's running.
|
|
5
|
+
// Only libretto-cloud surfaces this today; direct-SDK providers leave it
|
|
6
|
+
// undefined.
|
|
7
|
+
liveViewUrl?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ProviderCloseResult = {
|
|
11
|
+
// Provider-hosted URL for playback of the session recording, surfaced on
|
|
12
|
+
// successful close. Undefined when the provider didn't capture a
|
|
13
|
+
// recording or doesn't return one on close.
|
|
14
|
+
replayUrl?: string;
|
|
4
15
|
};
|
|
5
16
|
|
|
6
17
|
export type ProviderApi = {
|
|
7
18
|
createSession(): Promise<ProviderSession>;
|
|
8
|
-
closeSession(sessionId: string): Promise<
|
|
19
|
+
closeSession(sessionId: string): Promise<ProviderCloseResult>;
|
|
9
20
|
};
|
|
@@ -68,6 +68,7 @@ const LOCATOR_ALLOWED_PROPERTIES = new Set<string>([]);
|
|
|
68
68
|
|
|
69
69
|
type ReadonlyExecOptions = {
|
|
70
70
|
onActivity?: () => void;
|
|
71
|
+
console?: Console;
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
const readonlyPageCache = new WeakMap<Page, Page>();
|
|
@@ -273,7 +274,7 @@ export function createReadonlyExecHelpers(
|
|
|
273
274
|
"fetch is blocked in readonly-exec; use get() instead",
|
|
274
275
|
);
|
|
275
276
|
},
|
|
276
|
-
console,
|
|
277
|
+
console: options.console ?? console,
|
|
277
278
|
setTimeout,
|
|
278
279
|
setInterval,
|
|
279
280
|
clearTimeout,
|
package/src/cli/router.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { aiCommands } from "./commands/ai.js";
|
|
2
|
+
import { authCommands } from "./commands/auth.js";
|
|
3
|
+
import { billingCommands } from "./commands/billing.js";
|
|
2
4
|
import { browserCommands } from "./commands/browser.js";
|
|
3
5
|
import { deployCommand } from "./commands/deploy.js";
|
|
4
6
|
import { executionCommands } from "./commands/execution.js";
|
|
@@ -12,6 +14,8 @@ export const cliRoutes = {
|
|
|
12
14
|
deploy: deployCommand,
|
|
13
15
|
...executionCommands,
|
|
14
16
|
ai: aiCommands,
|
|
17
|
+
auth: authCommands,
|
|
18
|
+
billing: billingCommands,
|
|
15
19
|
setup: setupCommand,
|
|
16
20
|
status: statusCommand,
|
|
17
21
|
snapshot: snapshotCommand,
|
|
@@ -4,7 +4,6 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { cwd } from "node:process";
|
|
5
5
|
import { isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { loadEnv } from "../../shared/env/load-env.js";
|
|
8
7
|
import {
|
|
9
8
|
getDefaultWorkflowFromModuleExports,
|
|
10
9
|
getWorkflowsFromModuleExports,
|
|
@@ -196,11 +195,6 @@ async function runIntegrationInternal(
|
|
|
196
195
|
const { logger } = options;
|
|
197
196
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
198
197
|
|
|
199
|
-
const envPath = loadEnv();
|
|
200
|
-
if (envPath) {
|
|
201
|
-
logger.info("loaded-env", { path: envPath });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
198
|
const workflow = await loadDefaultWorkflow(absolutePath);
|
|
205
199
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
206
200
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
@@ -32,6 +32,7 @@ export const SessionStateFileSchema = z.object({
|
|
|
32
32
|
mode: SessionAccessModeSchema.default("write-access"),
|
|
33
33
|
viewport: SessionViewportSchema.optional(),
|
|
34
34
|
provider: ProviderStateSchema.optional(),
|
|
35
|
+
daemonSocketPath: z.string().optional(),
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
export type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { chromium } from "playwright";
|
|
2
|
-
import { mkdir, unlink } from "node:fs/promises";
|
|
3
|
-
import { appendFileSync } from "node:fs";
|
|
4
|
-
import { installSessionTelemetry } from "./session-telemetry.js";
|
|
5
|
-
import {
|
|
6
|
-
getSessionDir,
|
|
7
|
-
getSessionLogsPath,
|
|
8
|
-
getSessionNetworkLogPath,
|
|
9
|
-
getSessionActionsLogPath,
|
|
10
|
-
getSessionStatePath
|
|
11
|
-
} from "./context.js";
|
|
12
|
-
const config = JSON.parse(process.argv[2]);
|
|
13
|
-
const sessionDir = getSessionDir(config.session);
|
|
14
|
-
await mkdir(sessionDir, { recursive: true });
|
|
15
|
-
const logFile = getSessionLogsPath(config.session);
|
|
16
|
-
const networkLogFile = getSessionNetworkLogPath(config.session);
|
|
17
|
-
const actionsLogFile = getSessionActionsLogPath(config.session);
|
|
18
|
-
function childLog(level, event, data = {}) {
|
|
19
|
-
const entry = JSON.stringify({
|
|
20
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21
|
-
id: Math.random().toString(36).slice(2, 10),
|
|
22
|
-
level,
|
|
23
|
-
scope: "libretto.child",
|
|
24
|
-
event,
|
|
25
|
-
data
|
|
26
|
-
});
|
|
27
|
-
appendFileSync(logFile, entry + "\n");
|
|
28
|
-
}
|
|
29
|
-
function logAction(entry) {
|
|
30
|
-
appendFileSync(actionsLogFile, JSON.stringify(entry) + "\n");
|
|
31
|
-
}
|
|
32
|
-
function logNetwork(entry) {
|
|
33
|
-
appendFileSync(networkLogFile, JSON.stringify(entry) + "\n");
|
|
34
|
-
}
|
|
35
|
-
const windowPositionArg = config.windowPosition ? `--window-position=${config.windowPosition.x},${config.windowPosition.y}` : void 0;
|
|
36
|
-
const launchArgs = [
|
|
37
|
-
"--disable-blink-features=AutomationControlled",
|
|
38
|
-
`--remote-debugging-port=${config.port}`,
|
|
39
|
-
"--remote-debugging-address=127.0.0.1",
|
|
40
|
-
"--no-focus-on-check",
|
|
41
|
-
...windowPositionArg ? [windowPositionArg] : []
|
|
42
|
-
];
|
|
43
|
-
const browser = await chromium.launch({
|
|
44
|
-
headless: !config.headed,
|
|
45
|
-
args: launchArgs
|
|
46
|
-
});
|
|
47
|
-
async function cleanupSessionState() {
|
|
48
|
-
const sessionStatePath = getSessionStatePath(config.session);
|
|
49
|
-
try {
|
|
50
|
-
await unlink(sessionStatePath);
|
|
51
|
-
} catch (err) {
|
|
52
|
-
if (err.code !== "ENOENT") throw err;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
let shuttingDown = false;
|
|
56
|
-
let wakeDaemon;
|
|
57
|
-
const sleepPromise = new Promise((resolve) => {
|
|
58
|
-
wakeDaemon = resolve;
|
|
59
|
-
});
|
|
60
|
-
async function shutdown(reason, closeBrowser) {
|
|
61
|
-
if (shuttingDown) return;
|
|
62
|
-
shuttingDown = true;
|
|
63
|
-
try {
|
|
64
|
-
childLog("info", reason, { port: config.port });
|
|
65
|
-
await cleanupSessionState();
|
|
66
|
-
if (closeBrowser) await browser.close();
|
|
67
|
-
} finally {
|
|
68
|
-
wakeDaemon();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
browser.on("disconnected", () => {
|
|
72
|
-
void shutdown("browser-disconnected-exiting", false);
|
|
73
|
-
});
|
|
74
|
-
const context = await browser.newContext({
|
|
75
|
-
...config.storageStatePath ? { storageState: config.storageStatePath } : {},
|
|
76
|
-
viewport: {
|
|
77
|
-
width: config.viewport.width,
|
|
78
|
-
height: config.viewport.height
|
|
79
|
-
},
|
|
80
|
-
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
|
81
|
-
});
|
|
82
|
-
const page = await context.newPage();
|
|
83
|
-
page.setDefaultTimeout(3e4);
|
|
84
|
-
page.setDefaultNavigationTimeout(45e3);
|
|
85
|
-
await installSessionTelemetry({
|
|
86
|
-
context,
|
|
87
|
-
initialPage: page,
|
|
88
|
-
includeUserDomActions: true,
|
|
89
|
-
logAction,
|
|
90
|
-
logNetwork
|
|
91
|
-
});
|
|
92
|
-
await page.goto(config.url);
|
|
93
|
-
process.on("SIGTERM", () => {
|
|
94
|
-
void shutdown("child-sigterm", true);
|
|
95
|
-
});
|
|
96
|
-
process.on("SIGINT", () => {
|
|
97
|
-
void shutdown("child-sigint", true);
|
|
98
|
-
});
|
|
99
|
-
process.on("uncaughtException", (err) => {
|
|
100
|
-
childLog("error", "uncaught-exception", {
|
|
101
|
-
message: err.message,
|
|
102
|
-
stack: err.stack
|
|
103
|
-
});
|
|
104
|
-
process.exit(1);
|
|
105
|
-
});
|
|
106
|
-
process.on("unhandledRejection", (reason) => {
|
|
107
|
-
childLog("warn", "unhandled-rejection", { reason: String(reason) });
|
|
108
|
-
});
|
|
109
|
-
process.on("exit", (code) => {
|
|
110
|
-
childLog("info", "child-exit", {
|
|
111
|
-
code,
|
|
112
|
-
pid: process.pid,
|
|
113
|
-
port: config.port
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
childLog("info", "child-launched", {
|
|
117
|
-
port: config.port,
|
|
118
|
-
pid: process.pid,
|
|
119
|
-
session: config.session
|
|
120
|
-
});
|
|
121
|
-
await sleepPromise;
|
|
122
|
-
process.exit(0);
|