libretto 0.5.0 → 0.5.2
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 +109 -35
- 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 +34 -29
- 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 +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- 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 +20 -4
- 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 +17 -69
- 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/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- 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 +47 -3
- 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 +36 -14
- 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 +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- 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 +37 -33
- 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 +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- 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 +39 -4
- 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 +27 -82
- 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/dom-semantics.ts +68 -0
- 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 +65 -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 +180 -149
- 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
package/src/cli/core/browser.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chromium,
|
|
3
|
+
type Browser,
|
|
4
|
+
type BrowserContext,
|
|
5
|
+
type CDPSession,
|
|
6
|
+
type Page,
|
|
7
|
+
} from "playwright";
|
|
2
8
|
import { openSync, existsSync, writeFileSync } from "node:fs";
|
|
3
9
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -6,6 +12,14 @@ import { createRequire } from "node:module";
|
|
|
6
12
|
import { createServer } from "node:net";
|
|
7
13
|
import { spawn } from "node:child_process";
|
|
8
14
|
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
15
|
+
import {
|
|
16
|
+
filterSemanticClasses,
|
|
17
|
+
INTERACTIVE_ROLE_NAMES,
|
|
18
|
+
INTERACTIVE_TAG_NAMES,
|
|
19
|
+
isObfuscatedClass,
|
|
20
|
+
TEST_ATTRIBUTE_NAMES,
|
|
21
|
+
TRUSTED_ATTRIBUTE_NAMES,
|
|
22
|
+
} from "../../shared/dom-semantics.js";
|
|
9
23
|
import {
|
|
10
24
|
getSessionActionsLogPath,
|
|
11
25
|
getSessionNetworkLogPath,
|
|
@@ -66,13 +80,12 @@ export function hasProfile(domain: string): boolean {
|
|
|
66
80
|
return existsSync(getProfilePath(domain));
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
async function
|
|
70
|
-
|
|
83
|
+
async function tryConnectToCDP(
|
|
84
|
+
endpoint: string,
|
|
71
85
|
logger: LoggerApi,
|
|
72
86
|
timeoutMs: number = 5000,
|
|
73
87
|
): Promise<Browser | null> {
|
|
74
|
-
|
|
75
|
-
logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
|
|
88
|
+
logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
|
|
76
89
|
try {
|
|
77
90
|
const connectPromise = chromium.connectOverCDP(endpoint);
|
|
78
91
|
const timeoutPromise = new Promise<null>((resolve) =>
|
|
@@ -81,16 +94,15 @@ async function tryConnectToPort(
|
|
|
81
94
|
const browser = await Promise.race([connectPromise, timeoutPromise]);
|
|
82
95
|
if (browser) {
|
|
83
96
|
logger.info("cdp-connect-success", {
|
|
84
|
-
port,
|
|
85
97
|
endpoint,
|
|
86
98
|
contexts: browser.contexts().length,
|
|
87
99
|
});
|
|
88
100
|
} else {
|
|
89
|
-
logger.warn("cdp-connect-timeout", {
|
|
101
|
+
logger.warn("cdp-connect-timeout", { endpoint, timeoutMs });
|
|
90
102
|
}
|
|
91
103
|
return browser;
|
|
92
104
|
} catch (err) {
|
|
93
|
-
logger.error("cdp-connect-error", { error: err,
|
|
105
|
+
logger.error("cdp-connect-error", { error: err, endpoint });
|
|
94
106
|
return null;
|
|
95
107
|
}
|
|
96
108
|
}
|
|
@@ -135,10 +147,12 @@ async function resolvePageId(page: Page): Promise<string> {
|
|
|
135
147
|
const cdpSession: CDPSession = await page.context().newCDPSession(page);
|
|
136
148
|
try {
|
|
137
149
|
const targetInfo = await cdpSession.send("Target.getTargetInfo");
|
|
138
|
-
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
139
|
-
?.targetId;
|
|
150
|
+
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
151
|
+
?.targetInfo?.targetId;
|
|
140
152
|
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
141
|
-
throw new Error(
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Could not resolve target id for page at URL "${page.url()}".`,
|
|
155
|
+
);
|
|
142
156
|
}
|
|
143
157
|
return targetId;
|
|
144
158
|
} finally {
|
|
@@ -162,7 +176,10 @@ export async function listOpenPages(
|
|
|
162
176
|
): Promise<OpenPageSummary[]> {
|
|
163
177
|
const { browser, page: activePage } = await connect(session, logger);
|
|
164
178
|
try {
|
|
165
|
-
const pages = browser
|
|
179
|
+
const pages = browser
|
|
180
|
+
.contexts()
|
|
181
|
+
.flatMap((ctx) => ctx.pages())
|
|
182
|
+
.filter(isOperationalPage);
|
|
166
183
|
const pageRefs = await resolvePageReferences(pages);
|
|
167
184
|
return pageRefs.map(({ id, page }) => ({
|
|
168
185
|
id,
|
|
@@ -190,14 +207,15 @@ export async function connect(
|
|
|
190
207
|
}> {
|
|
191
208
|
logger.info("connect", { session, timeoutMs });
|
|
192
209
|
const state = readSessionStateOrThrow(session);
|
|
193
|
-
const
|
|
210
|
+
const endpoint = state.cdpEndpoint ?? `http://localhost:${state.port}`;
|
|
211
|
+
const browser = await tryConnectToCDP(endpoint, logger, timeoutMs);
|
|
194
212
|
if (!browser) {
|
|
195
213
|
logger.error("connect-no-browser", {
|
|
196
214
|
session,
|
|
197
|
-
|
|
215
|
+
endpoint,
|
|
198
216
|
pid: state.pid,
|
|
199
217
|
});
|
|
200
|
-
if (!isPidRunning(state.pid)) {
|
|
218
|
+
if (state.pid == null || !isPidRunning(state.pid)) {
|
|
201
219
|
clearSessionState(session, logger);
|
|
202
220
|
throw new Error(
|
|
203
221
|
`No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`,
|
|
@@ -205,7 +223,7 @@ export async function connect(
|
|
|
205
223
|
}
|
|
206
224
|
|
|
207
225
|
throw new Error(
|
|
208
|
-
`Could not connect to the browser for session "${session}" at
|
|
226
|
+
`Could not connect to the browser for session "${session}" at ${endpoint}, but the session process (pid ${state.pid}) is still running. Try the command again, or close and reopen the session if it stays stuck.`,
|
|
209
227
|
);
|
|
210
228
|
}
|
|
211
229
|
|
|
@@ -276,7 +294,10 @@ export async function connect(
|
|
|
276
294
|
return { browser, context, page, pageId: pageRef.id };
|
|
277
295
|
}
|
|
278
296
|
|
|
279
|
-
export async function runPages(
|
|
297
|
+
export async function runPages(
|
|
298
|
+
session: string,
|
|
299
|
+
logger: LoggerApi,
|
|
300
|
+
): Promise<void> {
|
|
280
301
|
logger.info("pages-start", { session });
|
|
281
302
|
const pageSummaries = await listOpenPages(session, logger);
|
|
282
303
|
|
|
@@ -294,7 +315,7 @@ export async function runPages(session: string, logger: LoggerApi): Promise<void
|
|
|
294
315
|
|
|
295
316
|
const DEFAULT_VIEWPORT = { width: 1366, height: 768 } as const;
|
|
296
317
|
|
|
297
|
-
function resolveViewport(
|
|
318
|
+
export function resolveViewport(
|
|
298
319
|
cliViewport: { width: number; height: number } | undefined,
|
|
299
320
|
logger: LoggerApi,
|
|
300
321
|
): { width: number; height: number } {
|
|
@@ -304,13 +325,33 @@ function resolveViewport(
|
|
|
304
325
|
}
|
|
305
326
|
const config = readLibrettoConfig();
|
|
306
327
|
if (config.viewport) {
|
|
307
|
-
logger.info("viewport-source", {
|
|
328
|
+
logger.info("viewport-source", {
|
|
329
|
+
source: "config",
|
|
330
|
+
viewport: config.viewport,
|
|
331
|
+
});
|
|
308
332
|
return config.viewport;
|
|
309
333
|
}
|
|
310
|
-
logger.info("viewport-source", {
|
|
334
|
+
logger.info("viewport-source", {
|
|
335
|
+
source: "default",
|
|
336
|
+
viewport: DEFAULT_VIEWPORT,
|
|
337
|
+
});
|
|
311
338
|
return DEFAULT_VIEWPORT;
|
|
312
339
|
}
|
|
313
340
|
|
|
341
|
+
function resolveWindowPosition(
|
|
342
|
+
logger: LoggerApi,
|
|
343
|
+
): { x: number; y: number } | undefined {
|
|
344
|
+
const config = readLibrettoConfig();
|
|
345
|
+
if (config.windowPosition) {
|
|
346
|
+
logger.info("window-position-source", {
|
|
347
|
+
source: "config",
|
|
348
|
+
windowPosition: config.windowPosition,
|
|
349
|
+
});
|
|
350
|
+
return config.windowPosition;
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
314
355
|
export async function runOpen(
|
|
315
356
|
rawUrl: string,
|
|
316
357
|
headed: boolean,
|
|
@@ -320,7 +361,8 @@ export async function runOpen(
|
|
|
320
361
|
): Promise<void> {
|
|
321
362
|
const url = normalizeUrl(rawUrl);
|
|
322
363
|
const viewport = resolveViewport(options?.viewport, logger);
|
|
323
|
-
|
|
364
|
+
const windowPosition = headed ? resolveWindowPosition(logger) : undefined;
|
|
365
|
+
logger.info("open-start", { url, headed, session, viewport, windowPosition });
|
|
324
366
|
assertSessionAvailableForStart(session, logger);
|
|
325
367
|
|
|
326
368
|
const port = await pickFreePort();
|
|
@@ -363,6 +405,49 @@ export async function runOpen(
|
|
|
363
405
|
const escapedActionsLogPath = actionsLogPath
|
|
364
406
|
.replace(/\\/g, "\\\\")
|
|
365
407
|
.replace(/'/g, "\\'");
|
|
408
|
+
const windowPositionArg = windowPosition
|
|
409
|
+
? `, '--window-position=${windowPosition.x},${windowPosition.y}'`
|
|
410
|
+
: "";
|
|
411
|
+
const windowBoundsSetupCode = windowPosition
|
|
412
|
+
? `
|
|
413
|
+
const requestedWindowBounds = { left: ${windowPosition.x}, top: ${windowPosition.y}, windowState: 'normal' };
|
|
414
|
+
const pageCdp = await context.newCDPSession(page);
|
|
415
|
+
let browserCdp;
|
|
416
|
+
try {
|
|
417
|
+
const targetInfo = await pageCdp.send('Target.getTargetInfo');
|
|
418
|
+
const targetId = targetInfo?.targetInfo?.targetId;
|
|
419
|
+
browserCdp = await browser.newBrowserCDPSession();
|
|
420
|
+
const windowResult = await browserCdp.send(
|
|
421
|
+
'Browser.getWindowForTarget',
|
|
422
|
+
targetId ? { targetId } : {},
|
|
423
|
+
);
|
|
424
|
+
await browserCdp.send('Browser.setWindowBounds', {
|
|
425
|
+
windowId: windowResult.windowId,
|
|
426
|
+
bounds: requestedWindowBounds,
|
|
427
|
+
});
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
429
|
+
const actualWindow = await browserCdp.send('Browser.getWindowBounds', {
|
|
430
|
+
windowId: windowResult.windowId,
|
|
431
|
+
});
|
|
432
|
+
childLog('info', 'window-bounds-set', {
|
|
433
|
+
windowId: windowResult.windowId,
|
|
434
|
+
requestedBounds: requestedWindowBounds,
|
|
435
|
+
actualBounds: actualWindow.bounds,
|
|
436
|
+
});
|
|
437
|
+
} catch (error) {
|
|
438
|
+
childLog('warn', 'window-bounds-set-failed', {
|
|
439
|
+
requestedBounds: requestedWindowBounds,
|
|
440
|
+
message: error instanceof Error ? error.message : String(error),
|
|
441
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
442
|
+
});
|
|
443
|
+
} finally {
|
|
444
|
+
await pageCdp.detach().catch(() => {});
|
|
445
|
+
if (browserCdp) {
|
|
446
|
+
await browserCdp.detach().catch(() => {});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
`
|
|
450
|
+
: "";
|
|
366
451
|
|
|
367
452
|
const launcherCode = `
|
|
368
453
|
import { chromium } from 'playwright';
|
|
@@ -375,14 +460,21 @@ const ACTIONS_LOG = '${escapedActionsLogPath}';
|
|
|
375
460
|
mkdirSync(dirname(NETWORK_LOG), { recursive: true });
|
|
376
461
|
|
|
377
462
|
// tsx/esbuild may emit __name() wrappers in Function#toString output.
|
|
378
|
-
const __name = (target, value) =>
|
|
379
|
-
|
|
463
|
+
const __name = (target, value) =>
|
|
464
|
+
Object.defineProperty(target, 'name', { value, configurable: true });
|
|
380
465
|
|
|
381
|
-
${
|
|
466
|
+
const TEST_ATTRIBUTE_NAMES = ${JSON.stringify([...TEST_ATTRIBUTE_NAMES])};
|
|
467
|
+
const TRUSTED_ATTRIBUTE_NAMES = ${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])};
|
|
468
|
+
const INTERACTIVE_TAG_NAMES = ${JSON.stringify([...INTERACTIVE_TAG_NAMES])};
|
|
469
|
+
const INTERACTIVE_ROLE_NAMES = ${JSON.stringify([...INTERACTIVE_ROLE_NAMES])};
|
|
470
|
+
const filterSemanticClasses = ${filterSemanticClasses.toString()};
|
|
471
|
+
const isObfuscatedClass = ${isObfuscatedClass.toString()};
|
|
382
472
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
473
|
+
${installSessionTelemetry.toString()}
|
|
474
|
+
|
|
475
|
+
function logAction(entry) {
|
|
476
|
+
appendFileSync(ACTIONS_LOG, JSON.stringify(entry) + '\\n');
|
|
477
|
+
}
|
|
386
478
|
|
|
387
479
|
function logNetwork(entry) {
|
|
388
480
|
appendFileSync(NETWORK_LOG, JSON.stringify(entry) + '\\n');
|
|
@@ -404,7 +496,7 @@ function childLog(level, event, data = {}) {
|
|
|
404
496
|
|
|
405
497
|
const browser = await chromium.launch({
|
|
406
498
|
headless: ${!headed},
|
|
407
|
-
args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'],
|
|
499
|
+
args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'${windowPositionArg}],
|
|
408
500
|
});
|
|
409
501
|
|
|
410
502
|
browser.on('disconnected', () => {
|
|
@@ -418,6 +510,7 @@ const context = await browser.newContext({
|
|
|
418
510
|
});
|
|
419
511
|
|
|
420
512
|
const page = await context.newPage();
|
|
513
|
+
${windowBoundsSetupCode}
|
|
421
514
|
page.setDefaultTimeout(30000);
|
|
422
515
|
page.setDefaultNavigationTimeout(45000);
|
|
423
516
|
|
|
@@ -474,8 +567,10 @@ await new Promise(() => {});
|
|
|
474
567
|
logger.info("open-child-spawned", { pid: child.pid, port, session });
|
|
475
568
|
|
|
476
569
|
let childSpawnError: Error | null = null;
|
|
477
|
-
let childEarlyExit: {
|
|
478
|
-
null;
|
|
570
|
+
let childEarlyExit: {
|
|
571
|
+
code: number | null;
|
|
572
|
+
signal: NodeJS.Signals | null;
|
|
573
|
+
} | null = null;
|
|
479
574
|
|
|
480
575
|
child.on("error", (err) => {
|
|
481
576
|
childSpawnError = err;
|
|
@@ -529,14 +624,17 @@ await new Promise(() => {});
|
|
|
529
624
|
logger.info("open-waiting-for-cdp", { attempt: i, port, session });
|
|
530
625
|
}
|
|
531
626
|
if (ready) {
|
|
532
|
-
writeSessionState(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
627
|
+
writeSessionState(
|
|
628
|
+
{
|
|
629
|
+
port,
|
|
630
|
+
pid: child.pid!,
|
|
631
|
+
session,
|
|
632
|
+
startedAt: new Date().toISOString(),
|
|
633
|
+
status: "active",
|
|
634
|
+
viewport,
|
|
635
|
+
},
|
|
636
|
+
logger,
|
|
637
|
+
);
|
|
540
638
|
logger.info("open-success", {
|
|
541
639
|
url,
|
|
542
640
|
mode: browserMode,
|
|
@@ -644,7 +742,10 @@ export async function runSave(
|
|
|
644
742
|
}
|
|
645
743
|
}
|
|
646
744
|
|
|
647
|
-
export async function runClose(
|
|
745
|
+
export async function runClose(
|
|
746
|
+
session: string,
|
|
747
|
+
logger: LoggerApi,
|
|
748
|
+
): Promise<void> {
|
|
648
749
|
logger.info("close-start", { session });
|
|
649
750
|
const state = readSessionState(session, logger);
|
|
650
751
|
if (!state) {
|
|
@@ -655,9 +756,10 @@ export async function runClose(session: string, logger: LoggerApi): Promise<void
|
|
|
655
756
|
|
|
656
757
|
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
657
758
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
759
|
+
if (state.pid != null) {
|
|
760
|
+
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
761
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
762
|
+
}
|
|
661
763
|
|
|
662
764
|
clearSessionState(session, logger);
|
|
663
765
|
logger.info("close-success", { session });
|
|
@@ -666,7 +768,7 @@ export async function runClose(session: string, logger: LoggerApi): Promise<void
|
|
|
666
768
|
|
|
667
769
|
type ClosableSession = {
|
|
668
770
|
session: string;
|
|
669
|
-
pid
|
|
771
|
+
pid?: number;
|
|
670
772
|
port: number;
|
|
671
773
|
};
|
|
672
774
|
|
|
@@ -705,7 +807,9 @@ function sendSignalToProcessGroupOrPid(
|
|
|
705
807
|
}
|
|
706
808
|
}
|
|
707
809
|
|
|
708
|
-
function formatSessionList(
|
|
810
|
+
function formatSessionList(
|
|
811
|
+
targets: ReadonlyArray<{ session: string }>,
|
|
812
|
+
): string {
|
|
709
813
|
return targets.map((target) => `"${target.session}"`).join(", ");
|
|
710
814
|
}
|
|
711
815
|
|
|
@@ -739,7 +843,7 @@ function clearStoppedSessionStates(
|
|
|
739
843
|
): number {
|
|
740
844
|
let cleared = 0;
|
|
741
845
|
for (const session of sessions) {
|
|
742
|
-
if (!isPidRunning(session.pid)) {
|
|
846
|
+
if (session.pid == null || !isPidRunning(session.pid)) {
|
|
743
847
|
clearSessionState(session.session, logger);
|
|
744
848
|
cleared += 1;
|
|
745
849
|
}
|
|
@@ -770,12 +874,21 @@ export async function runCloseAll(
|
|
|
770
874
|
pid: target.pid,
|
|
771
875
|
port: target.port,
|
|
772
876
|
});
|
|
773
|
-
|
|
877
|
+
if (target.pid != null) {
|
|
878
|
+
sendSignalToProcessGroupOrPid(
|
|
879
|
+
target.pid,
|
|
880
|
+
"SIGTERM",
|
|
881
|
+
logger,
|
|
882
|
+
target.session,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
774
885
|
}
|
|
775
886
|
|
|
776
887
|
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
777
888
|
|
|
778
|
-
let survivors = closable.filter(
|
|
889
|
+
let survivors = closable.filter(
|
|
890
|
+
(target) => target.pid != null && isPidRunning(target.pid),
|
|
891
|
+
);
|
|
779
892
|
if (survivors.length > 0 && !force) {
|
|
780
893
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
781
894
|
|
|
@@ -795,11 +908,20 @@ export async function runCloseAll(
|
|
|
795
908
|
session: survivor.session,
|
|
796
909
|
pid: survivor.pid,
|
|
797
910
|
});
|
|
798
|
-
|
|
911
|
+
if (survivor.pid != null) {
|
|
912
|
+
sendSignalToProcessGroupOrPid(
|
|
913
|
+
survivor.pid,
|
|
914
|
+
"SIGKILL",
|
|
915
|
+
logger,
|
|
916
|
+
survivor.session,
|
|
917
|
+
);
|
|
918
|
+
}
|
|
799
919
|
forceKilled += 1;
|
|
800
920
|
}
|
|
801
921
|
await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
|
|
802
|
-
survivors = survivors.filter(
|
|
922
|
+
survivors = survivors.filter(
|
|
923
|
+
(target) => target.pid != null && isPidRunning(target.pid),
|
|
924
|
+
);
|
|
803
925
|
if (survivors.length > 0) {
|
|
804
926
|
const closed = clearStoppedSessionStates(closable, logger);
|
|
805
927
|
throw new Error(
|
|
@@ -824,6 +946,95 @@ export async function runCloseAll(
|
|
|
824
946
|
}
|
|
825
947
|
}
|
|
826
948
|
|
|
949
|
+
export async function runConnect(
|
|
950
|
+
cdpUrl: string,
|
|
951
|
+
session: string,
|
|
952
|
+
logger: LoggerApi,
|
|
953
|
+
): Promise<void> {
|
|
954
|
+
logger.info("connect-start", { cdpUrl, session });
|
|
955
|
+
assertSessionAvailableForStart(session, logger);
|
|
956
|
+
|
|
957
|
+
let parsedUrl: URL;
|
|
958
|
+
try {
|
|
959
|
+
parsedUrl = new URL(cdpUrl);
|
|
960
|
+
} catch {
|
|
961
|
+
throw new Error(
|
|
962
|
+
[
|
|
963
|
+
`Invalid CDP URL: ${cdpUrl}`,
|
|
964
|
+
``,
|
|
965
|
+
`Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
|
|
966
|
+
` libretto connect http://127.0.0.1:9222`,
|
|
967
|
+
` libretto connect http://remote-host:9222`,
|
|
968
|
+
` libretto connect http://remote-host:9222/devtools/browser/<id>`,
|
|
969
|
+
].join("\n"),
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const endpoint = parsedUrl.href;
|
|
974
|
+
const port = parsedUrl.port
|
|
975
|
+
? Number(parsedUrl.port)
|
|
976
|
+
: parsedUrl.protocol === "https:"
|
|
977
|
+
? 443
|
|
978
|
+
: 80;
|
|
979
|
+
|
|
980
|
+
console.log(
|
|
981
|
+
`Connecting to CDP endpoint at ${endpoint} (session: ${session})...`,
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
// Verify the CDP endpoint is reachable
|
|
985
|
+
const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
|
|
986
|
+
try {
|
|
987
|
+
const resp = await fetch(versionUrl);
|
|
988
|
+
const versionInfo = await resp.json();
|
|
989
|
+
logger.info("connect-version-ok", { versionUrl, versionInfo });
|
|
990
|
+
} catch (err) {
|
|
991
|
+
logger.error("connect-version-failed", { versionUrl, error: err });
|
|
992
|
+
throw new Error(
|
|
993
|
+
`Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`,
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Connect via CDP using the full endpoint URL
|
|
998
|
+
const browser = await tryConnectToCDP(endpoint, logger, 10_000);
|
|
999
|
+
if (!browser) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const pages = resolveOperationalPages(browser);
|
|
1006
|
+
logger.info("connect-pages", {
|
|
1007
|
+
session,
|
|
1008
|
+
pageCount: pages.length,
|
|
1009
|
+
urls: pages.map((p) => p.url()),
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
disconnectBrowser(browser, logger, session);
|
|
1013
|
+
|
|
1014
|
+
writeSessionState(
|
|
1015
|
+
{
|
|
1016
|
+
port,
|
|
1017
|
+
cdpEndpoint: endpoint,
|
|
1018
|
+
session,
|
|
1019
|
+
startedAt: new Date().toISOString(),
|
|
1020
|
+
status: "active",
|
|
1021
|
+
},
|
|
1022
|
+
logger,
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
logger.info("connect-success", { cdpUrl: endpoint, session, port });
|
|
1026
|
+
console.log(`Connected to ${endpoint} (session: ${session})`);
|
|
1027
|
+
console.log(` Pages found: ${pages.length}`);
|
|
1028
|
+
if (pages.length > 0) {
|
|
1029
|
+
for (const p of pages.slice(0, 5)) {
|
|
1030
|
+
console.log(` ${p.url()}`);
|
|
1031
|
+
}
|
|
1032
|
+
if (pages.length > 5) {
|
|
1033
|
+
console.log(` ... and ${pages.length - 5} more`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
827
1038
|
export function resolvePath(filePath: string): string {
|
|
828
1039
|
return join(process.cwd(), filePath);
|
|
829
1040
|
}
|
package/src/cli/core/context.ts
CHANGED
|
@@ -66,10 +66,15 @@ export function createLoggerForSession(session: string): Logger {
|
|
|
66
66
|
const sessionDir = getSessionDir(session);
|
|
67
67
|
mkdirSync(sessionDir, { recursive: true });
|
|
68
68
|
const logFilePath = getSessionLogsPath(session);
|
|
69
|
-
return new Logger(
|
|
69
|
+
return new Logger(
|
|
70
|
+
["libretto"],
|
|
71
|
+
[createFileLogSink({ filePath: logFilePath })],
|
|
72
|
+
);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
export async function closeLogger(
|
|
75
|
+
export async function closeLogger(
|
|
76
|
+
logger: Logger | null | undefined,
|
|
77
|
+
): Promise<void> {
|
|
73
78
|
if (!logger) return;
|
|
74
79
|
await logger.close();
|
|
75
80
|
}
|