libretto 0.6.11 → 0.6.12
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 +4 -0
- package/README.template.md +4 -0
- package/dist/cli/cli.js +4 -3
- package/dist/cli/commands/ai.js +3 -2
- package/dist/cli/commands/browser.js +17 -17
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +20 -34
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +81 -9
- package/dist/cli/commands/status.js +5 -4
- package/dist/cli/core/ai-model.js +6 -3
- package/dist/cli/core/browser.js +300 -121
- package/dist/cli/core/config.js +4 -2
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +535 -89
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +72 -6
- package/dist/cli/core/experiments.js +66 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/snapshot-analyzer.js +4 -3
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/router.js +4 -1
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/package.json +4 -2
- package/skills/libretto/SKILL.md +3 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +4 -3
- package/src/cli/commands/ai.ts +3 -2
- package/src/cli/commands/browser.ts +13 -11
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +18 -36
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +99 -11
- package/src/cli/commands/status.ts +5 -4
- package/src/cli/core/ai-model.ts +6 -3
- package/src/cli/core/browser.ts +369 -147
- package/src/cli/core/config.ts +3 -1
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +686 -106
- package/src/cli/core/daemon/ipc.ts +330 -214
- package/src/cli/core/daemon/snapshot.ts +106 -8
- package/src/cli/core/experiments.ts +85 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/snapshot-analyzer.ts +4 -3
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +85 -0
- package/src/cli/router.ts +4 -1
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
|
@@ -1,32 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
3
2
|
import * as moduleBuiltin from "node:module";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
3
|
import { z } from "zod";
|
|
6
4
|
import { installInstrumentation } from "../../shared/instrumentation/index.js";
|
|
7
5
|
import {
|
|
8
6
|
connect,
|
|
9
7
|
disconnectBrowser,
|
|
8
|
+
getProfilePath,
|
|
9
|
+
hasProfile,
|
|
10
|
+
normalizeDomain,
|
|
11
|
+
normalizeUrl,
|
|
12
|
+
runClose,
|
|
10
13
|
resolveViewport
|
|
11
14
|
} from "../core/browser.js";
|
|
12
15
|
import { parseViewportArg } from "./browser.js";
|
|
13
|
-
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
14
16
|
import {
|
|
15
17
|
assertSessionAvailableForStart,
|
|
16
18
|
assertSessionAllowsCommand,
|
|
17
19
|
clearSessionState,
|
|
20
|
+
logFileForSession,
|
|
18
21
|
readSessionState,
|
|
19
22
|
readSessionStateOrThrow,
|
|
20
|
-
setSessionStatus
|
|
23
|
+
setSessionStatus,
|
|
24
|
+
writeSessionState
|
|
21
25
|
} from "../core/session.js";
|
|
22
26
|
import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
|
|
23
27
|
import { readLibrettoConfig } from "../core/config.js";
|
|
24
|
-
import {
|
|
28
|
+
import { librettoCommand } from "../../shared/package-manager.js";
|
|
29
|
+
import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
|
|
30
|
+
import { resolveProviderName } from "../core/providers/index.js";
|
|
31
|
+
import { getAbsoluteIntegrationPath } from "../core/workflow-runtime.js";
|
|
25
32
|
import {
|
|
26
33
|
compileExecFunction,
|
|
27
34
|
stripEmptyCatchHandlers
|
|
28
35
|
} from "../core/exec-compiler.js";
|
|
29
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
DaemonClient
|
|
38
|
+
} from "../core/daemon/ipc.js";
|
|
30
39
|
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
31
40
|
import {
|
|
32
41
|
readActionLog,
|
|
@@ -38,10 +47,10 @@ import {
|
|
|
38
47
|
pageOption,
|
|
39
48
|
sessionOption,
|
|
40
49
|
withAutoSession,
|
|
50
|
+
withExperiments,
|
|
41
51
|
withRequiredSession
|
|
42
52
|
} from "./shared.js";
|
|
43
53
|
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
44
|
-
const tsxCliPath = require2.resolve("tsx/cli");
|
|
45
54
|
function writeDaemonExecOutput(output) {
|
|
46
55
|
if (output?.stdout) {
|
|
47
56
|
process.stdout.write(output.stdout);
|
|
@@ -50,6 +59,13 @@ function writeDaemonExecOutput(output) {
|
|
|
50
59
|
process.stderr.write(output.stderr);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
62
|
+
function writeDaemonSnapshotDiff(snapshotDiff) {
|
|
63
|
+
if (!snapshotDiff) return;
|
|
64
|
+
const renderedDiff = renderSnapshotDiff(snapshotDiff);
|
|
65
|
+
if (!renderedDiff) return;
|
|
66
|
+
console.log("Page changes:");
|
|
67
|
+
console.log(renderedDiff);
|
|
68
|
+
}
|
|
53
69
|
async function execViaDaemon(code, session, daemonSocketPath, logger, options) {
|
|
54
70
|
const mode = options.mode ?? "exec";
|
|
55
71
|
const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
|
|
@@ -64,20 +80,25 @@ async function execViaDaemon(code, session, daemonSocketPath, logger, options) {
|
|
|
64
80
|
pageId: options.pageId,
|
|
65
81
|
via: "daemon"
|
|
66
82
|
});
|
|
67
|
-
const client =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
const client = await DaemonClient.connect(daemonSocketPath);
|
|
84
|
+
let response;
|
|
85
|
+
try {
|
|
86
|
+
response = mode === "exec" ? await client.exec({
|
|
87
|
+
code: cleanedCode,
|
|
88
|
+
pageId: options.pageId,
|
|
89
|
+
visualize: options.visualize
|
|
90
|
+
}) : await client.readonlyExec({
|
|
91
|
+
code: cleanedCode,
|
|
92
|
+
pageId: options.pageId
|
|
93
|
+
});
|
|
94
|
+
} finally {
|
|
95
|
+
client.destroy();
|
|
96
|
+
}
|
|
76
97
|
if (!response.ok) {
|
|
77
98
|
writeDaemonExecOutput(response.output);
|
|
78
99
|
throw new Error(response.message);
|
|
79
100
|
}
|
|
80
|
-
const { result, output } = response.data;
|
|
101
|
+
const { result, output, snapshotDiff } = response.data;
|
|
81
102
|
writeDaemonExecOutput(output);
|
|
82
103
|
logger.info(`${mode}-success`, {
|
|
83
104
|
session,
|
|
@@ -91,6 +112,7 @@ async function execViaDaemon(code, session, daemonSocketPath, logger, options) {
|
|
|
91
112
|
} else {
|
|
92
113
|
console.log("Executed successfully");
|
|
93
114
|
}
|
|
115
|
+
writeDaemonSnapshotDiff(snapshotDiff);
|
|
94
116
|
}
|
|
95
117
|
async function execViaCdpFallback(code, session, logger, options) {
|
|
96
118
|
const visualize = options.visualize ?? false;
|
|
@@ -237,8 +259,22 @@ async function stopExistingFailedRunSession(session, logger) {
|
|
|
237
259
|
pid: existingState.pid,
|
|
238
260
|
port: existingState.port
|
|
239
261
|
});
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
if (existingState.pid == null) {
|
|
263
|
+
clearSessionState(session, logger);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
process.kill(existingState.pid, "SIGTERM");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const code = error.code;
|
|
270
|
+
if (code !== "ESRCH") {
|
|
271
|
+
logger.warn("run-release-existing-failed-session-signal-failed", {
|
|
272
|
+
session,
|
|
273
|
+
pid: existingState.pid,
|
|
274
|
+
error
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
242
278
|
const stopDeadline = Date.now() + 3e3;
|
|
243
279
|
while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
|
|
244
280
|
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
@@ -248,148 +284,115 @@ async function stopExistingFailedRunSession(session, logger) {
|
|
|
248
284
|
session,
|
|
249
285
|
pid: existingState.pid
|
|
250
286
|
});
|
|
251
|
-
|
|
252
|
-
`Existing failed workflow process for session "${session}" (pid ${existingState.pid}) is still
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Existing failed workflow process for session "${session}" (pid ${existingState.pid}) is still running. Close it with: ${librettoCommand(`close --session ${session}`)}`
|
|
253
289
|
);
|
|
254
|
-
return;
|
|
255
290
|
}
|
|
291
|
+
clearSessionState(session, logger);
|
|
256
292
|
console.log(
|
|
257
293
|
`Closed existing failed workflow process for session "${session}" (pid ${existingState.pid}).`
|
|
258
294
|
);
|
|
259
295
|
}
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
}
|
|
296
|
+
function createDeferred() {
|
|
297
|
+
let resolve;
|
|
298
|
+
const promise = new Promise((resolvePromise) => {
|
|
299
|
+
resolve = resolvePromise;
|
|
300
|
+
});
|
|
301
|
+
return { promise, resolve };
|
|
267
302
|
}
|
|
268
|
-
function
|
|
269
|
-
const raw = readJsonFileIfExists(path);
|
|
270
|
-
if (!raw || typeof raw !== "object") return null;
|
|
271
|
-
const message = raw.message;
|
|
272
|
-
const phase = raw.phase;
|
|
303
|
+
function createWorkflowHandlers(settleOutcome) {
|
|
273
304
|
return {
|
|
274
|
-
|
|
275
|
-
|
|
305
|
+
workflowOutput: (event) => {
|
|
306
|
+
const stream = event.stream === "stdout" ? process.stdout : process.stderr;
|
|
307
|
+
stream.write(event.text);
|
|
308
|
+
},
|
|
309
|
+
workflowPaused: () => {
|
|
310
|
+
settleOutcome({ status: "paused" });
|
|
311
|
+
},
|
|
312
|
+
workflowFinished: (event) => {
|
|
313
|
+
if (event.result === "completed") {
|
|
314
|
+
settleOutcome({ status: "completed" });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
settleOutcome({
|
|
318
|
+
status: "failed",
|
|
319
|
+
message: event.message,
|
|
320
|
+
phase: event.phase
|
|
321
|
+
});
|
|
322
|
+
}
|
|
276
323
|
};
|
|
277
324
|
}
|
|
278
|
-
async function
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
process.stdout.write(output.subarray(offset));
|
|
292
|
-
return output.length;
|
|
293
|
-
}
|
|
294
|
-
function clearSignalIfExists(path) {
|
|
295
|
-
if (!existsSync(path)) return;
|
|
325
|
+
async function waitForWorkflowOutcome(pid, outcomePromise) {
|
|
326
|
+
let processExitInterval;
|
|
327
|
+
const processExitPromise = new Promise((resolve) => {
|
|
328
|
+
if (pid <= 0 || !isProcessRunning(pid)) {
|
|
329
|
+
resolve({ status: "exited" });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
processExitInterval = setInterval(() => {
|
|
333
|
+
if (!isProcessRunning(pid)) {
|
|
334
|
+
resolve({ status: "exited" });
|
|
335
|
+
}
|
|
336
|
+
}, 250);
|
|
337
|
+
});
|
|
296
338
|
try {
|
|
297
|
-
|
|
298
|
-
}
|
|
339
|
+
return await Promise.race([outcomePromise, processExitPromise]);
|
|
340
|
+
} finally {
|
|
341
|
+
if (processExitInterval) clearInterval(processExitInterval);
|
|
299
342
|
}
|
|
300
343
|
}
|
|
301
|
-
async function
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
let outputOffset = 0;
|
|
307
|
-
while (true) {
|
|
308
|
-
outputOffset = streamOutputSince(
|
|
309
|
-
signalPaths.outputSignalPath,
|
|
310
|
-
outputOffset
|
|
344
|
+
async function runResume(session, logger, sessionState) {
|
|
345
|
+
if (sessionState.pid == null || !isProcessRunning(sessionState.pid)) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid ?? "unknown"} is not running).`
|
|
311
348
|
);
|
|
312
|
-
if (existsSync(signalPaths.failedSignalPath)) {
|
|
313
|
-
outputOffset = streamOutputSince(
|
|
314
|
-
signalPaths.outputSignalPath,
|
|
315
|
-
outputOffset
|
|
316
|
-
);
|
|
317
|
-
const failureDetails = await waitForFailureDetails(
|
|
318
|
-
signalPaths.failedSignalPath
|
|
319
|
-
);
|
|
320
|
-
return {
|
|
321
|
-
status: "failed",
|
|
322
|
-
message: failureDetails?.message,
|
|
323
|
-
phase: failureDetails?.phase
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
if (existsSync(signalPaths.completedSignalPath)) {
|
|
327
|
-
outputOffset = streamOutputSince(
|
|
328
|
-
signalPaths.outputSignalPath,
|
|
329
|
-
outputOffset
|
|
330
|
-
);
|
|
331
|
-
return { status: "completed" };
|
|
332
|
-
}
|
|
333
|
-
if (existsSync(signalPaths.pausedSignalPath)) {
|
|
334
|
-
outputOffset = streamOutputSince(
|
|
335
|
-
signalPaths.outputSignalPath,
|
|
336
|
-
outputOffset
|
|
337
|
-
);
|
|
338
|
-
return { status: "paused" };
|
|
339
|
-
}
|
|
340
|
-
if (!isProcessRunning(args.pid)) {
|
|
341
|
-
outputOffset = streamOutputSince(
|
|
342
|
-
signalPaths.outputSignalPath,
|
|
343
|
-
outputOffset
|
|
344
|
-
);
|
|
345
|
-
return { status: "exited" };
|
|
346
|
-
}
|
|
347
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
348
349
|
}
|
|
349
|
-
|
|
350
|
-
async function runResume(session, logger, sessionState) {
|
|
351
|
-
const {
|
|
352
|
-
pausedSignalPath,
|
|
353
|
-
resumeSignalPath,
|
|
354
|
-
completedSignalPath,
|
|
355
|
-
failedSignalPath,
|
|
356
|
-
outputSignalPath
|
|
357
|
-
} = getPauseSignalPaths(session);
|
|
358
|
-
if (!existsSync(pausedSignalPath)) {
|
|
350
|
+
if (!sessionState.daemonSocketPath) {
|
|
359
351
|
throw new Error(
|
|
360
|
-
`
|
|
352
|
+
`No active paused workflow found for session "${session}" (daemon socket is missing).`
|
|
361
353
|
);
|
|
362
354
|
}
|
|
363
|
-
|
|
355
|
+
const workflowOutcome = createDeferred();
|
|
356
|
+
const handlers = createWorkflowHandlers(workflowOutcome.resolve);
|
|
357
|
+
let client;
|
|
358
|
+
try {
|
|
359
|
+
client = await DaemonClient.connect(
|
|
360
|
+
sessionState.daemonSocketPath,
|
|
361
|
+
handlers
|
|
362
|
+
);
|
|
363
|
+
} catch {
|
|
364
364
|
throw new Error(
|
|
365
|
-
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid
|
|
365
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`
|
|
366
366
|
);
|
|
367
367
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const outcome = await waitForWorkflowOutcome({
|
|
387
|
-
session,
|
|
388
|
-
pid: sessionState.pid
|
|
389
|
-
});
|
|
368
|
+
let outcome;
|
|
369
|
+
try {
|
|
370
|
+
const status = await client.getWorkflowStatus();
|
|
371
|
+
if (status.state !== "paused") {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`Session "${session}" is not paused. Run "${librettoCommand(`run ... --session ${session}`)}" and call pause("${session}") first.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
await client.resumeWorkflow();
|
|
377
|
+
setSessionStatus(session, "active", logger);
|
|
378
|
+
console.log(`Resume requested for session "${session}".`);
|
|
379
|
+
outcome = await waitForWorkflowOutcome(
|
|
380
|
+
sessionState.pid,
|
|
381
|
+
workflowOutcome.promise
|
|
382
|
+
);
|
|
383
|
+
} finally {
|
|
384
|
+
client.destroy();
|
|
385
|
+
}
|
|
390
386
|
if (outcome.status === "completed") {
|
|
391
387
|
setSessionStatus(session, "completed", logger);
|
|
392
388
|
console.log("Integration completed.");
|
|
389
|
+
if (sessionState.stayOpenOnSuccess) {
|
|
390
|
+
console.log(
|
|
391
|
+
`Browser is still open for session "${session}". Close it with: libretto close --session ${session}`
|
|
392
|
+
);
|
|
393
|
+
} else {
|
|
394
|
+
await runClose(session, logger);
|
|
395
|
+
}
|
|
393
396
|
return;
|
|
394
397
|
}
|
|
395
398
|
if (outcome.status === "failed") {
|
|
@@ -401,7 +404,7 @@ async function runResume(session, logger, sessionState) {
|
|
|
401
404
|
if (outcome.status === "exited") {
|
|
402
405
|
setSessionStatus(session, "exited", logger);
|
|
403
406
|
throw new Error(
|
|
404
|
-
`Workflow process for session "${session}" exited before reporting completion or pause.`
|
|
407
|
+
outcome.message ?? `Workflow process for session "${session}" exited before reporting completion or pause.`
|
|
405
408
|
);
|
|
406
409
|
}
|
|
407
410
|
setSessionStatus(session, "paused", logger);
|
|
@@ -409,50 +412,85 @@ async function runResume(session, logger, sessionState) {
|
|
|
409
412
|
}
|
|
410
413
|
async function runIntegrationFromFile(args, logger) {
|
|
411
414
|
await stopExistingFailedRunSession(args.session, logger);
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
clearSignalIfExists(signalPaths.resumeSignalPath);
|
|
415
|
-
clearSignalIfExists(signalPaths.completedSignalPath);
|
|
416
|
-
clearSignalIfExists(signalPaths.failedSignalPath);
|
|
417
|
-
clearSignalIfExists(signalPaths.outputSignalPath);
|
|
418
|
-
const workerEntryPath = fileURLToPath(
|
|
419
|
-
new URL("../workers/run-integration-worker.js", import.meta.url)
|
|
415
|
+
const absoluteIntegrationPath = getAbsoluteIntegrationPath(
|
|
416
|
+
args.integrationPath
|
|
420
417
|
);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
418
|
+
if (args.authProfileDomain) {
|
|
419
|
+
const normalizedDomain = normalizeDomain(normalizeUrl(args.authProfileDomain));
|
|
420
|
+
if (!hasProfile(normalizedDomain)) {
|
|
421
|
+
const profilePath = getProfilePath(normalizedDomain);
|
|
422
|
+
throw new Error(
|
|
423
|
+
[
|
|
424
|
+
`Local auth profile not found for domain "${normalizedDomain}".`,
|
|
425
|
+
`Expected profile file: ${profilePath}`,
|
|
426
|
+
"To create it:",
|
|
427
|
+
` 1. ${librettoCommand(`open https://${normalizedDomain} --headed --session ${args.session}`)}`,
|
|
428
|
+
" 2. Log in manually in the browser window.",
|
|
429
|
+
` 3. ${librettoCommand(`save ${normalizedDomain} --session ${args.session}`)}`
|
|
430
|
+
].join("\n")
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const runLogPath = logFileForSession(args.session);
|
|
435
|
+
const workflowOutcome = createDeferred();
|
|
436
|
+
const handlers = createWorkflowHandlers(workflowOutcome.resolve);
|
|
437
|
+
const {
|
|
438
|
+
pid,
|
|
439
|
+
socketPath: daemonSocketPath,
|
|
440
|
+
provider,
|
|
441
|
+
client
|
|
442
|
+
} = await DaemonClient.spawn({
|
|
443
|
+
config: {
|
|
444
|
+
session: args.session,
|
|
445
|
+
experiments: args.experiments,
|
|
446
|
+
browser: args.providerName ? { kind: "provider", providerName: args.providerName } : {
|
|
447
|
+
kind: "launch",
|
|
448
|
+
headed: !args.headless,
|
|
449
|
+
viewport: args.viewport ?? { width: 1366, height: 768 }
|
|
450
|
+
},
|
|
451
|
+
workflow: {
|
|
452
|
+
integrationPath: absoluteIntegrationPath,
|
|
453
|
+
params: args.params,
|
|
454
|
+
visualize: args.visualize,
|
|
455
|
+
stayOpenOnSuccess: args.stayOpenOnSuccess,
|
|
456
|
+
tsconfigPath: args.tsconfigPath,
|
|
457
|
+
authProfileDomain: args.authProfileDomain
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
logger,
|
|
461
|
+
logPath: runLogPath,
|
|
462
|
+
startupTimeoutMs: 6e4,
|
|
463
|
+
handlers
|
|
432
464
|
});
|
|
433
|
-
|
|
434
|
-
process.execPath,
|
|
435
|
-
[
|
|
436
|
-
tsxCliPath,
|
|
437
|
-
...args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : [],
|
|
438
|
-
workerEntryPath,
|
|
439
|
-
payload
|
|
440
|
-
],
|
|
465
|
+
writeSessionState(
|
|
441
466
|
{
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
467
|
+
port: 0,
|
|
468
|
+
pid,
|
|
469
|
+
cdpEndpoint: provider?.cdpEndpoint,
|
|
470
|
+
session: args.session,
|
|
471
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
472
|
+
status: "active",
|
|
473
|
+
mode: args.accessMode,
|
|
474
|
+
viewport: args.viewport,
|
|
475
|
+
stayOpenOnSuccess: args.stayOpenOnSuccess,
|
|
476
|
+
daemonSocketPath,
|
|
477
|
+
provider: provider ? { name: provider.name, sessionId: provider.sessionId } : void 0
|
|
478
|
+
},
|
|
479
|
+
logger
|
|
446
480
|
);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
481
|
+
if (provider?.liveViewUrl) {
|
|
482
|
+
console.log(`View live session: ${provider.liveViewUrl}`);
|
|
483
|
+
}
|
|
484
|
+
let outcome;
|
|
485
|
+
try {
|
|
486
|
+
outcome = await waitForWorkflowOutcome(pid, workflowOutcome.promise);
|
|
487
|
+
} finally {
|
|
488
|
+
client.destroy();
|
|
489
|
+
}
|
|
452
490
|
if (outcome.status === "paused") {
|
|
453
491
|
setSessionStatus(args.session, "paused", logger);
|
|
454
492
|
console.log("Workflow paused.");
|
|
455
|
-
return;
|
|
493
|
+
return "paused";
|
|
456
494
|
}
|
|
457
495
|
if (outcome.status === "failed") {
|
|
458
496
|
setSessionStatus(args.session, "failed", logger);
|
|
@@ -467,11 +505,19 @@ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-ru
|
|
|
467
505
|
if (outcome.status === "exited") {
|
|
468
506
|
setSessionStatus(args.session, "exited", logger);
|
|
469
507
|
throw new Error(
|
|
470
|
-
"Workflow process exited before reporting completion or pause during run."
|
|
508
|
+
outcome.message ?? "Workflow process exited before reporting completion or pause during run."
|
|
471
509
|
);
|
|
472
510
|
}
|
|
473
511
|
setSessionStatus(args.session, "completed", logger);
|
|
474
512
|
console.log("Integration completed.");
|
|
513
|
+
if (args.stayOpenOnSuccess) {
|
|
514
|
+
console.log(
|
|
515
|
+
`Browser is still open for session "${args.session}". Close it with: libretto close --session ${args.session}`
|
|
516
|
+
);
|
|
517
|
+
} else {
|
|
518
|
+
await runClose(args.session, logger);
|
|
519
|
+
}
|
|
520
|
+
return "completed";
|
|
475
521
|
}
|
|
476
522
|
function readStdinSync() {
|
|
477
523
|
if (process.stdin.isTTY === true) return null;
|
|
@@ -497,8 +543,8 @@ const execInput = SimpleCLI.input({
|
|
|
497
543
|
}
|
|
498
544
|
}).refine(
|
|
499
545
|
(input) => input.code !== void 0,
|
|
500
|
-
`Usage:
|
|
501
|
-
echo '<code>' |
|
|
546
|
+
`Usage: ${librettoCommand("exec <code|-> [--session <name>] [--visualize]")}
|
|
547
|
+
echo '<code>' | ${librettoCommand("exec - [--session <name>] [--visualize]")}`
|
|
502
548
|
);
|
|
503
549
|
const execCommand = SimpleCLI.command({
|
|
504
550
|
description: "Execute Playwright TypeScript code"
|
|
@@ -534,8 +580,8 @@ const readonlyExecInput = SimpleCLI.input({
|
|
|
534
580
|
}
|
|
535
581
|
}).refine(
|
|
536
582
|
(input) => input.code !== void 0,
|
|
537
|
-
`Usage:
|
|
538
|
-
echo '<code>' |
|
|
583
|
+
`Usage: ${librettoCommand("readonly-exec <code|-> [--session <name>] [--page <id>]")}
|
|
584
|
+
echo '<code>' | ${librettoCommand("readonly-exec - [--session <name>] [--page <id>]")}`
|
|
539
585
|
);
|
|
540
586
|
const readonlyExecCommand = SimpleCLI.command({
|
|
541
587
|
description: "Execute read-only Playwright inspection code"
|
|
@@ -552,7 +598,7 @@ const readonlyExecCommand = SimpleCLI.command({
|
|
|
552
598
|
mode: "readonly-exec"
|
|
553
599
|
});
|
|
554
600
|
});
|
|
555
|
-
const runUsage = `Usage:
|
|
601
|
+
const runUsage = `Usage: ${librettoCommand("run <integrationFile> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--read-only|--write-access] [--no-visualize] [--stay-open-on-success] [--viewport WxH]")}`;
|
|
556
602
|
const runInput = SimpleCLI.input({
|
|
557
603
|
positionals: [
|
|
558
604
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
@@ -585,6 +631,10 @@ const runInput = SimpleCLI.input({
|
|
|
585
631
|
name: "no-visualize",
|
|
586
632
|
help: "Disable ghost cursor + highlight visualization in headed mode"
|
|
587
633
|
}),
|
|
634
|
+
stayOpenOnSuccess: SimpleCLI.flag({
|
|
635
|
+
name: "stay-open-on-success",
|
|
636
|
+
help: "Keep the browser session open after the workflow completes successfully"
|
|
637
|
+
}),
|
|
588
638
|
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
589
639
|
name: "auth-profile",
|
|
590
640
|
help: "Domain for local auth profile (e.g. apps.example.com)"
|
|
@@ -629,7 +679,7 @@ function resolveRunParams(rawInlineParams, paramsFile) {
|
|
|
629
679
|
}
|
|
630
680
|
const runCommand = SimpleCLI.command({
|
|
631
681
|
description: "Run the default-exported Libretto workflow from a file"
|
|
632
|
-
}).input(runInput).use(withAutoSession()).handle(async ({ input, ctx }) => {
|
|
682
|
+
}).input(runInput).use(withAutoSession()).use(withExperiments()).handle(async ({ input, ctx }) => {
|
|
633
683
|
warnIfInstalledSkillOutOfDate();
|
|
634
684
|
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
635
685
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
@@ -641,63 +691,33 @@ const runCommand = SimpleCLI.command({
|
|
|
641
691
|
ctx.logger
|
|
642
692
|
);
|
|
643
693
|
const providerName = resolveProviderName(input.provider);
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
let provider;
|
|
647
|
-
if (providerName !== "local") {
|
|
648
|
-
provider = getCloudProviderApi(providerName);
|
|
694
|
+
const daemonProviderName = providerName === "local" ? void 0 : providerName;
|
|
695
|
+
if (daemonProviderName) {
|
|
649
696
|
console.log(
|
|
650
697
|
`Creating ${providerName} browser session (session: ${ctx.session})...`
|
|
651
698
|
);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
provider: providerName,
|
|
655
|
-
sessionId: providerSession.sessionId,
|
|
656
|
-
cdpEndpoint: providerSession.cdpEndpoint,
|
|
657
|
-
liveViewUrl: providerSession.liveViewUrl
|
|
699
|
+
ctx.logger.info("run-provider-session-requested", {
|
|
700
|
+
provider: providerName
|
|
658
701
|
});
|
|
659
|
-
if (providerSession.liveViewUrl) {
|
|
660
|
-
console.log(`View live session: ${providerSession.liveViewUrl}`);
|
|
661
|
-
}
|
|
662
702
|
console.log(`Connecting to ${providerName} browser...`);
|
|
663
|
-
cdpEndpoint = providerSession.cdpEndpoint;
|
|
664
|
-
providerInfo = {
|
|
665
|
-
name: providerName,
|
|
666
|
-
sessionId: providerSession.sessionId
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
try {
|
|
670
|
-
await runIntegrationFromFile(
|
|
671
|
-
{
|
|
672
|
-
integrationPath: input.integrationFile,
|
|
673
|
-
session: ctx.session,
|
|
674
|
-
params,
|
|
675
|
-
tsconfigPath: input.tsconfig,
|
|
676
|
-
headless: cdpEndpoint ? true : headlessMode ?? false,
|
|
677
|
-
visualize,
|
|
678
|
-
authProfileDomain: input.authProfile,
|
|
679
|
-
viewport,
|
|
680
|
-
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access",
|
|
681
|
-
cdpEndpoint,
|
|
682
|
-
provider: providerInfo
|
|
683
|
-
},
|
|
684
|
-
ctx.logger
|
|
685
|
-
);
|
|
686
|
-
} finally {
|
|
687
|
-
if (provider && providerInfo) {
|
|
688
|
-
try {
|
|
689
|
-
const result = await provider.closeSession(providerInfo.sessionId);
|
|
690
|
-
if (result.replayUrl) {
|
|
691
|
-
console.log(`View recording: ${result.replayUrl}`);
|
|
692
|
-
}
|
|
693
|
-
} catch (cleanupErr) {
|
|
694
|
-
console.error(
|
|
695
|
-
`Failed to clean up ${providerInfo.name} session ${providerInfo.sessionId}:`,
|
|
696
|
-
cleanupErr instanceof Error ? cleanupErr.message : cleanupErr
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
703
|
}
|
|
704
|
+
await runIntegrationFromFile(
|
|
705
|
+
{
|
|
706
|
+
integrationPath: input.integrationFile,
|
|
707
|
+
session: ctx.session,
|
|
708
|
+
params,
|
|
709
|
+
tsconfigPath: input.tsconfig,
|
|
710
|
+
headless: daemonProviderName ? true : headlessMode ?? false,
|
|
711
|
+
visualize,
|
|
712
|
+
authProfileDomain: input.authProfile,
|
|
713
|
+
viewport,
|
|
714
|
+
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access",
|
|
715
|
+
providerName: daemonProviderName,
|
|
716
|
+
stayOpenOnSuccess: input.stayOpenOnSuccess,
|
|
717
|
+
experiments: ctx.experiments
|
|
718
|
+
},
|
|
719
|
+
ctx.logger
|
|
720
|
+
);
|
|
701
721
|
});
|
|
702
722
|
const resumeInput = SimpleCLI.input({
|
|
703
723
|
positionals: [],
|