libretto 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -8,7 +8,9 @@ import type { LoggerApi } from "../../shared/logger/index.js";
|
|
|
8
8
|
import {
|
|
9
9
|
connect,
|
|
10
10
|
disconnectBrowser,
|
|
11
|
+
resolveViewport,
|
|
11
12
|
} from "../core/browser.js";
|
|
13
|
+
import { parseViewportArg } from "./browser.js";
|
|
12
14
|
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
13
15
|
import {
|
|
14
16
|
assertSessionAvailableForStart,
|
|
@@ -22,15 +24,13 @@ import {
|
|
|
22
24
|
readNetworkLog,
|
|
23
25
|
wrapPageForActionLogging,
|
|
24
26
|
} from "../core/telemetry.js";
|
|
25
|
-
import type {
|
|
26
|
-
RunIntegrationWorkerRequest,
|
|
27
|
-
} from "../workers/run-integration-worker-protocol.js";
|
|
27
|
+
import type { RunIntegrationWorkerRequest } from "../workers/run-integration-worker-protocol.js";
|
|
28
28
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
29
29
|
import {
|
|
30
|
-
loadSessionStateMiddleware,
|
|
31
30
|
pageOption,
|
|
32
|
-
resolveSessionMiddleware,
|
|
33
31
|
sessionOption,
|
|
32
|
+
withAutoSession,
|
|
33
|
+
withRequiredSession,
|
|
34
34
|
} from "./shared.js";
|
|
35
35
|
|
|
36
36
|
type ExecFunction = (...args: unknown[]) => Promise<unknown>;
|
|
@@ -117,6 +117,87 @@ function compileExecFunction(
|
|
|
117
117
|
return new AsyncFunction(...helperNames, code);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Strip `.catch(() => {})` / `?.catch(() => {})` from executable code,
|
|
122
|
+
* skipping occurrences inside string literals (single, double, backtick)
|
|
123
|
+
* and single-line / multi-line comments so we never corrupt non-code text.
|
|
124
|
+
*/
|
|
125
|
+
function stripEmptyCatchHandlers(code: string): {
|
|
126
|
+
cleaned: string;
|
|
127
|
+
strippedCount: number;
|
|
128
|
+
} {
|
|
129
|
+
const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
|
|
130
|
+
let strippedCount = 0;
|
|
131
|
+
let result = "";
|
|
132
|
+
let i = 0;
|
|
133
|
+
|
|
134
|
+
while (i < code.length) {
|
|
135
|
+
// Single-line comment
|
|
136
|
+
if (code[i] === "/" && code[i + 1] === "/") {
|
|
137
|
+
const end = code.indexOf("\n", i);
|
|
138
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
|
|
139
|
+
result += slice;
|
|
140
|
+
i += slice.length;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
// Multi-line comment
|
|
144
|
+
if (code[i] === "/" && code[i + 1] === "*") {
|
|
145
|
+
const end = code.indexOf("*/", i + 2);
|
|
146
|
+
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
|
|
147
|
+
result += slice;
|
|
148
|
+
i += slice.length;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// String literals
|
|
152
|
+
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
153
|
+
const quote = code[i];
|
|
154
|
+
let j = i + 1;
|
|
155
|
+
while (j < code.length) {
|
|
156
|
+
if (code[j] === "\\" && quote !== "`") {
|
|
157
|
+
j += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (code[j] === "\\" && quote === "`") {
|
|
161
|
+
j += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (code[j] === quote) {
|
|
165
|
+
j++;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
// Template literal interpolation — skip nested braces
|
|
169
|
+
if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
|
|
170
|
+
let depth = 1;
|
|
171
|
+
j += 2;
|
|
172
|
+
while (j < code.length && depth > 0) {
|
|
173
|
+
if (code[j] === "{") depth++;
|
|
174
|
+
else if (code[j] === "}") depth--;
|
|
175
|
+
j++;
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
j++;
|
|
180
|
+
}
|
|
181
|
+
result += code.slice(i, j);
|
|
182
|
+
i = j;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Try to match the catch pattern at the current position
|
|
186
|
+
catchRe.lastIndex = i;
|
|
187
|
+
const match = catchRe.exec(code);
|
|
188
|
+
if (match && match.index === i) {
|
|
189
|
+
strippedCount++;
|
|
190
|
+
i += match[0].length;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// Regular character
|
|
194
|
+
result += code[i];
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { cleaned: result, strippedCount };
|
|
199
|
+
}
|
|
200
|
+
|
|
120
201
|
async function runExec(
|
|
121
202
|
code: string,
|
|
122
203
|
session: string,
|
|
@@ -124,22 +205,26 @@ async function runExec(
|
|
|
124
205
|
visualize = false,
|
|
125
206
|
pageId?: string,
|
|
126
207
|
): Promise<void> {
|
|
208
|
+
const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
|
|
209
|
+
if (strippedCount > 0) {
|
|
210
|
+
console.log("(Stripped `.catch(() => {})` — letting errors bubble up)");
|
|
211
|
+
}
|
|
127
212
|
logger.info("exec-start", {
|
|
128
213
|
session,
|
|
129
|
-
codeLength:
|
|
130
|
-
codePreview:
|
|
214
|
+
codeLength: cleanedCode.length,
|
|
215
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
131
216
|
visualize,
|
|
132
217
|
pageId,
|
|
133
218
|
});
|
|
134
|
-
const {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
219
|
+
const {
|
|
220
|
+
browser,
|
|
221
|
+
context,
|
|
222
|
+
page,
|
|
223
|
+
pageId: resolvedPageId,
|
|
224
|
+
} = await connect(session, logger, 10000, {
|
|
225
|
+
pageId,
|
|
226
|
+
requireSinglePage: true,
|
|
227
|
+
});
|
|
143
228
|
|
|
144
229
|
const STALL_THRESHOLD_MS = 60_000;
|
|
145
230
|
let lastActivityTs = Date.now();
|
|
@@ -153,10 +238,10 @@ async function runExec(
|
|
|
153
238
|
logger.warn("exec-stall-warning", {
|
|
154
239
|
session,
|
|
155
240
|
silenceMs,
|
|
156
|
-
codePreview:
|
|
241
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
157
242
|
});
|
|
158
243
|
console.warn(
|
|
159
|
-
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — exec may be hung (code: ${
|
|
244
|
+
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — exec may be hung (code: ${cleanedCode.slice(0, 100)}...)`,
|
|
160
245
|
);
|
|
161
246
|
}
|
|
162
247
|
}, STALL_THRESHOLD_MS);
|
|
@@ -166,7 +251,7 @@ async function runExec(
|
|
|
166
251
|
logger.info("exec-interrupted", {
|
|
167
252
|
session,
|
|
168
253
|
duration: Date.now() - execStartTs,
|
|
169
|
-
codePreview:
|
|
254
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
170
255
|
});
|
|
171
256
|
};
|
|
172
257
|
process.on("SIGINT", sigintHandler);
|
|
@@ -181,7 +266,12 @@ async function runExec(
|
|
|
181
266
|
const execState: Record<string, unknown> = {};
|
|
182
267
|
|
|
183
268
|
const networkLog = (
|
|
184
|
-
opts: {
|
|
269
|
+
opts: {
|
|
270
|
+
last?: number;
|
|
271
|
+
filter?: string;
|
|
272
|
+
method?: string;
|
|
273
|
+
pageId?: string;
|
|
274
|
+
} = {},
|
|
185
275
|
) => {
|
|
186
276
|
return readNetworkLog(session, opts);
|
|
187
277
|
};
|
|
@@ -216,7 +306,7 @@ async function runExec(
|
|
|
216
306
|
};
|
|
217
307
|
|
|
218
308
|
const helperNames = Object.keys(helpers);
|
|
219
|
-
const fn = compileExecFunction(
|
|
309
|
+
const fn = compileExecFunction(cleanedCode, helperNames);
|
|
220
310
|
|
|
221
311
|
const result = await fn(...Object.values(helpers));
|
|
222
312
|
logger.info("exec-success", { session, hasResult: result !== undefined });
|
|
@@ -224,12 +314,14 @@ async function runExec(
|
|
|
224
314
|
console.log(
|
|
225
315
|
typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
226
316
|
);
|
|
317
|
+
} else {
|
|
318
|
+
console.log("Executed successfully");
|
|
227
319
|
}
|
|
228
320
|
} catch (err) {
|
|
229
321
|
logger.error("exec-error", {
|
|
230
322
|
error: err,
|
|
231
323
|
session,
|
|
232
|
-
codePreview:
|
|
324
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
233
325
|
});
|
|
234
326
|
throw err;
|
|
235
327
|
} finally {
|
|
@@ -273,6 +365,8 @@ async function stopExistingFailedRunSession(
|
|
|
273
365
|
});
|
|
274
366
|
clearSessionState(session, logger);
|
|
275
367
|
|
|
368
|
+
if (existingState.pid == null) return;
|
|
369
|
+
|
|
276
370
|
const stopDeadline = Date.now() + 3_000;
|
|
277
371
|
while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
|
|
278
372
|
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
@@ -371,11 +465,19 @@ async function waitForWorkflowOutcome(
|
|
|
371
465
|
let outputOffset = 0;
|
|
372
466
|
|
|
373
467
|
while (true) {
|
|
374
|
-
outputOffset = streamOutputSince(
|
|
468
|
+
outputOffset = streamOutputSince(
|
|
469
|
+
signalPaths.outputSignalPath,
|
|
470
|
+
outputOffset,
|
|
471
|
+
);
|
|
375
472
|
|
|
376
473
|
if (existsSync(signalPaths.failedSignalPath)) {
|
|
377
|
-
outputOffset = streamOutputSince(
|
|
378
|
-
|
|
474
|
+
outputOffset = streamOutputSince(
|
|
475
|
+
signalPaths.outputSignalPath,
|
|
476
|
+
outputOffset,
|
|
477
|
+
);
|
|
478
|
+
const failureDetails = await waitForFailureDetails(
|
|
479
|
+
signalPaths.failedSignalPath,
|
|
480
|
+
);
|
|
379
481
|
return {
|
|
380
482
|
status: "failed",
|
|
381
483
|
message: failureDetails?.message,
|
|
@@ -384,17 +486,26 @@ async function waitForWorkflowOutcome(
|
|
|
384
486
|
}
|
|
385
487
|
|
|
386
488
|
if (existsSync(signalPaths.completedSignalPath)) {
|
|
387
|
-
outputOffset = streamOutputSince(
|
|
489
|
+
outputOffset = streamOutputSince(
|
|
490
|
+
signalPaths.outputSignalPath,
|
|
491
|
+
outputOffset,
|
|
492
|
+
);
|
|
388
493
|
return { status: "completed" };
|
|
389
494
|
}
|
|
390
495
|
|
|
391
496
|
if (existsSync(signalPaths.pausedSignalPath)) {
|
|
392
|
-
outputOffset = streamOutputSince(
|
|
497
|
+
outputOffset = streamOutputSince(
|
|
498
|
+
signalPaths.outputSignalPath,
|
|
499
|
+
outputOffset,
|
|
500
|
+
);
|
|
393
501
|
return { status: "paused" };
|
|
394
502
|
}
|
|
395
503
|
|
|
396
504
|
if (!isProcessRunning(args.pid)) {
|
|
397
|
-
outputOffset = streamOutputSince(
|
|
505
|
+
outputOffset = streamOutputSince(
|
|
506
|
+
signalPaths.outputSignalPath,
|
|
507
|
+
outputOffset,
|
|
508
|
+
);
|
|
398
509
|
return { status: "exited" };
|
|
399
510
|
}
|
|
400
511
|
|
|
@@ -421,9 +532,9 @@ async function runResume(
|
|
|
421
532
|
);
|
|
422
533
|
}
|
|
423
534
|
|
|
424
|
-
if (!isProcessRunning(sessionState.pid)) {
|
|
535
|
+
if (sessionState.pid == null || !isProcessRunning(sessionState.pid)) {
|
|
425
536
|
throw new Error(
|
|
426
|
-
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`,
|
|
537
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid ?? "unknown"} is not running).`,
|
|
427
538
|
);
|
|
428
539
|
}
|
|
429
540
|
|
|
@@ -451,7 +562,7 @@ async function runResume(
|
|
|
451
562
|
|
|
452
563
|
const outcome = await waitForWorkflowOutcome({
|
|
453
564
|
session,
|
|
454
|
-
pid: sessionState.pid
|
|
565
|
+
pid: sessionState.pid!,
|
|
455
566
|
});
|
|
456
567
|
|
|
457
568
|
if (outcome.status === "completed") {
|
|
@@ -498,18 +609,24 @@ async function runIntegrationFromFile(
|
|
|
498
609
|
session: args.session,
|
|
499
610
|
params: args.params,
|
|
500
611
|
headless: args.headless,
|
|
612
|
+
visualize: args.visualize,
|
|
501
613
|
authProfileDomain: args.authProfileDomain,
|
|
614
|
+
viewport: args.viewport,
|
|
502
615
|
} satisfies RunIntegrationWorkerRequest);
|
|
503
|
-
const worker = spawn(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
616
|
+
const worker = spawn(
|
|
617
|
+
process.execPath,
|
|
618
|
+
[
|
|
619
|
+
tsxCliPath,
|
|
620
|
+
...(args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : []),
|
|
621
|
+
workerEntryPath,
|
|
622
|
+
payload,
|
|
623
|
+
],
|
|
624
|
+
{
|
|
625
|
+
detached: true,
|
|
626
|
+
stdio: "ignore",
|
|
627
|
+
env: process.env,
|
|
628
|
+
},
|
|
629
|
+
);
|
|
513
630
|
worker.unref();
|
|
514
631
|
const outcome = await waitForWorkflowOutcome({
|
|
515
632
|
session: args.session,
|
|
@@ -548,7 +665,9 @@ export const execInput = SimpleCLI.input({
|
|
|
548
665
|
],
|
|
549
666
|
named: {
|
|
550
667
|
session: sessionOption(),
|
|
551
|
-
visualize: SimpleCLI.flag({
|
|
668
|
+
visualize: SimpleCLI.flag({
|
|
669
|
+
help: "Enable ghost cursor + highlight visualization",
|
|
670
|
+
}),
|
|
552
671
|
page: pageOption(),
|
|
553
672
|
},
|
|
554
673
|
}).refine(
|
|
@@ -556,26 +675,22 @@ export const execInput = SimpleCLI.input({
|
|
|
556
675
|
`Usage: libretto exec <code> [--session <name>] [--visualize]`,
|
|
557
676
|
);
|
|
558
677
|
|
|
559
|
-
export
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
);
|
|
574
|
-
});
|
|
575
|
-
}
|
|
678
|
+
export const execCommand = SimpleCLI.command({
|
|
679
|
+
description: "Execute Playwright TypeScript code",
|
|
680
|
+
})
|
|
681
|
+
.input(execInput)
|
|
682
|
+
.use(withRequiredSession())
|
|
683
|
+
.handle(async ({ input, ctx }) => {
|
|
684
|
+
await runExec(
|
|
685
|
+
input.codeParts.join(" "),
|
|
686
|
+
ctx.session,
|
|
687
|
+
ctx.logger,
|
|
688
|
+
input.visualize,
|
|
689
|
+
input.page,
|
|
690
|
+
);
|
|
691
|
+
});
|
|
576
692
|
|
|
577
|
-
const runUsage =
|
|
578
|
-
`Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]`;
|
|
693
|
+
const runUsage = `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
579
694
|
|
|
580
695
|
export const runInput = SimpleCLI.input({
|
|
581
696
|
positionals: [
|
|
@@ -600,18 +715,31 @@ export const runInput = SimpleCLI.input({
|
|
|
600
715
|
}),
|
|
601
716
|
headed: SimpleCLI.flag({ help: "Run in headed mode" }),
|
|
602
717
|
headless: SimpleCLI.flag({ help: "Run in headless mode" }),
|
|
718
|
+
noVisualize: SimpleCLI.flag({
|
|
719
|
+
name: "no-visualize",
|
|
720
|
+
help: "Disable ghost cursor + highlight visualization in headed mode",
|
|
721
|
+
}),
|
|
603
722
|
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
604
723
|
name: "auth-profile",
|
|
605
724
|
help: "Domain for local auth profile (e.g. apps.example.com)",
|
|
606
725
|
}),
|
|
726
|
+
viewport: SimpleCLI.option(z.string().optional(), {
|
|
727
|
+
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
|
|
728
|
+
}),
|
|
607
729
|
},
|
|
608
730
|
})
|
|
609
731
|
.refine(
|
|
610
732
|
(input) => Boolean(input.integrationFile && input.integrationExport),
|
|
611
733
|
runUsage,
|
|
612
734
|
)
|
|
613
|
-
.refine(
|
|
614
|
-
|
|
735
|
+
.refine(
|
|
736
|
+
(input) => !(input.params && input.paramsFile),
|
|
737
|
+
"Pass either --params or --params-file, not both.",
|
|
738
|
+
)
|
|
739
|
+
.refine(
|
|
740
|
+
(input) => !(input.headed && input.headless),
|
|
741
|
+
"Cannot pass both --headed and --headless.",
|
|
742
|
+
);
|
|
615
743
|
|
|
616
744
|
function resolveRunParams(
|
|
617
745
|
rawInlineParams: string | undefined,
|
|
@@ -634,34 +762,42 @@ function resolveRunParams(
|
|
|
634
762
|
return {};
|
|
635
763
|
}
|
|
636
764
|
|
|
637
|
-
export
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
765
|
+
export const runCommand = SimpleCLI.command({
|
|
766
|
+
description: "Run an exported Libretto workflow from a file",
|
|
767
|
+
})
|
|
768
|
+
.input(runInput)
|
|
769
|
+
.use(withAutoSession())
|
|
770
|
+
.handle(async ({ input, ctx }) => {
|
|
771
|
+
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
772
|
+
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
773
|
+
|
|
774
|
+
const params = resolveRunParams(input.params, input.paramsFile);
|
|
775
|
+
const headlessMode = input.headed
|
|
776
|
+
? false
|
|
777
|
+
: input.headless
|
|
778
|
+
? true
|
|
779
|
+
: undefined;
|
|
780
|
+
const visualize = !input.noVisualize;
|
|
781
|
+
const viewport = resolveViewport(
|
|
782
|
+
parseViewportArg(input.viewport),
|
|
783
|
+
ctx.logger,
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
await runIntegrationFromFile(
|
|
787
|
+
{
|
|
655
788
|
integrationPath: input.integrationFile!,
|
|
656
789
|
exportName: input.integrationExport!,
|
|
657
790
|
session: ctx.session,
|
|
658
791
|
params,
|
|
659
792
|
tsconfigPath: input.tsconfig,
|
|
660
793
|
headless: headlessMode ?? false,
|
|
794
|
+
visualize,
|
|
661
795
|
authProfileDomain: input.authProfile,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
796
|
+
viewport,
|
|
797
|
+
},
|
|
798
|
+
ctx.logger,
|
|
799
|
+
);
|
|
800
|
+
});
|
|
665
801
|
|
|
666
802
|
export const resumeInput = SimpleCLI.input({
|
|
667
803
|
positionals: [],
|
|
@@ -670,22 +806,17 @@ export const resumeInput = SimpleCLI.input({
|
|
|
670
806
|
},
|
|
671
807
|
});
|
|
672
808
|
|
|
673
|
-
export
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
.
|
|
680
|
-
|
|
681
|
-
await runResume(ctx.session, logger, ctx.sessionState);
|
|
682
|
-
});
|
|
683
|
-
}
|
|
809
|
+
export const resumeCommand = SimpleCLI.command({
|
|
810
|
+
description: "Resume a paused workflow for the current session",
|
|
811
|
+
})
|
|
812
|
+
.input(resumeInput)
|
|
813
|
+
.use(withRequiredSession())
|
|
814
|
+
.handle(async ({ ctx }) => {
|
|
815
|
+
await runResume(ctx.session, ctx.logger, ctx.sessionState);
|
|
816
|
+
});
|
|
684
817
|
|
|
685
|
-
export
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
};
|
|
691
|
-
}
|
|
818
|
+
export const executionCommands = {
|
|
819
|
+
exec: execCommand,
|
|
820
|
+
run: runCommand,
|
|
821
|
+
resume: resumeCommand,
|
|
822
|
+
};
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -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
|
|
|
15
23
|
type ProviderChoice = {
|
|
16
24
|
key: string;
|
|
@@ -42,7 +50,8 @@ const PROVIDER_CHOICES: ProviderChoice[] = [
|
|
|
42
50
|
key: "4",
|
|
43
51
|
label: "Google Vertex AI",
|
|
44
52
|
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
45
|
-
envHint:
|
|
53
|
+
envHint:
|
|
54
|
+
"Requires gcloud auth application-default login and a GCP project ID",
|
|
46
55
|
},
|
|
47
56
|
];
|
|
48
57
|
|
|
@@ -57,16 +66,6 @@ function promptUser(
|
|
|
57
66
|
});
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
function askYesNo(question: string): Promise<boolean> {
|
|
61
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
62
|
-
return new Promise((resolve) => {
|
|
63
|
-
rl.question(`${question} (y/N) `, (answer) => {
|
|
64
|
-
rl.close();
|
|
65
|
-
resolve(answer.trim().toLowerCase() === "y");
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
69
|
function safeReadAiConfig(): ReturnType<typeof readAiConfig> {
|
|
71
70
|
try {
|
|
72
71
|
return readAiConfig();
|
|
@@ -101,7 +100,9 @@ function printSnapshotApiStatus(): void {
|
|
|
101
100
|
|
|
102
101
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
103
102
|
console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
|
|
104
|
-
console.log(
|
|
103
|
+
console.log(
|
|
104
|
+
" Snapshot objectives will use the API analyzer by default.",
|
|
105
|
+
);
|
|
105
106
|
console.log(" No further action required.");
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
@@ -117,7 +118,9 @@ function printSnapshotApiStatus(): void {
|
|
|
117
118
|
console.log(
|
|
118
119
|
" Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
|
|
119
120
|
);
|
|
120
|
-
console.log(
|
|
121
|
+
console.log(
|
|
122
|
+
" Run `npx libretto init` interactively to set up credentials.",
|
|
123
|
+
);
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
async function runInteractiveApiSetup(): Promise<void> {
|
|
@@ -132,7 +135,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
132
135
|
|
|
133
136
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
134
137
|
console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
|
|
135
|
-
console.log(
|
|
138
|
+
console.log(
|
|
139
|
+
" Snapshot objectives will use the API analyzer by default.",
|
|
140
|
+
);
|
|
136
141
|
return;
|
|
137
142
|
}
|
|
138
143
|
|
|
@@ -144,7 +149,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
144
149
|
});
|
|
145
150
|
|
|
146
151
|
try {
|
|
147
|
-
console.log(
|
|
152
|
+
console.log(
|
|
153
|
+
" Which API provider would you like to use for snapshot analysis?\n",
|
|
154
|
+
);
|
|
148
155
|
for (const choice of PROVIDER_CHOICES) {
|
|
149
156
|
console.log(` ${choice.key}) ${choice.label}`);
|
|
150
157
|
}
|
|
@@ -153,7 +160,9 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
153
160
|
const answer = await promptUser(rl, " Choice: ");
|
|
154
161
|
|
|
155
162
|
if (answer.toLowerCase() === "s" || !answer) {
|
|
156
|
-
console.log(
|
|
163
|
+
console.log(
|
|
164
|
+
"\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.",
|
|
165
|
+
);
|
|
157
166
|
console.log(" Or add credentials directly to your .env file:");
|
|
158
167
|
console.log(" OPENAI_API_KEY=...");
|
|
159
168
|
console.log(" ANTHROPIC_API_KEY=...");
|
|
@@ -251,26 +260,17 @@ function detectAgentDirs(root: string): string[] {
|
|
|
251
260
|
return dirs;
|
|
252
261
|
}
|
|
253
262
|
|
|
254
|
-
|
|
263
|
+
function copySkills(): void {
|
|
255
264
|
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
256
265
|
|
|
257
266
|
if (agentDirs.length === 0) {
|
|
258
|
-
console.log(
|
|
267
|
+
console.log(
|
|
268
|
+
"\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.",
|
|
269
|
+
);
|
|
259
270
|
return;
|
|
260
271
|
}
|
|
261
272
|
|
|
262
273
|
const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
|
|
263
|
-
const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
|
|
264
|
-
// Say "Overwrite" if skills already exist in ANY target dir — skills must
|
|
265
|
-
// be identical across coding agents, so we always copy to all of them.
|
|
266
|
-
const existing = destinations.filter((d) => existsSync(d));
|
|
267
|
-
const verb = existing.length > 0 ? "Overwrite" : "Install";
|
|
268
|
-
|
|
269
|
-
const proceed = await askYesNo(`\n${verb} libretto skills in ${dirNames}?`);
|
|
270
|
-
if (!proceed) {
|
|
271
|
-
console.log(" Skipping skill copy.");
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
274
|
|
|
275
275
|
let sourceDir: string;
|
|
276
276
|
try {
|
|
@@ -289,7 +289,9 @@ async function copySkills(): Promise<void> {
|
|
|
289
289
|
}
|
|
290
290
|
cpSync(sourceDir, skillDest, { recursive: true });
|
|
291
291
|
const fileCount = readdirSync(skillDest).length;
|
|
292
|
-
console.log(
|
|
292
|
+
console.log(
|
|
293
|
+
` ✓ Copied ${fileCount} skill files to ${name}/skills/libretto/`,
|
|
294
|
+
);
|
|
293
295
|
}
|
|
294
296
|
}
|
|
295
297
|
|
|
@@ -316,10 +318,12 @@ export const initCommand = SimpleCLI.command({
|
|
|
316
318
|
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
317
319
|
}
|
|
318
320
|
|
|
321
|
+
copySkills();
|
|
322
|
+
|
|
319
323
|
if (process.stdin.isTTY) {
|
|
320
|
-
await copySkills();
|
|
321
324
|
await runInteractiveApiSetup();
|
|
322
325
|
} else {
|
|
326
|
+
loadSnapshotEnv();
|
|
323
327
|
printSnapshotApiStatus();
|
|
324
328
|
}
|
|
325
329
|
|