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,56 +1,80 @@
|
|
|
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 type { LoggerApi } from "../../shared/logger/index.js";
|
|
8
6
|
import {
|
|
9
7
|
connect,
|
|
10
8
|
disconnectBrowser,
|
|
9
|
+
getProfilePath,
|
|
10
|
+
hasProfile,
|
|
11
|
+
normalizeDomain,
|
|
12
|
+
normalizeUrl,
|
|
13
|
+
runClose,
|
|
11
14
|
resolveViewport,
|
|
12
15
|
} from "../core/browser.js";
|
|
13
16
|
import { parseViewportArg } from "./browser.js";
|
|
14
|
-
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
15
17
|
import {
|
|
16
18
|
assertSessionAvailableForStart,
|
|
17
19
|
assertSessionAllowsCommand,
|
|
18
20
|
clearSessionState,
|
|
21
|
+
logFileForSession,
|
|
19
22
|
readSessionState,
|
|
20
23
|
readSessionStateOrThrow,
|
|
21
24
|
setSessionStatus,
|
|
25
|
+
writeSessionState,
|
|
22
26
|
type SessionState,
|
|
23
27
|
} from "../core/session.js";
|
|
24
28
|
import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
|
|
25
29
|
import { readLibrettoConfig } from "../core/config.js";
|
|
26
|
-
import {
|
|
30
|
+
import { librettoCommand } from "../../shared/package-manager.js";
|
|
31
|
+
import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
|
|
32
|
+
import { resolveProviderName } from "../core/providers/index.js";
|
|
33
|
+
import { getAbsoluteIntegrationPath } from "../core/workflow-runtime.js";
|
|
27
34
|
import {
|
|
28
35
|
compileExecFunction,
|
|
29
36
|
stripEmptyCatchHandlers,
|
|
30
37
|
} from "../core/exec-compiler.js";
|
|
31
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
DaemonClient,
|
|
40
|
+
type DaemonExecSuccess,
|
|
41
|
+
type DaemonExecResult,
|
|
42
|
+
type DaemonToCliApi,
|
|
43
|
+
} from "../core/daemon/ipc.js";
|
|
32
44
|
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
33
45
|
import {
|
|
34
46
|
readActionLog,
|
|
35
47
|
readNetworkLog,
|
|
36
48
|
wrapPageForActionLogging,
|
|
37
49
|
} from "../core/telemetry.js";
|
|
38
|
-
import type {
|
|
50
|
+
import type { SessionAccessMode } from "../../shared/state/index.js";
|
|
51
|
+
import type { Experiments } from "../core/experiments.js";
|
|
39
52
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
40
53
|
import {
|
|
41
54
|
pageOption,
|
|
42
55
|
sessionOption,
|
|
43
56
|
withAutoSession,
|
|
57
|
+
withExperiments,
|
|
44
58
|
withRequiredSession,
|
|
45
59
|
} from "./shared.js";
|
|
46
60
|
|
|
47
|
-
type RunIntegrationCommandRequest =
|
|
61
|
+
type RunIntegrationCommandRequest = {
|
|
62
|
+
integrationPath: string;
|
|
63
|
+
session: string;
|
|
64
|
+
params: unknown;
|
|
65
|
+
headless: boolean;
|
|
66
|
+
visualize: boolean;
|
|
67
|
+
viewport?: { width: number; height: number };
|
|
68
|
+
accessMode: SessionAccessMode;
|
|
69
|
+
authProfileDomain?: string;
|
|
70
|
+
providerName?: string;
|
|
71
|
+
stayOpenOnSuccess: boolean;
|
|
48
72
|
tsconfigPath?: string;
|
|
73
|
+
experiments: Experiments;
|
|
49
74
|
};
|
|
50
75
|
type ExecMode = "exec" | "readonly-exec";
|
|
51
76
|
|
|
52
77
|
const require = moduleBuiltin.createRequire(import.meta.url);
|
|
53
|
-
const tsxCliPath = require.resolve("tsx/cli");
|
|
54
78
|
|
|
55
79
|
function writeDaemonExecOutput(output?: { stdout: string; stderr: string }) {
|
|
56
80
|
if (output?.stdout) {
|
|
@@ -61,6 +85,16 @@ function writeDaemonExecOutput(output?: { stdout: string; stderr: string }) {
|
|
|
61
85
|
}
|
|
62
86
|
}
|
|
63
87
|
|
|
88
|
+
function writeDaemonSnapshotDiff(
|
|
89
|
+
snapshotDiff: DaemonExecSuccess["snapshotDiff"],
|
|
90
|
+
) {
|
|
91
|
+
if (!snapshotDiff) return;
|
|
92
|
+
const renderedDiff = renderSnapshotDiff(snapshotDiff);
|
|
93
|
+
if (!renderedDiff) return;
|
|
94
|
+
console.log("Page changes:");
|
|
95
|
+
console.log(renderedDiff);
|
|
96
|
+
}
|
|
97
|
+
|
|
64
98
|
async function execViaDaemon(
|
|
65
99
|
code: string,
|
|
66
100
|
session: string,
|
|
@@ -86,26 +120,30 @@ async function execViaDaemon(
|
|
|
86
120
|
via: "daemon",
|
|
87
121
|
});
|
|
88
122
|
|
|
89
|
-
const client =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
const client = await DaemonClient.connect(daemonSocketPath);
|
|
124
|
+
let response: DaemonExecResult;
|
|
125
|
+
try {
|
|
126
|
+
response =
|
|
127
|
+
mode === "exec"
|
|
128
|
+
? await client.exec({
|
|
129
|
+
code: cleanedCode,
|
|
130
|
+
pageId: options.pageId,
|
|
131
|
+
visualize: options.visualize,
|
|
132
|
+
})
|
|
133
|
+
: await client.readonlyExec({
|
|
134
|
+
code: cleanedCode,
|
|
135
|
+
pageId: options.pageId,
|
|
136
|
+
});
|
|
137
|
+
} finally {
|
|
138
|
+
client.destroy();
|
|
139
|
+
}
|
|
102
140
|
|
|
103
141
|
if (!response.ok) {
|
|
104
142
|
writeDaemonExecOutput(response.output);
|
|
105
143
|
throw new Error(response.message);
|
|
106
144
|
}
|
|
107
145
|
|
|
108
|
-
const { result, output } = response.data;
|
|
146
|
+
const { result, output, snapshotDiff } = response.data;
|
|
109
147
|
writeDaemonExecOutput(output);
|
|
110
148
|
|
|
111
149
|
logger.info(`${mode}-success`, {
|
|
@@ -120,6 +158,7 @@ async function execViaDaemon(
|
|
|
120
158
|
} else {
|
|
121
159
|
console.log("Executed successfully");
|
|
122
160
|
}
|
|
161
|
+
writeDaemonSnapshotDiff(snapshotDiff);
|
|
123
162
|
}
|
|
124
163
|
|
|
125
164
|
async function execViaCdpFallback(
|
|
@@ -276,10 +315,9 @@ async function runExec(
|
|
|
276
315
|
): Promise<void> {
|
|
277
316
|
const state = readSessionStateOrThrow(session);
|
|
278
317
|
if (!state.daemonSocketPath) {
|
|
279
|
-
// Compatibility fallback for
|
|
280
|
-
//
|
|
281
|
-
// no daemon socket.
|
|
282
|
-
// gone. Context: https://www.notion.so/Make-libretto-run-daemon-backed-for-failed-workflow-inspection-352ac9fb35f181c1b7d3f08c0a735e9d
|
|
318
|
+
// Compatibility fallback for older sessions that predate daemon-backed
|
|
319
|
+
// command handling. Keep `exec` inspection working when state has a live
|
|
320
|
+
// CDP endpoint/port but no daemon socket.
|
|
283
321
|
logger.warn(`${options.mode ?? "exec"}-daemon-socket-missing-cdp-fallback`, {
|
|
284
322
|
session,
|
|
285
323
|
hasCdpEndpoint: Boolean(state.cdpEndpoint),
|
|
@@ -322,9 +360,23 @@ async function stopExistingFailedRunSession(
|
|
|
322
360
|
pid: existingState.pid,
|
|
323
361
|
port: existingState.port,
|
|
324
362
|
});
|
|
325
|
-
|
|
363
|
+
if (existingState.pid == null) {
|
|
364
|
+
clearSessionState(session, logger);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
326
367
|
|
|
327
|
-
|
|
368
|
+
try {
|
|
369
|
+
process.kill(existingState.pid, "SIGTERM");
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
372
|
+
if (code !== "ESRCH") {
|
|
373
|
+
logger.warn("run-release-existing-failed-session-signal-failed", {
|
|
374
|
+
session,
|
|
375
|
+
pid: existingState.pid,
|
|
376
|
+
error,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
328
380
|
|
|
329
381
|
const stopDeadline = Date.now() + 3_000;
|
|
330
382
|
while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
|
|
@@ -335,69 +387,17 @@ async function stopExistingFailedRunSession(
|
|
|
335
387
|
session,
|
|
336
388
|
pid: existingState.pid,
|
|
337
389
|
});
|
|
338
|
-
|
|
339
|
-
`Existing failed workflow process for session "${session}" (pid ${existingState.pid}) is still
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Existing failed workflow process for session "${session}" (pid ${existingState.pid}) is still running. Close it with: ${librettoCommand(`close --session ${session}`)}`,
|
|
340
392
|
);
|
|
341
|
-
return;
|
|
342
393
|
}
|
|
394
|
+
clearSessionState(session, logger);
|
|
343
395
|
console.log(
|
|
344
396
|
`Closed existing failed workflow process for session "${session}" (pid ${existingState.pid}).`,
|
|
345
397
|
);
|
|
346
398
|
}
|
|
347
399
|
|
|
348
|
-
|
|
349
|
-
if (!existsSync(path)) return null;
|
|
350
|
-
try {
|
|
351
|
-
return JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
352
|
-
} catch {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function readFailureDetails(path: string): {
|
|
358
|
-
message?: string;
|
|
359
|
-
phase?: "setup" | "workflow";
|
|
360
|
-
} | null {
|
|
361
|
-
const raw = readJsonFileIfExists(path);
|
|
362
|
-
if (!raw || typeof raw !== "object") return null;
|
|
363
|
-
|
|
364
|
-
const message = (raw as { message?: unknown }).message;
|
|
365
|
-
const phase = (raw as { phase?: unknown }).phase;
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
message: typeof message === "string" ? message : undefined,
|
|
369
|
-
phase: phase === "setup" || phase === "workflow" ? phase : undefined,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function waitForFailureDetails(
|
|
374
|
-
path: string,
|
|
375
|
-
timeoutMs = 1_000,
|
|
376
|
-
): Promise<{
|
|
377
|
-
message?: string;
|
|
378
|
-
phase?: "setup" | "workflow";
|
|
379
|
-
} | null> {
|
|
380
|
-
const deadline = Date.now() + timeoutMs;
|
|
381
|
-
while (Date.now() < deadline) {
|
|
382
|
-
const details = readFailureDetails(path);
|
|
383
|
-
if (details?.message) return details;
|
|
384
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
385
|
-
}
|
|
386
|
-
return readFailureDetails(path);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function streamOutputSince(path: string, offset: number): number {
|
|
390
|
-
if (!existsSync(path)) return offset;
|
|
391
|
-
const output = readFileSync(path);
|
|
392
|
-
if (output.length <= offset) return output.length;
|
|
393
|
-
process.stdout.write(output.subarray(offset));
|
|
394
|
-
return output.length;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
type WaitForWorkflowOutcomeArgs = {
|
|
398
|
-
session: string;
|
|
399
|
-
pid: number;
|
|
400
|
-
};
|
|
400
|
+
type RunIntegrationResult = "completed" | "paused";
|
|
401
401
|
|
|
402
402
|
type WorkflowOutcome = {
|
|
403
403
|
status: "completed" | "paused" | "failed" | "exited";
|
|
@@ -405,70 +405,68 @@ type WorkflowOutcome = {
|
|
|
405
405
|
phase?: "setup" | "workflow";
|
|
406
406
|
};
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
} catch {
|
|
413
|
-
// Ignore cleanup failures; next checks still validate actual state.
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function waitForWorkflowOutcome(
|
|
418
|
-
args: WaitForWorkflowOutcomeArgs,
|
|
419
|
-
): Promise<WorkflowOutcome> {
|
|
420
|
-
const signalPaths = getPauseSignalPaths(args.session);
|
|
421
|
-
if (args.pid <= 0) {
|
|
422
|
-
return { status: "exited" };
|
|
423
|
-
}
|
|
424
|
-
let outputOffset = 0;
|
|
408
|
+
type Deferred<T> = {
|
|
409
|
+
promise: Promise<T>;
|
|
410
|
+
resolve(value: T): void;
|
|
411
|
+
};
|
|
425
412
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
413
|
+
function createDeferred<T>(): Deferred<T> {
|
|
414
|
+
let resolve!: (value: T) => void;
|
|
415
|
+
const promise = new Promise<T>((resolvePromise) => {
|
|
416
|
+
resolve = resolvePromise;
|
|
417
|
+
});
|
|
418
|
+
return { promise, resolve };
|
|
419
|
+
}
|
|
431
420
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
);
|
|
440
|
-
|
|
421
|
+
function createWorkflowHandlers(
|
|
422
|
+
settleOutcome: (outcome: WorkflowOutcome) => void,
|
|
423
|
+
): DaemonToCliApi {
|
|
424
|
+
return {
|
|
425
|
+
workflowOutput: (event) => {
|
|
426
|
+
const stream =
|
|
427
|
+
event.stream === "stdout" ? process.stdout : process.stderr;
|
|
428
|
+
stream.write(event.text);
|
|
429
|
+
},
|
|
430
|
+
workflowPaused: () => {
|
|
431
|
+
settleOutcome({ status: "paused" });
|
|
432
|
+
},
|
|
433
|
+
workflowFinished: (event) => {
|
|
434
|
+
if (event.result === "completed") {
|
|
435
|
+
settleOutcome({ status: "completed" });
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
settleOutcome({
|
|
441
439
|
status: "failed",
|
|
442
|
-
message:
|
|
443
|
-
phase:
|
|
444
|
-
};
|
|
445
|
-
}
|
|
440
|
+
message: event.message,
|
|
441
|
+
phase: event.phase,
|
|
442
|
+
});
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
446
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return { status: "completed" };
|
|
453
|
-
}
|
|
447
|
+
async function waitForWorkflowOutcome(
|
|
448
|
+
pid: number,
|
|
449
|
+
outcomePromise: Promise<WorkflowOutcome>,
|
|
450
|
+
): Promise<WorkflowOutcome> {
|
|
451
|
+
let processExitInterval: ReturnType<typeof setInterval> | undefined;
|
|
454
452
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
);
|
|
460
|
-
return { status: "paused" };
|
|
453
|
+
const processExitPromise = new Promise<WorkflowOutcome>((resolve) => {
|
|
454
|
+
if (pid <= 0 || !isProcessRunning(pid)) {
|
|
455
|
+
resolve({ status: "exited" });
|
|
456
|
+
return;
|
|
461
457
|
}
|
|
462
458
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
459
|
+
processExitInterval = setInterval(() => {
|
|
460
|
+
if (!isProcessRunning(pid)) {
|
|
461
|
+
resolve({ status: "exited" });
|
|
462
|
+
}
|
|
463
|
+
}, 250);
|
|
464
|
+
});
|
|
470
465
|
|
|
471
|
-
|
|
466
|
+
try {
|
|
467
|
+
return await Promise.race([outcomePromise, processExitPromise]);
|
|
468
|
+
} finally {
|
|
469
|
+
if (processExitInterval) clearInterval(processExitInterval);
|
|
472
470
|
}
|
|
473
471
|
}
|
|
474
472
|
|
|
@@ -477,56 +475,63 @@ async function runResume(
|
|
|
477
475
|
logger: LoggerApi,
|
|
478
476
|
sessionState: SessionState,
|
|
479
477
|
): Promise<void> {
|
|
480
|
-
|
|
481
|
-
pausedSignalPath,
|
|
482
|
-
resumeSignalPath,
|
|
483
|
-
completedSignalPath,
|
|
484
|
-
failedSignalPath,
|
|
485
|
-
outputSignalPath,
|
|
486
|
-
} = getPauseSignalPaths(session);
|
|
487
|
-
|
|
488
|
-
if (!existsSync(pausedSignalPath)) {
|
|
478
|
+
if (sessionState.pid == null || !isProcessRunning(sessionState.pid)) {
|
|
489
479
|
throw new Error(
|
|
490
|
-
`
|
|
480
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid ?? "unknown"} is not running).`,
|
|
491
481
|
);
|
|
492
482
|
}
|
|
493
483
|
|
|
494
|
-
if (
|
|
484
|
+
if (!sessionState.daemonSocketPath) {
|
|
495
485
|
throw new Error(
|
|
496
|
-
`No active paused workflow found for session "${session}" (
|
|
486
|
+
`No active paused workflow found for session "${session}" (daemon socket is missing).`,
|
|
497
487
|
);
|
|
498
488
|
}
|
|
499
489
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
490
|
+
const workflowOutcome = createDeferred<WorkflowOutcome>();
|
|
491
|
+
const handlers = createWorkflowHandlers(workflowOutcome.resolve);
|
|
492
|
+
let client: DaemonClient;
|
|
493
|
+
try {
|
|
494
|
+
client = await DaemonClient.connect(
|
|
495
|
+
sessionState.daemonSocketPath,
|
|
496
|
+
handlers,
|
|
497
|
+
);
|
|
498
|
+
} catch {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
507
503
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
2,
|
|
517
|
-
),
|
|
518
|
-
"utf8",
|
|
519
|
-
);
|
|
520
|
-
console.log(`Resume signal sent for session "${session}".`);
|
|
504
|
+
let outcome: WorkflowOutcome;
|
|
505
|
+
try {
|
|
506
|
+
const status = await client.getWorkflowStatus();
|
|
507
|
+
if (status.state !== "paused") {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`Session "${session}" is not paused. Run "${librettoCommand(`run ... --session ${session}`)}" and call pause("${session}") first.`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
521
512
|
|
|
522
|
-
|
|
523
|
-
session,
|
|
524
|
-
|
|
525
|
-
|
|
513
|
+
await client.resumeWorkflow();
|
|
514
|
+
setSessionStatus(session, "active", logger);
|
|
515
|
+
console.log(`Resume requested for session "${session}".`);
|
|
516
|
+
|
|
517
|
+
outcome = await waitForWorkflowOutcome(
|
|
518
|
+
sessionState.pid!,
|
|
519
|
+
workflowOutcome.promise,
|
|
520
|
+
);
|
|
521
|
+
} finally {
|
|
522
|
+
client.destroy();
|
|
523
|
+
}
|
|
526
524
|
|
|
527
525
|
if (outcome.status === "completed") {
|
|
528
526
|
setSessionStatus(session, "completed", logger);
|
|
529
527
|
console.log("Integration completed.");
|
|
528
|
+
if (sessionState.stayOpenOnSuccess) {
|
|
529
|
+
console.log(
|
|
530
|
+
`Browser is still open for session "${session}". Close it with: libretto close --session ${session}`,
|
|
531
|
+
);
|
|
532
|
+
} else {
|
|
533
|
+
await runClose(session, logger);
|
|
534
|
+
}
|
|
530
535
|
return;
|
|
531
536
|
}
|
|
532
537
|
if (outcome.status === "failed") {
|
|
@@ -540,7 +545,8 @@ async function runResume(
|
|
|
540
545
|
if (outcome.status === "exited") {
|
|
541
546
|
setSessionStatus(session, "exited", logger);
|
|
542
547
|
throw new Error(
|
|
543
|
-
|
|
548
|
+
outcome.message ??
|
|
549
|
+
`Workflow process for session "${session}" exited before reporting completion or pause.`,
|
|
544
550
|
);
|
|
545
551
|
}
|
|
546
552
|
setSessionStatus(session, "paused", logger);
|
|
@@ -550,53 +556,95 @@ async function runResume(
|
|
|
550
556
|
async function runIntegrationFromFile(
|
|
551
557
|
args: RunIntegrationCommandRequest,
|
|
552
558
|
logger: LoggerApi,
|
|
553
|
-
): Promise<
|
|
559
|
+
): Promise<RunIntegrationResult> {
|
|
554
560
|
await stopExistingFailedRunSession(args.session, logger);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
clearSignalIfExists(signalPaths.completedSignalPath);
|
|
559
|
-
clearSignalIfExists(signalPaths.failedSignalPath);
|
|
560
|
-
clearSignalIfExists(signalPaths.outputSignalPath);
|
|
561
|
-
|
|
562
|
-
const workerEntryPath = fileURLToPath(
|
|
563
|
-
new URL("../workers/run-integration-worker.js", import.meta.url),
|
|
561
|
+
|
|
562
|
+
const absoluteIntegrationPath = getAbsoluteIntegrationPath(
|
|
563
|
+
args.integrationPath,
|
|
564
564
|
);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
565
|
+
if (args.authProfileDomain) {
|
|
566
|
+
const normalizedDomain = normalizeDomain(normalizeUrl(args.authProfileDomain));
|
|
567
|
+
if (!hasProfile(normalizedDomain)) {
|
|
568
|
+
const profilePath = getProfilePath(normalizedDomain);
|
|
569
|
+
throw new Error(
|
|
570
|
+
[
|
|
571
|
+
`Local auth profile not found for domain "${normalizedDomain}".`,
|
|
572
|
+
`Expected profile file: ${profilePath}`,
|
|
573
|
+
"To create it:",
|
|
574
|
+
` 1. ${librettoCommand(`open https://${normalizedDomain} --headed --session ${args.session}`)}`,
|
|
575
|
+
" 2. Log in manually in the browser window.",
|
|
576
|
+
` 3. ${librettoCommand(`save ${normalizedDomain} --session ${args.session}`)}`,
|
|
577
|
+
].join("\n"),
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const runLogPath = logFileForSession(args.session);
|
|
583
|
+
const workflowOutcome = createDeferred<WorkflowOutcome>();
|
|
584
|
+
const handlers = createWorkflowHandlers(workflowOutcome.resolve);
|
|
585
|
+
const {
|
|
586
|
+
pid,
|
|
587
|
+
socketPath: daemonSocketPath,
|
|
588
|
+
provider,
|
|
589
|
+
client,
|
|
590
|
+
} = await DaemonClient.spawn({
|
|
591
|
+
config: {
|
|
592
|
+
session: args.session,
|
|
593
|
+
experiments: args.experiments,
|
|
594
|
+
browser: args.providerName
|
|
595
|
+
? { kind: "provider", providerName: args.providerName }
|
|
596
|
+
: {
|
|
597
|
+
kind: "launch",
|
|
598
|
+
headed: !args.headless,
|
|
599
|
+
viewport: args.viewport ?? { width: 1366, height: 768 },
|
|
600
|
+
},
|
|
601
|
+
workflow: {
|
|
602
|
+
integrationPath: absoluteIntegrationPath,
|
|
603
|
+
params: args.params,
|
|
604
|
+
visualize: args.visualize,
|
|
605
|
+
stayOpenOnSuccess: args.stayOpenOnSuccess,
|
|
606
|
+
tsconfigPath: args.tsconfigPath,
|
|
607
|
+
authProfileDomain: args.authProfileDomain,
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
logger,
|
|
611
|
+
logPath: runLogPath,
|
|
612
|
+
startupTimeoutMs: 60_000,
|
|
613
|
+
handlers,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
writeSessionState(
|
|
585
617
|
{
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
618
|
+
port: 0,
|
|
619
|
+
pid,
|
|
620
|
+
cdpEndpoint: provider?.cdpEndpoint,
|
|
621
|
+
session: args.session,
|
|
622
|
+
startedAt: new Date().toISOString(),
|
|
623
|
+
status: "active",
|
|
624
|
+
mode: args.accessMode,
|
|
625
|
+
viewport: args.viewport,
|
|
626
|
+
stayOpenOnSuccess: args.stayOpenOnSuccess,
|
|
627
|
+
daemonSocketPath,
|
|
628
|
+
provider: provider
|
|
629
|
+
? { name: provider.name, sessionId: provider.sessionId }
|
|
630
|
+
: undefined,
|
|
589
631
|
},
|
|
632
|
+
logger,
|
|
590
633
|
);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
634
|
+
if (provider?.liveViewUrl) {
|
|
635
|
+
console.log(`View live session: ${provider.liveViewUrl}`);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let outcome: WorkflowOutcome;
|
|
639
|
+
try {
|
|
640
|
+
outcome = await waitForWorkflowOutcome(pid, workflowOutcome.promise);
|
|
641
|
+
} finally {
|
|
642
|
+
client.destroy();
|
|
643
|
+
}
|
|
596
644
|
if (outcome.status === "paused") {
|
|
597
645
|
setSessionStatus(args.session, "paused", logger);
|
|
598
646
|
console.log("Workflow paused.");
|
|
599
|
-
return;
|
|
647
|
+
return "paused";
|
|
600
648
|
}
|
|
601
649
|
if (outcome.status === "failed") {
|
|
602
650
|
setSessionStatus(args.session, "failed", logger);
|
|
@@ -610,11 +658,20 @@ async function runIntegrationFromFile(
|
|
|
610
658
|
if (outcome.status === "exited") {
|
|
611
659
|
setSessionStatus(args.session, "exited", logger);
|
|
612
660
|
throw new Error(
|
|
613
|
-
|
|
661
|
+
outcome.message ??
|
|
662
|
+
"Workflow process exited before reporting completion or pause during run.",
|
|
614
663
|
);
|
|
615
664
|
}
|
|
616
665
|
setSessionStatus(args.session, "completed", logger);
|
|
617
666
|
console.log("Integration completed.");
|
|
667
|
+
if (args.stayOpenOnSuccess) {
|
|
668
|
+
console.log(
|
|
669
|
+
`Browser is still open for session "${args.session}". Close it with: libretto close --session ${args.session}`,
|
|
670
|
+
);
|
|
671
|
+
} else {
|
|
672
|
+
await runClose(args.session, logger);
|
|
673
|
+
}
|
|
674
|
+
return "completed";
|
|
618
675
|
}
|
|
619
676
|
|
|
620
677
|
function readStdinSync(): string | null {
|
|
@@ -642,7 +699,7 @@ export const execInput = SimpleCLI.input({
|
|
|
642
699
|
},
|
|
643
700
|
}).refine(
|
|
644
701
|
(input) => input.code !== undefined,
|
|
645
|
-
`Usage:
|
|
702
|
+
`Usage: ${librettoCommand("exec <code|-> [--session <name>] [--visualize]")}\n echo '<code>' | ${librettoCommand("exec - [--session <name>] [--visualize]")}`,
|
|
646
703
|
);
|
|
647
704
|
|
|
648
705
|
export const execCommand = SimpleCLI.command({
|
|
@@ -683,7 +740,7 @@ export const readonlyExecInput = SimpleCLI.input({
|
|
|
683
740
|
},
|
|
684
741
|
}).refine(
|
|
685
742
|
(input) => input.code !== undefined,
|
|
686
|
-
`Usage:
|
|
743
|
+
`Usage: ${librettoCommand("readonly-exec <code|-> [--session <name>] [--page <id>]")}\n echo '<code>' | ${librettoCommand("readonly-exec - [--session <name>] [--page <id>]")}`,
|
|
687
744
|
);
|
|
688
745
|
|
|
689
746
|
export const readonlyExecCommand = SimpleCLI.command({
|
|
@@ -705,7 +762,7 @@ export const readonlyExecCommand = SimpleCLI.command({
|
|
|
705
762
|
});
|
|
706
763
|
});
|
|
707
764
|
|
|
708
|
-
const runUsage = `Usage:
|
|
765
|
+
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]")}`;
|
|
709
766
|
|
|
710
767
|
export const runInput = SimpleCLI.input({
|
|
711
768
|
positionals: [
|
|
@@ -739,6 +796,10 @@ export const runInput = SimpleCLI.input({
|
|
|
739
796
|
name: "no-visualize",
|
|
740
797
|
help: "Disable ghost cursor + highlight visualization in headed mode",
|
|
741
798
|
}),
|
|
799
|
+
stayOpenOnSuccess: SimpleCLI.flag({
|
|
800
|
+
name: "stay-open-on-success",
|
|
801
|
+
help: "Keep the browser session open after the workflow completes successfully",
|
|
802
|
+
}),
|
|
742
803
|
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
743
804
|
name: "auth-profile",
|
|
744
805
|
help: "Domain for local auth profile (e.g. apps.example.com)",
|
|
@@ -795,6 +856,7 @@ export const runCommand = SimpleCLI.command({
|
|
|
795
856
|
})
|
|
796
857
|
.input(runInput)
|
|
797
858
|
.use(withAutoSession())
|
|
859
|
+
.use(withExperiments())
|
|
798
860
|
.handle(async ({ input, ctx }) => {
|
|
799
861
|
warnIfInstalledSkillOutOfDate();
|
|
800
862
|
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
@@ -813,64 +875,34 @@ export const runCommand = SimpleCLI.command({
|
|
|
813
875
|
);
|
|
814
876
|
|
|
815
877
|
const providerName = resolveProviderName(input.provider);
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
let provider: ReturnType<typeof getCloudProviderApi> | undefined;
|
|
819
|
-
if (providerName !== "local") {
|
|
820
|
-
provider = getCloudProviderApi(providerName);
|
|
878
|
+
const daemonProviderName = providerName === "local" ? undefined : providerName;
|
|
879
|
+
if (daemonProviderName) {
|
|
821
880
|
console.log(
|
|
822
881
|
`Creating ${providerName} browser session (session: ${ctx.session})...`,
|
|
823
882
|
);
|
|
824
|
-
|
|
825
|
-
ctx.logger.info("run-provider-session-created", {
|
|
883
|
+
ctx.logger.info("run-provider-session-requested", {
|
|
826
884
|
provider: providerName,
|
|
827
|
-
sessionId: providerSession.sessionId,
|
|
828
|
-
cdpEndpoint: providerSession.cdpEndpoint,
|
|
829
|
-
liveViewUrl: providerSession.liveViewUrl,
|
|
830
885
|
});
|
|
831
|
-
if (providerSession.liveViewUrl) {
|
|
832
|
-
console.log(`View live session: ${providerSession.liveViewUrl}`);
|
|
833
|
-
}
|
|
834
886
|
console.log(`Connecting to ${providerName} browser...`);
|
|
835
|
-
cdpEndpoint = providerSession.cdpEndpoint;
|
|
836
|
-
providerInfo = {
|
|
837
|
-
name: providerName,
|
|
838
|
-
sessionId: providerSession.sessionId,
|
|
839
|
-
};
|
|
840
887
|
}
|
|
841
888
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
} finally {
|
|
860
|
-
if (provider && providerInfo) {
|
|
861
|
-
try {
|
|
862
|
-
const result = await provider.closeSession(providerInfo.sessionId);
|
|
863
|
-
if (result.replayUrl) {
|
|
864
|
-
console.log(`View recording: ${result.replayUrl}`);
|
|
865
|
-
}
|
|
866
|
-
} catch (cleanupErr) {
|
|
867
|
-
console.error(
|
|
868
|
-
`Failed to clean up ${providerInfo.name} session ${providerInfo.sessionId}:`,
|
|
869
|
-
cleanupErr instanceof Error ? cleanupErr.message : cleanupErr,
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
889
|
+
await runIntegrationFromFile(
|
|
890
|
+
{
|
|
891
|
+
integrationPath: input.integrationFile!,
|
|
892
|
+
session: ctx.session,
|
|
893
|
+
params,
|
|
894
|
+
tsconfigPath: input.tsconfig,
|
|
895
|
+
headless: daemonProviderName ? true : (headlessMode ?? false),
|
|
896
|
+
visualize,
|
|
897
|
+
authProfileDomain: input.authProfile,
|
|
898
|
+
viewport,
|
|
899
|
+
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : (readLibrettoConfig().sessionMode ?? "write-access"),
|
|
900
|
+
providerName: daemonProviderName,
|
|
901
|
+
stayOpenOnSuccess: input.stayOpenOnSuccess,
|
|
902
|
+
experiments: ctx.experiments,
|
|
903
|
+
},
|
|
904
|
+
ctx.logger,
|
|
905
|
+
);
|
|
874
906
|
});
|
|
875
907
|
|
|
876
908
|
export const resumeInput = SimpleCLI.input({
|