libretto 0.5.0 → 0.5.1
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 +106 -36
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +30 -8
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +9 -2
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +132 -29
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +5 -2
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +10 -2
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +6 -13
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +7 -2
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +19 -10
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +113 -49
- package/skills/libretto/references/code-generation-rules.md +208 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +32 -9
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +12 -3
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +178 -41
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +19 -8
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +16 -3
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +15 -18
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +12 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +161 -148
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -3,142 +3,146 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { LoggerSink } from "./logger.js";
|
|
4
4
|
|
|
5
5
|
export function createFileLogSink({
|
|
6
|
-
|
|
6
|
+
filePath,
|
|
7
7
|
}: {
|
|
8
|
-
|
|
8
|
+
filePath: string;
|
|
9
9
|
}): LoggerSink {
|
|
10
|
-
|
|
10
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const writeStream = fs.createWriteStream(filePath, { flags: "a" });
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
return {
|
|
15
|
+
write: ({ id, scope, level, event, data, options }) => {
|
|
16
|
+
if (writeStream.destroyed || writeStream.writableEnded) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const timestamp = options?.timestamp || new Date();
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
const logEntry = {
|
|
22
|
+
timestamp: timestamp.toISOString(),
|
|
23
|
+
id,
|
|
24
|
+
level,
|
|
25
|
+
scope,
|
|
26
|
+
event,
|
|
27
|
+
data,
|
|
28
|
+
};
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const jsonLine = JSON.stringify(logEntry) + "\n";
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
32
|
+
try {
|
|
33
|
+
writeStream.write(jsonLine, (error) => {
|
|
34
|
+
if (error) {
|
|
35
|
+
console.error("Failed to write to log file:", error);
|
|
36
|
+
console[level]({ id, scope, event, data, timestamp });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("Failed to write to log file:", error);
|
|
41
|
+
console[level]({ id, scope, event, data, timestamp });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
flush: () =>
|
|
45
|
+
new Promise<void>((resolve, reject) => {
|
|
46
|
+
if (
|
|
47
|
+
!writeStream.writable ||
|
|
48
|
+
writeStream.writableEnded ||
|
|
49
|
+
writeStream.destroyed
|
|
50
|
+
) {
|
|
51
|
+
resolve();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
writeStream.write("", (error) => {
|
|
55
|
+
if (error) {
|
|
56
|
+
reject(error);
|
|
57
|
+
} else {
|
|
58
|
+
resolve();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
close: () =>
|
|
63
|
+
new Promise<void>((resolve) => {
|
|
64
|
+
if (writeStream.destroyed || writeStream.closed) {
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let settled = false;
|
|
69
|
+
const done = () => {
|
|
70
|
+
if (settled) return;
|
|
71
|
+
settled = true;
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
writeStream.once("finish", done);
|
|
75
|
+
writeStream.once("close", done);
|
|
76
|
+
writeStream.once("error", done);
|
|
77
|
+
try {
|
|
78
|
+
writeStream.end();
|
|
79
|
+
} catch {
|
|
80
|
+
done();
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
// ANSI color codes
|
|
83
87
|
const colors = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
reset: "\x1b[0m",
|
|
89
|
+
gray: "\x1b[90m",
|
|
90
|
+
red: "\x1b[31m",
|
|
91
|
+
yellow: "\x1b[33m",
|
|
92
|
+
blue: "\x1b[34m",
|
|
93
|
+
cyan: "\x1b[36m",
|
|
90
94
|
};
|
|
91
95
|
|
|
92
96
|
function formatTimestamp(date: Date): string {
|
|
93
|
-
|
|
97
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
export const prettyConsoleSink: LoggerSink = {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
write: ({ scope, level, event, data, options }) => {
|
|
102
|
+
const timestamp = `${colors.gray}${formatTimestamp(options?.timestamp || new Date())}${colors.reset}`;
|
|
103
|
+
const levelColor =
|
|
104
|
+
level === "error"
|
|
105
|
+
? colors.red
|
|
106
|
+
: level === "warn"
|
|
107
|
+
? colors.yellow
|
|
108
|
+
: colors.blue;
|
|
109
|
+
const coloredScope = scope ? `${colors.cyan}[${scope}]${colors.reset}` : "";
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
const logPrefix = `${timestamp} ${levelColor}${level.toUpperCase()}${colors.reset} ${coloredScope} ${event}`;
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
113
|
+
if (level === "error" && data.error) {
|
|
114
|
+
const { error, ...otherData } = data;
|
|
115
|
+
console.error(logPrefix);
|
|
116
|
+
if (error.stack) {
|
|
117
|
+
console.error(` ${error.stack}`);
|
|
118
|
+
} else if (error.type && error.message) {
|
|
119
|
+
console.error(` ${error.type}: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
if (Object.keys(otherData).length > 0) {
|
|
122
|
+
console.error(JSON.stringify(otherData, null, 2));
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
console[level](logPrefix);
|
|
126
|
+
if (Object.keys(data).length > 0) {
|
|
127
|
+
console[level](JSON.stringify(data, null, 2));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
127
131
|
};
|
|
128
132
|
|
|
129
133
|
export const jsonlConsoleSink: LoggerSink = {
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
write: ({ id, scope, level, event, data, options }) => {
|
|
135
|
+
const timestamp = options?.timestamp || new Date();
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
const logEntry = {
|
|
138
|
+
timestamp: timestamp.toISOString(),
|
|
139
|
+
id,
|
|
140
|
+
level,
|
|
141
|
+
scope: scope || undefined,
|
|
142
|
+
event,
|
|
143
|
+
data,
|
|
144
|
+
};
|
|
141
145
|
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
console.log(JSON.stringify(logEntry));
|
|
147
|
+
},
|
|
144
148
|
};
|
|
@@ -11,99 +11,100 @@ const PAUSED_SIGNAL_SUFFIX = "paused";
|
|
|
11
11
|
const RESUME_SIGNAL_SUFFIX = "resume";
|
|
12
12
|
|
|
13
13
|
function getLibrettoRoot(cwd: string = process.cwd()): string {
|
|
14
|
-
|
|
14
|
+
return join(resolveLibrettoRepoRoot(cwd), LIBRETTO_DIRNAME);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function getLibrettoSessionsDir(cwd: string = process.cwd()): string {
|
|
18
|
-
|
|
18
|
+
return join(getLibrettoRoot(cwd), LIBRETTO_SESSIONS_DIRNAME);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function getLibrettoSessionDir(
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
sessionName: string,
|
|
23
|
+
cwd: string = process.cwd(),
|
|
24
24
|
): string {
|
|
25
|
-
|
|
25
|
+
return join(getLibrettoSessionsDir(cwd), sessionName);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function getLibrettoSessionStatePath(
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
sessionName: string,
|
|
30
|
+
cwd: string = process.cwd(),
|
|
31
31
|
): string {
|
|
32
|
-
|
|
32
|
+
return join(getLibrettoSessionDir(sessionName, cwd), SESSION_STATE_FILENAME);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function getLibrettoPauseSignalDir(
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
sessionName: string,
|
|
37
|
+
cwd: string = process.cwd(),
|
|
38
38
|
): string {
|
|
39
|
-
|
|
39
|
+
return getLibrettoSessionDir(sessionName, cwd);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function getLibrettoRunnerLogDir(
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
sessionName: string,
|
|
44
|
+
cwd: string = process.cwd(),
|
|
45
45
|
): string {
|
|
46
|
-
|
|
46
|
+
return join(getLibrettoSessionDir(sessionName, cwd), RUNNER_LOG_DIRNAME);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function getRunnerLogPathForDir(logDir: string): string {
|
|
50
|
-
|
|
50
|
+
return join(logDir, RUNNER_LOG_FILENAME);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function getPauseSignalPathForDir(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
signalDir: string,
|
|
55
|
+
sessionName: string,
|
|
56
|
+
signal: "paused" | "resume",
|
|
57
57
|
): string {
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const suffix =
|
|
59
|
+
signal === "paused" ? PAUSED_SIGNAL_SUFFIX : RESUME_SIGNAL_SUFFIX;
|
|
60
|
+
return join(signalDir, `${sessionName}.${suffix}`);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function getLibrettoPausedSignalPath(
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
sessionName: string,
|
|
65
|
+
cwd: string = process.cwd(),
|
|
65
66
|
): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
return getPauseSignalPathForDir(
|
|
68
|
+
getLibrettoPauseSignalDir(sessionName, cwd),
|
|
69
|
+
sessionName,
|
|
70
|
+
"paused",
|
|
71
|
+
);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function getLibrettoResumeSignalPath(
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
sessionName: string,
|
|
76
|
+
cwd: string = process.cwd(),
|
|
76
77
|
): string {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
return getPauseSignalPathForDir(
|
|
79
|
+
getLibrettoPauseSignalDir(sessionName, cwd),
|
|
80
|
+
sessionName,
|
|
81
|
+
"resume",
|
|
82
|
+
);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export function ensureLibrettoSessionStatePath(
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
sessionName: string,
|
|
87
|
+
cwd: string = process.cwd(),
|
|
87
88
|
): string {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const filePath = getLibrettoSessionStatePath(sessionName, cwd);
|
|
90
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
91
|
+
return filePath;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export function ensureLibrettoPauseSignalDir(
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
sessionName: string,
|
|
96
|
+
cwd: string = process.cwd(),
|
|
96
97
|
): string {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const dir = getLibrettoPauseSignalDir(sessionName, cwd);
|
|
99
|
+
mkdirSync(dir, { recursive: true });
|
|
100
|
+
return dir;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
export function ensureLibrettoRunnerLogDir(
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
sessionName: string,
|
|
105
|
+
cwd: string = process.cwd(),
|
|
105
106
|
): string {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const dir = getLibrettoRunnerLogDir(sessionName, cwd);
|
|
108
|
+
mkdirSync(dir, { recursive: true });
|
|
109
|
+
return dir;
|
|
109
110
|
}
|
|
@@ -4,24 +4,24 @@ import { resolve } from "node:path";
|
|
|
4
4
|
const repoRootCache = new Map<string, string>();
|
|
5
5
|
|
|
6
6
|
export function resolveLibrettoRepoRoot(cwd: string = process.cwd()): string {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const override = process.env.LIBRETTO_REPO_ROOT?.trim();
|
|
8
|
+
if (override) {
|
|
9
|
+
return resolve(override);
|
|
10
|
+
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const normalizedCwd = resolve(cwd);
|
|
13
|
+
const cached = repoRootCache.get(normalizedCwd);
|
|
14
|
+
if (cached) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
19
|
+
cwd: normalizedCwd,
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
});
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const repoRoot =
|
|
24
|
+
result.status === 0 && result.stdout ? result.stdout.trim() : normalizedCwd;
|
|
25
|
+
repoRootCache.set(normalizedCwd, repoRoot);
|
|
26
|
+
return repoRoot;
|
|
27
27
|
}
|
package/src/shared/run/api.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chromium,
|
|
3
|
+
type Browser,
|
|
4
|
+
type BrowserContext,
|
|
5
|
+
type Page,
|
|
6
|
+
} from "playwright";
|
|
2
7
|
import { createServer } from "node:net";
|
|
3
8
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
9
|
import { ensureLibrettoSessionStatePath } from "../paths/paths.js";
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
SESSION_STATE_VERSION,
|
|
12
|
+
SessionStateFileSchema,
|
|
13
|
+
} from "../state/session-state.js";
|
|
6
14
|
|
|
7
15
|
async function pickFreePort(): Promise<number> {
|
|
8
16
|
return await new Promise((resolve, reject) => {
|
|
@@ -65,7 +73,8 @@ export async function launchBrowser({
|
|
|
65
73
|
? (JSON.parse(readFileSync(metadataPath, "utf-8")) as unknown)
|
|
66
74
|
: undefined;
|
|
67
75
|
|
|
68
|
-
const parsedExistingState =
|
|
76
|
+
const parsedExistingState =
|
|
77
|
+
SessionStateFileSchema.safeParse(existingStateRaw);
|
|
69
78
|
|
|
70
79
|
writeFileSync(
|
|
71
80
|
metadataPath,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
SESSION_STATE_VERSION,
|
|
3
|
+
SessionStatusSchema,
|
|
4
|
+
SessionStateFileSchema,
|
|
5
|
+
parseSessionStateData,
|
|
6
|
+
parseSessionStateContent,
|
|
7
|
+
serializeSessionState,
|
|
8
|
+
type SessionStatus,
|
|
9
|
+
type SessionState,
|
|
10
|
+
type SessionStateFile,
|
|
11
11
|
} from "./session-state.js";
|
|
@@ -3,25 +3,26 @@ import { z } from "zod";
|
|
|
3
3
|
export const SESSION_STATE_VERSION = 1;
|
|
4
4
|
|
|
5
5
|
export const SessionStatusSchema = z.enum([
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
"active",
|
|
7
|
+
"paused",
|
|
8
|
+
"completed",
|
|
9
|
+
"failed",
|
|
10
|
+
"exited",
|
|
11
11
|
]);
|
|
12
12
|
export const SessionViewportSchema = z.object({
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
width: z.number().int().min(1),
|
|
14
|
+
height: z.number().int().min(1),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
export const SessionStateFileSchema = z.object({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
version: z.literal(SESSION_STATE_VERSION),
|
|
19
|
+
port: z.number().int().min(0).max(65535),
|
|
20
|
+
pid: z.number().int().optional(),
|
|
21
|
+
cdpEndpoint: z.string().url().optional(),
|
|
22
|
+
session: z.string().min(1),
|
|
23
|
+
startedAt: z.string().datetime({ offset: true }),
|
|
24
|
+
status: SessionStatusSchema.optional(),
|
|
25
|
+
viewport: SessionViewportSchema.optional(),
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
export type SessionStatus = z.infer<typeof SessionStatusSchema>;
|
|
@@ -29,46 +30,48 @@ export type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
|
|
|
29
30
|
export type SessionState = Omit<SessionStateFile, "version">;
|
|
30
31
|
|
|
31
32
|
function formatIssues(error: z.ZodError): string {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
return error.issues
|
|
34
|
+
.map((issue) => {
|
|
35
|
+
const path = issue.path.join(".") || "root";
|
|
36
|
+
return `${path}: ${issue.message}`;
|
|
37
|
+
})
|
|
38
|
+
.join("; ");
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function parseSessionStateData(
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
rawState: unknown,
|
|
43
|
+
source: string,
|
|
43
44
|
): SessionState {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const parsed = SessionStateFileSchema.safeParse(rawState);
|
|
46
|
+
if (!parsed.success) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Session state at ${source} is invalid: ${formatIssues(parsed.error)}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
const { version: _version, ...state } = parsed.data;
|
|
53
|
+
return state;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export function parseSessionStateContent(
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
content: string,
|
|
58
|
+
source: string,
|
|
56
59
|
): SessionState {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
let rawState: unknown;
|
|
61
|
+
try {
|
|
62
|
+
rawState = JSON.parse(content);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Session state at ${source} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
return parseSessionStateData(rawState, source);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export function serializeSessionState(state: SessionState): SessionStateFile {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
return SessionStateFileSchema.parse({
|
|
74
|
+
version: SESSION_STATE_VERSION,
|
|
75
|
+
...state,
|
|
76
|
+
});
|
|
74
77
|
}
|