libretto 0.5.3-experimental.5 → 0.5.3
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 +114 -37
- package/README.template.md +160 -0
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/deploy.js +148 -0
- package/dist/cli/commands/execution.js +218 -96
- 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/deploy-artifact.js +687 -0
- 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 +144 -43
- package/dist/cli/router.js +16 -21
- package/dist/cli/workers/run-integration-runtime.js +25 -45
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +13 -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 +18 -10
- package/dist/shared/workflow/workflow.js +50 -5
- package/package.json +14 -6
- package/scripts/generate-changelog.ts +132 -0
- package/scripts/postinstall.mjs +4 -3
- package/scripts/skills-libretto.mjs +2 -88
- package/scripts/summarize-evals.mjs +32 -10
- package/skills/libretto/SKILL.md +132 -62
- 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 +176 -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/deploy.ts +198 -0
- package/src/cli/commands/execution.ts +251 -111
- 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/deploy-artifact.ts +938 -0
- 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 +281 -98
- package/src/cli/router.ts +15 -21
- package/src/cli/workers/run-integration-runtime.ts +35 -57
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +77 -67
- 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 +107 -30
- package/scripts/check-skills-sync.mjs +0 -23
- package/scripts/prepare-release.sh +0 -97
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -6,8 +6,10 @@ import { z } from "zod";
|
|
|
6
6
|
import { installInstrumentation } from "../../shared/instrumentation/index.js";
|
|
7
7
|
import {
|
|
8
8
|
connect,
|
|
9
|
-
disconnectBrowser
|
|
9
|
+
disconnectBrowser,
|
|
10
|
+
resolveViewport
|
|
10
11
|
} from "../core/browser.js";
|
|
12
|
+
import { parseViewportArg } from "./browser.js";
|
|
11
13
|
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
12
14
|
import {
|
|
13
15
|
assertSessionAvailableForStart,
|
|
@@ -22,10 +24,10 @@ import {
|
|
|
22
24
|
} from "../core/telemetry.js";
|
|
23
25
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
24
26
|
import {
|
|
25
|
-
loadSessionStateMiddleware,
|
|
26
27
|
pageOption,
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
sessionOption,
|
|
29
|
+
withAutoSession,
|
|
30
|
+
withRequiredSession
|
|
29
31
|
} from "./shared.js";
|
|
30
32
|
const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
|
|
31
33
|
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
@@ -69,23 +71,91 @@ function compileExecFunction(code, helperNames) {
|
|
|
69
71
|
}).constructor;
|
|
70
72
|
return new AsyncFunction(...helperNames, code);
|
|
71
73
|
}
|
|
74
|
+
function stripEmptyCatchHandlers(code) {
|
|
75
|
+
const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
|
|
76
|
+
let strippedCount = 0;
|
|
77
|
+
let result = "";
|
|
78
|
+
let i = 0;
|
|
79
|
+
while (i < code.length) {
|
|
80
|
+
if (code[i] === "/" && code[i + 1] === "/") {
|
|
81
|
+
const end = code.indexOf("\n", i);
|
|
82
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
|
|
83
|
+
result += slice;
|
|
84
|
+
i += slice.length;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (code[i] === "/" && code[i + 1] === "*") {
|
|
88
|
+
const end = code.indexOf("*/", i + 2);
|
|
89
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
|
|
90
|
+
result += slice;
|
|
91
|
+
i += slice.length;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
95
|
+
const quote = code[i];
|
|
96
|
+
let j = i + 1;
|
|
97
|
+
while (j < code.length) {
|
|
98
|
+
if (code[j] === "\\" && quote !== "`") {
|
|
99
|
+
j += 2;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (code[j] === "\\" && quote === "`") {
|
|
103
|
+
j += 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (code[j] === quote) {
|
|
107
|
+
j++;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
|
|
111
|
+
let depth = 1;
|
|
112
|
+
j += 2;
|
|
113
|
+
while (j < code.length && depth > 0) {
|
|
114
|
+
if (code[j] === "{") depth++;
|
|
115
|
+
else if (code[j] === "}") depth--;
|
|
116
|
+
j++;
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
j++;
|
|
121
|
+
}
|
|
122
|
+
result += code.slice(i, j);
|
|
123
|
+
i = j;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
catchRe.lastIndex = i;
|
|
127
|
+
const match = catchRe.exec(code);
|
|
128
|
+
if (match && match.index === i) {
|
|
129
|
+
strippedCount++;
|
|
130
|
+
i += match[0].length;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
result += code[i];
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
return { cleaned: result, strippedCount };
|
|
137
|
+
}
|
|
72
138
|
async function runExec(code, session, logger, visualize = false, pageId) {
|
|
139
|
+
const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
|
|
140
|
+
if (strippedCount > 0) {
|
|
141
|
+
console.log("(Stripped `.catch(() => {})` \u2014 letting errors bubble up)");
|
|
142
|
+
}
|
|
73
143
|
logger.info("exec-start", {
|
|
74
144
|
session,
|
|
75
|
-
codeLength:
|
|
76
|
-
codePreview:
|
|
145
|
+
codeLength: cleanedCode.length,
|
|
146
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
77
147
|
visualize,
|
|
78
148
|
pageId
|
|
79
149
|
});
|
|
80
|
-
const {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
150
|
+
const {
|
|
151
|
+
browser,
|
|
152
|
+
context,
|
|
153
|
+
page,
|
|
154
|
+
pageId: resolvedPageId
|
|
155
|
+
} = await connect(session, logger, 1e4, {
|
|
156
|
+
pageId,
|
|
157
|
+
requireSinglePage: true
|
|
158
|
+
});
|
|
89
159
|
const STALL_THRESHOLD_MS = 6e4;
|
|
90
160
|
let lastActivityTs = Date.now();
|
|
91
161
|
const onActivity = () => {
|
|
@@ -97,10 +167,10 @@ async function runExec(code, session, logger, visualize = false, pageId) {
|
|
|
97
167
|
logger.warn("exec-stall-warning", {
|
|
98
168
|
session,
|
|
99
169
|
silenceMs,
|
|
100
|
-
codePreview:
|
|
170
|
+
codePreview: cleanedCode.slice(0, 200)
|
|
101
171
|
});
|
|
102
172
|
console.warn(
|
|
103
|
-
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 exec may be hung (code: ${
|
|
173
|
+
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 exec may be hung (code: ${cleanedCode.slice(0, 100)}...)`
|
|
104
174
|
);
|
|
105
175
|
}
|
|
106
176
|
}, STALL_THRESHOLD_MS);
|
|
@@ -109,7 +179,7 @@ async function runExec(code, session, logger, visualize = false, pageId) {
|
|
|
109
179
|
logger.info("exec-interrupted", {
|
|
110
180
|
session,
|
|
111
181
|
duration: Date.now() - execStartTs,
|
|
112
|
-
codePreview:
|
|
182
|
+
codePreview: cleanedCode.slice(0, 200)
|
|
113
183
|
});
|
|
114
184
|
};
|
|
115
185
|
process.on("SIGINT", sigintHandler);
|
|
@@ -142,19 +212,21 @@ async function runExec(code, session, logger, visualize = false, pageId) {
|
|
|
142
212
|
Buffer
|
|
143
213
|
};
|
|
144
214
|
const helperNames = Object.keys(helpers);
|
|
145
|
-
const fn = compileExecFunction(
|
|
215
|
+
const fn = compileExecFunction(cleanedCode, helperNames);
|
|
146
216
|
const result = await fn(...Object.values(helpers));
|
|
147
217
|
logger.info("exec-success", { session, hasResult: result !== void 0 });
|
|
148
218
|
if (result !== void 0) {
|
|
149
219
|
console.log(
|
|
150
220
|
typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
151
221
|
);
|
|
222
|
+
} else {
|
|
223
|
+
console.log("Executed successfully");
|
|
152
224
|
}
|
|
153
225
|
} catch (err) {
|
|
154
226
|
logger.error("exec-error", {
|
|
155
227
|
error: err,
|
|
156
228
|
session,
|
|
157
|
-
codePreview:
|
|
229
|
+
codePreview: cleanedCode.slice(0, 200)
|
|
158
230
|
});
|
|
159
231
|
throw err;
|
|
160
232
|
} finally {
|
|
@@ -191,6 +263,7 @@ async function stopExistingFailedRunSession(session, logger) {
|
|
|
191
263
|
port: existingState.port
|
|
192
264
|
});
|
|
193
265
|
clearSessionState(session, logger);
|
|
266
|
+
if (existingState.pid == null) return;
|
|
194
267
|
const stopDeadline = Date.now() + 3e3;
|
|
195
268
|
while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
|
|
196
269
|
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
@@ -257,10 +330,18 @@ async function waitForWorkflowOutcome(args) {
|
|
|
257
330
|
}
|
|
258
331
|
let outputOffset = 0;
|
|
259
332
|
while (true) {
|
|
260
|
-
outputOffset = streamOutputSince(
|
|
333
|
+
outputOffset = streamOutputSince(
|
|
334
|
+
signalPaths.outputSignalPath,
|
|
335
|
+
outputOffset
|
|
336
|
+
);
|
|
261
337
|
if (existsSync(signalPaths.failedSignalPath)) {
|
|
262
|
-
outputOffset = streamOutputSince(
|
|
263
|
-
|
|
338
|
+
outputOffset = streamOutputSince(
|
|
339
|
+
signalPaths.outputSignalPath,
|
|
340
|
+
outputOffset
|
|
341
|
+
);
|
|
342
|
+
const failureDetails = await waitForFailureDetails(
|
|
343
|
+
signalPaths.failedSignalPath
|
|
344
|
+
);
|
|
264
345
|
return {
|
|
265
346
|
status: "failed",
|
|
266
347
|
message: failureDetails?.message,
|
|
@@ -268,15 +349,24 @@ async function waitForWorkflowOutcome(args) {
|
|
|
268
349
|
};
|
|
269
350
|
}
|
|
270
351
|
if (existsSync(signalPaths.completedSignalPath)) {
|
|
271
|
-
outputOffset = streamOutputSince(
|
|
352
|
+
outputOffset = streamOutputSince(
|
|
353
|
+
signalPaths.outputSignalPath,
|
|
354
|
+
outputOffset
|
|
355
|
+
);
|
|
272
356
|
return { status: "completed" };
|
|
273
357
|
}
|
|
274
358
|
if (existsSync(signalPaths.pausedSignalPath)) {
|
|
275
|
-
outputOffset = streamOutputSince(
|
|
359
|
+
outputOffset = streamOutputSince(
|
|
360
|
+
signalPaths.outputSignalPath,
|
|
361
|
+
outputOffset
|
|
362
|
+
);
|
|
276
363
|
return { status: "paused" };
|
|
277
364
|
}
|
|
278
365
|
if (!isProcessRunning(args.pid)) {
|
|
279
|
-
outputOffset = streamOutputSince(
|
|
366
|
+
outputOffset = streamOutputSince(
|
|
367
|
+
signalPaths.outputSignalPath,
|
|
368
|
+
outputOffset
|
|
369
|
+
);
|
|
280
370
|
return { status: "exited" };
|
|
281
371
|
}
|
|
282
372
|
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
@@ -295,9 +385,9 @@ async function runResume(session, logger, sessionState) {
|
|
|
295
385
|
`Session "${session}" is not paused. Run "libretto run ... --session ${session}" and call pause("${session}") first.`
|
|
296
386
|
);
|
|
297
387
|
}
|
|
298
|
-
if (!isProcessRunning(sessionState.pid)) {
|
|
388
|
+
if (sessionState.pid == null || !isProcessRunning(sessionState.pid)) {
|
|
299
389
|
throw new Error(
|
|
300
|
-
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`
|
|
390
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid ?? "unknown"} is not running).`
|
|
301
391
|
);
|
|
302
392
|
}
|
|
303
393
|
clearSignalIfExists(pausedSignalPath);
|
|
@@ -355,23 +445,28 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
355
445
|
);
|
|
356
446
|
const payload = JSON.stringify({
|
|
357
447
|
integrationPath: args.integrationPath,
|
|
358
|
-
|
|
448
|
+
workflowName: args.workflowName,
|
|
359
449
|
session: args.session,
|
|
360
450
|
params: args.params,
|
|
361
451
|
headless: args.headless,
|
|
362
452
|
visualize: args.visualize,
|
|
363
|
-
authProfileDomain: args.authProfileDomain
|
|
364
|
-
|
|
365
|
-
const worker = spawn(process.execPath, [
|
|
366
|
-
tsxCliPath,
|
|
367
|
-
...args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : [],
|
|
368
|
-
workerEntryPath,
|
|
369
|
-
payload
|
|
370
|
-
], {
|
|
371
|
-
detached: true,
|
|
372
|
-
stdio: "ignore",
|
|
373
|
-
env: process.env
|
|
453
|
+
authProfileDomain: args.authProfileDomain,
|
|
454
|
+
viewport: args.viewport
|
|
374
455
|
});
|
|
456
|
+
const worker = spawn(
|
|
457
|
+
process.execPath,
|
|
458
|
+
[
|
|
459
|
+
tsxCliPath,
|
|
460
|
+
...args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : [],
|
|
461
|
+
workerEntryPath,
|
|
462
|
+
payload
|
|
463
|
+
],
|
|
464
|
+
{
|
|
465
|
+
detached: true,
|
|
466
|
+
stdio: "ignore",
|
|
467
|
+
env: process.env
|
|
468
|
+
}
|
|
469
|
+
);
|
|
375
470
|
worker.unref();
|
|
376
471
|
const outcome = await waitForWorkflowOutcome({
|
|
377
472
|
session: args.session,
|
|
@@ -401,43 +496,59 @@ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-ru
|
|
|
401
496
|
setSessionStatus(args.session, "completed", logger);
|
|
402
497
|
console.log("Integration completed.");
|
|
403
498
|
}
|
|
499
|
+
function readStdinSync() {
|
|
500
|
+
if (process.stdin.isTTY === true) return null;
|
|
501
|
+
try {
|
|
502
|
+
const content = readFileSync(0, "utf8");
|
|
503
|
+
return content.trim().length > 0 ? content : null;
|
|
504
|
+
} catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
404
508
|
const execInput = SimpleCLI.input({
|
|
405
509
|
positionals: [
|
|
406
|
-
SimpleCLI.positional("
|
|
407
|
-
help: "Playwright TypeScript code to execute"
|
|
408
|
-
variadic: true
|
|
510
|
+
SimpleCLI.positional("code", z.string().optional(), {
|
|
511
|
+
help: "Playwright TypeScript code to execute"
|
|
409
512
|
})
|
|
410
513
|
],
|
|
411
514
|
named: {
|
|
412
515
|
session: sessionOption(),
|
|
413
|
-
visualize: SimpleCLI.flag({
|
|
516
|
+
visualize: SimpleCLI.flag({
|
|
517
|
+
help: "Enable ghost cursor + highlight visualization"
|
|
518
|
+
}),
|
|
414
519
|
page: pageOption()
|
|
415
520
|
}
|
|
416
521
|
}).refine(
|
|
417
|
-
(input) => input.
|
|
418
|
-
`Usage: libretto exec <code
|
|
522
|
+
(input) => input.code !== void 0,
|
|
523
|
+
`Usage: libretto exec <code|-> [--session <name>] [--visualize]
|
|
524
|
+
echo '<code>' | libretto exec - [--session <name>] [--visualize]`
|
|
419
525
|
);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
input.visualize,
|
|
429
|
-
input.page
|
|
526
|
+
const execCommand = SimpleCLI.command({
|
|
527
|
+
description: "Execute Playwright TypeScript code"
|
|
528
|
+
}).input(execInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
529
|
+
const code = input.code;
|
|
530
|
+
const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
|
|
531
|
+
if (codeFromArgsOrStdin === null) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
"Missing stdin input for `exec -`. Pipe Playwright code into stdin."
|
|
430
534
|
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
535
|
+
}
|
|
536
|
+
await runExec(
|
|
537
|
+
codeFromArgsOrStdin,
|
|
538
|
+
ctx.session,
|
|
539
|
+
ctx.logger,
|
|
540
|
+
input.visualize,
|
|
541
|
+
input.page
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
434
545
|
const runInput = SimpleCLI.input({
|
|
435
546
|
positionals: [
|
|
436
547
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
437
548
|
help: "Path to the integration file"
|
|
438
549
|
}),
|
|
439
|
-
SimpleCLI.positional("
|
|
440
|
-
help: "
|
|
550
|
+
SimpleCLI.positional("workflowName", z.string().optional(), {
|
|
551
|
+
help: "Workflow name to run (from workflow(name, handler))"
|
|
441
552
|
})
|
|
442
553
|
],
|
|
443
554
|
named: {
|
|
@@ -461,12 +572,21 @@ const runInput = SimpleCLI.input({
|
|
|
461
572
|
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
462
573
|
name: "auth-profile",
|
|
463
574
|
help: "Domain for local auth profile (e.g. apps.example.com)"
|
|
575
|
+
}),
|
|
576
|
+
viewport: SimpleCLI.option(z.string().optional(), {
|
|
577
|
+
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
|
|
464
578
|
})
|
|
465
579
|
}
|
|
466
580
|
}).refine(
|
|
467
|
-
(input) => Boolean(input.integrationFile && input.
|
|
581
|
+
(input) => Boolean(input.integrationFile && input.workflowName),
|
|
468
582
|
runUsage
|
|
469
|
-
).refine(
|
|
583
|
+
).refine(
|
|
584
|
+
(input) => !(input.params && input.paramsFile),
|
|
585
|
+
"Pass either --params or --params-file, not both."
|
|
586
|
+
).refine(
|
|
587
|
+
(input) => !(input.headed && input.headless),
|
|
588
|
+
"Cannot pass both --headed and --headless."
|
|
589
|
+
);
|
|
470
590
|
function resolveRunParams(rawInlineParams, paramsFile) {
|
|
471
591
|
if (paramsFile) {
|
|
472
592
|
let content;
|
|
@@ -484,53 +604,55 @@ function resolveRunParams(rawInlineParams, paramsFile) {
|
|
|
484
604
|
}
|
|
485
605
|
return {};
|
|
486
606
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
607
|
+
const runCommand = SimpleCLI.command({
|
|
608
|
+
description: "Run an exported Libretto workflow from a file"
|
|
609
|
+
}).input(runInput).use(withAutoSession()).handle(async ({ input, ctx }) => {
|
|
610
|
+
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
611
|
+
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
612
|
+
const params = resolveRunParams(input.params, input.paramsFile);
|
|
613
|
+
const headlessMode = input.headed ? false : input.headless ? true : void 0;
|
|
614
|
+
const visualize = !input.noVisualize;
|
|
615
|
+
const viewport = resolveViewport(
|
|
616
|
+
parseViewportArg(input.viewport),
|
|
617
|
+
ctx.logger
|
|
618
|
+
);
|
|
619
|
+
await runIntegrationFromFile(
|
|
620
|
+
{
|
|
497
621
|
integrationPath: input.integrationFile,
|
|
498
|
-
|
|
622
|
+
workflowName: input.workflowName,
|
|
499
623
|
session: ctx.session,
|
|
500
624
|
params,
|
|
501
625
|
tsconfigPath: input.tsconfig,
|
|
502
626
|
headless: headlessMode ?? false,
|
|
503
627
|
visualize,
|
|
504
|
-
authProfileDomain: input.authProfile
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
628
|
+
authProfileDomain: input.authProfile,
|
|
629
|
+
viewport
|
|
630
|
+
},
|
|
631
|
+
ctx.logger
|
|
632
|
+
);
|
|
633
|
+
});
|
|
508
634
|
const resumeInput = SimpleCLI.input({
|
|
509
635
|
positionals: [],
|
|
510
636
|
named: {
|
|
511
637
|
session: sessionOption()
|
|
512
638
|
}
|
|
513
639
|
});
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
run: createRunCommand(logger),
|
|
525
|
-
resume: createResumeCommand(logger)
|
|
526
|
-
};
|
|
527
|
-
}
|
|
640
|
+
const resumeCommand = SimpleCLI.command({
|
|
641
|
+
description: "Resume a paused workflow for the current session"
|
|
642
|
+
}).input(resumeInput).use(withRequiredSession()).handle(async ({ ctx }) => {
|
|
643
|
+
await runResume(ctx.session, ctx.logger, ctx.sessionState);
|
|
644
|
+
});
|
|
645
|
+
const executionCommands = {
|
|
646
|
+
exec: execCommand,
|
|
647
|
+
run: runCommand,
|
|
648
|
+
resume: resumeCommand
|
|
649
|
+
};
|
|
528
650
|
export {
|
|
529
|
-
|
|
530
|
-
createExecutionCommands,
|
|
531
|
-
createResumeCommand,
|
|
532
|
-
createRunCommand,
|
|
651
|
+
execCommand,
|
|
533
652
|
execInput,
|
|
653
|
+
executionCommands,
|
|
654
|
+
resumeCommand,
|
|
534
655
|
resumeInput,
|
|
656
|
+
runCommand,
|
|
535
657
|
runInput
|
|
536
658
|
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from "node:fs";
|
|
3
11
|
import { spawnSync } from "node:child_process";
|
|
4
12
|
import { basename, dirname, join } from "node:path";
|
|
5
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,8 +17,8 @@ import {
|
|
|
9
17
|
loadSnapshotEnv,
|
|
10
18
|
resolveSnapshotApiModel
|
|
11
19
|
} from "../core/snapshot-api-config.js";
|
|
12
|
-
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
20
|
import { hasProviderCredentials } from "../../shared/llm/client.js";
|
|
21
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
14
22
|
const PROVIDER_CHOICES = [
|
|
15
23
|
{
|
|
16
24
|
key: "1",
|
|
@@ -44,15 +52,6 @@ function promptUser(rl, question) {
|
|
|
44
52
|
});
|
|
45
53
|
});
|
|
46
54
|
}
|
|
47
|
-
function askYesNo(question) {
|
|
48
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
-
return new Promise((resolve) => {
|
|
50
|
-
rl.question(`${question} (y/N) `, (answer) => {
|
|
51
|
-
rl.close();
|
|
52
|
-
resolve(answer.trim().toLowerCase() === "y");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
55
|
function safeReadAiConfig() {
|
|
57
56
|
try {
|
|
58
57
|
return readAiConfig();
|
|
@@ -83,7 +82,9 @@ function printSnapshotApiStatus() {
|
|
|
83
82
|
printInvalidAiConfigWarning();
|
|
84
83
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
85
84
|
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
86
|
-
console.log(
|
|
85
|
+
console.log(
|
|
86
|
+
" Snapshot objectives will use the API analyzer by default."
|
|
87
|
+
);
|
|
87
88
|
console.log(" No further action required.");
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
@@ -98,7 +99,9 @@ function printSnapshotApiStatus() {
|
|
|
98
99
|
console.log(
|
|
99
100
|
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
|
|
100
101
|
);
|
|
101
|
-
console.log(
|
|
102
|
+
console.log(
|
|
103
|
+
" Run `npx libretto init` interactively to set up credentials."
|
|
104
|
+
);
|
|
102
105
|
}
|
|
103
106
|
async function runInteractiveApiSetup() {
|
|
104
107
|
const config = safeReadAiConfig();
|
|
@@ -110,7 +113,9 @@ async function runInteractiveApiSetup() {
|
|
|
110
113
|
printInvalidAiConfigWarning();
|
|
111
114
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
112
115
|
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
113
|
-
console.log(
|
|
116
|
+
console.log(
|
|
117
|
+
" Snapshot objectives will use the API analyzer by default."
|
|
118
|
+
);
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
116
121
|
console.log(" \u2717 No snapshot API credentials detected.\n");
|
|
@@ -119,14 +124,18 @@ async function runInteractiveApiSetup() {
|
|
|
119
124
|
output: process.stdout
|
|
120
125
|
});
|
|
121
126
|
try {
|
|
122
|
-
console.log(
|
|
127
|
+
console.log(
|
|
128
|
+
" Which API provider would you like to use for snapshot analysis?\n"
|
|
129
|
+
);
|
|
123
130
|
for (const choice of PROVIDER_CHOICES) {
|
|
124
131
|
console.log(` ${choice.key}) ${choice.label}`);
|
|
125
132
|
}
|
|
126
133
|
console.log(" s) Skip for now\n");
|
|
127
134
|
const answer = await promptUser(rl, " Choice: ");
|
|
128
135
|
if (answer.toLowerCase() === "s" || !answer) {
|
|
129
|
-
console.log(
|
|
136
|
+
console.log(
|
|
137
|
+
"\n Skipped. You can set up API credentials later by rerunning `npx libretto init`."
|
|
138
|
+
);
|
|
130
139
|
console.log(" Or add credentials directly to your .env file:");
|
|
131
140
|
console.log(" OPENAI_API_KEY=...");
|
|
132
141
|
console.log(" ANTHROPIC_API_KEY=...");
|
|
@@ -215,22 +224,15 @@ function detectAgentDirs(root) {
|
|
|
215
224
|
if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
|
|
216
225
|
return dirs;
|
|
217
226
|
}
|
|
218
|
-
|
|
227
|
+
function copySkills() {
|
|
219
228
|
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
220
229
|
if (agentDirs.length === 0) {
|
|
221
|
-
console.log(
|
|
230
|
+
console.log(
|
|
231
|
+
"\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping."
|
|
232
|
+
);
|
|
222
233
|
return;
|
|
223
234
|
}
|
|
224
235
|
const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
|
|
225
|
-
const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
|
|
226
|
-
const existing = destinations.filter((d) => existsSync(d));
|
|
227
|
-
const verb = existing.length > 0 ? "Overwrite" : "Install";
|
|
228
|
-
const proceed = await askYesNo(`
|
|
229
|
-
${verb} libretto skills in ${dirNames}?`);
|
|
230
|
-
if (!proceed) {
|
|
231
|
-
console.log(" Skipping skill copy.");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
236
|
let sourceDir;
|
|
235
237
|
try {
|
|
236
238
|
sourceDir = getPackageSkillsDir();
|
|
@@ -246,7 +248,9 @@ ${verb} libretto skills in ${dirNames}?`);
|
|
|
246
248
|
}
|
|
247
249
|
cpSync(sourceDir, skillDest, { recursive: true });
|
|
248
250
|
const fileCount = readdirSync(skillDest).length;
|
|
249
|
-
console.log(
|
|
251
|
+
console.log(
|
|
252
|
+
` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`
|
|
253
|
+
);
|
|
250
254
|
}
|
|
251
255
|
}
|
|
252
256
|
const initInput = SimpleCLI.input({
|
|
@@ -267,10 +271,11 @@ const initCommand = SimpleCLI.command({
|
|
|
267
271
|
} else {
|
|
268
272
|
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
269
273
|
}
|
|
274
|
+
copySkills();
|
|
270
275
|
if (process.stdin.isTTY) {
|
|
271
|
-
await copySkills();
|
|
272
276
|
await runInteractiveApiSetup();
|
|
273
277
|
} else {
|
|
278
|
+
loadSnapshotEnv();
|
|
274
279
|
printSnapshotApiStatus();
|
|
275
280
|
}
|
|
276
281
|
console.log("\n\u2713 libretto init complete");
|
|
@@ -12,10 +12,9 @@ import {
|
|
|
12
12
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
13
|
import {
|
|
14
14
|
integerOption,
|
|
15
|
-
loadSessionStateMiddleware,
|
|
16
15
|
pageOption,
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
sessionOption,
|
|
17
|
+
withRequiredSession
|
|
19
18
|
} from "./shared.js";
|
|
20
19
|
async function resolvePageId(session, pageId) {
|
|
21
20
|
if (!pageId) return void 0;
|
|
@@ -44,7 +43,7 @@ const networkInput = SimpleCLI.input({
|
|
|
44
43
|
});
|
|
45
44
|
const networkCommand = SimpleCLI.command({
|
|
46
45
|
description: "View captured network requests"
|
|
47
|
-
}).input(networkInput).use(
|
|
46
|
+
}).input(networkInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
48
47
|
if (input.clear) {
|
|
49
48
|
clearNetworkLog(ctx.session);
|
|
50
49
|
console.log("Network log cleared.");
|
|
@@ -81,7 +80,7 @@ const actionsInput = SimpleCLI.input({
|
|
|
81
80
|
});
|
|
82
81
|
const actionsCommand = SimpleCLI.command({
|
|
83
82
|
description: "View captured actions"
|
|
84
|
-
}).input(actionsInput).use(
|
|
83
|
+
}).input(actionsInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
85
84
|
if (input.clear) {
|
|
86
85
|
clearActionLog(ctx.session);
|
|
87
86
|
console.log("Action log cleared.");
|