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
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
mkdtempSync,
|
|
4
|
-
readFileSync,
|
|
5
|
-
rmSync
|
|
6
|
-
} from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
7
2
|
import { extname, isAbsolute, join, resolve } from "node:path";
|
|
8
3
|
import { spawn } from "node:child_process";
|
|
9
4
|
import { tmpdir } from "node:os";
|
|
@@ -60,7 +55,12 @@ Screenshot file path: ${pngPath}
|
|
|
60
55
|
Use the screenshot alongside the HTML snapshot context above.`;
|
|
61
56
|
}
|
|
62
57
|
async runAnalyzer(args, logger, stdinText) {
|
|
63
|
-
const result = await runExternalCommand(
|
|
58
|
+
const result = await runExternalCommand(
|
|
59
|
+
this.command,
|
|
60
|
+
args,
|
|
61
|
+
logger,
|
|
62
|
+
stdinText
|
|
63
|
+
);
|
|
64
64
|
if (result.exitCode !== 0) {
|
|
65
65
|
throw new Error(
|
|
66
66
|
`Analyzer command failed (${[this.command, ...args].join(" ")}).
|
|
@@ -535,7 +535,9 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
|
|
|
535
535
|
fullDomChars: fullHtmlContent.length,
|
|
536
536
|
fullDomEstimatedTokens: estimateTokensFromChars(fullHtmlContent.length),
|
|
537
537
|
condensedDomChars: condensedHtmlContent.length,
|
|
538
|
-
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
538
|
+
condensedDomEstimatedTokens: estimateTokensFromChars(
|
|
539
|
+
condensedHtmlContent.length
|
|
540
|
+
),
|
|
539
541
|
configuredModel: model
|
|
540
542
|
};
|
|
541
543
|
const buildCandidate = (domSource, htmlContent, selectionReason, truncated) => {
|
|
@@ -607,7 +609,10 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
|
|
|
607
609
|
2e3,
|
|
608
610
|
budget.promptBudgetTokens - estimateTokensFromChars(basePrompt.length)
|
|
609
611
|
);
|
|
610
|
-
const truncatedHtml = truncateText(
|
|
612
|
+
const truncatedHtml = truncateText(
|
|
613
|
+
condensedHtmlContent,
|
|
614
|
+
availableHtmlTokens * 4
|
|
615
|
+
);
|
|
611
616
|
return buildCandidate(
|
|
612
617
|
"condensed",
|
|
613
618
|
truncatedHtml.text,
|
|
@@ -615,27 +620,6 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
|
|
|
615
620
|
truncatedHtml.truncated
|
|
616
621
|
);
|
|
617
622
|
}
|
|
618
|
-
function formatInterpretationOutput(parsed, header = "Interpretation:") {
|
|
619
|
-
const outputLines = [];
|
|
620
|
-
outputLines.push(header);
|
|
621
|
-
outputLines.push(`Answer: ${parsed.answer}`);
|
|
622
|
-
outputLines.push("");
|
|
623
|
-
if (parsed.selectors.length === 0) {
|
|
624
|
-
outputLines.push("Selectors: none found.");
|
|
625
|
-
} else {
|
|
626
|
-
outputLines.push("Selectors:");
|
|
627
|
-
parsed.selectors.forEach((selector, index) => {
|
|
628
|
-
outputLines.push(` ${index + 1}. ${selector.label}`);
|
|
629
|
-
outputLines.push(` selector: ${selector.selector}`);
|
|
630
|
-
outputLines.push(` rationale: ${selector.rationale}`);
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
if (parsed.notes && parsed.notes.trim()) {
|
|
634
|
-
outputLines.push("");
|
|
635
|
-
outputLines.push(`Notes: ${parsed.notes.trim()}`);
|
|
636
|
-
}
|
|
637
|
-
return outputLines.join("\n");
|
|
638
|
-
}
|
|
639
623
|
async function runInterpret(args, logger) {
|
|
640
624
|
logger.info("interpret-start", {
|
|
641
625
|
objective: args.objective,
|
|
@@ -676,7 +660,6 @@ export {
|
|
|
676
660
|
InterpretResultSchema,
|
|
677
661
|
buildInlinePromptSelection,
|
|
678
662
|
canAnalyzeSnapshots,
|
|
679
|
-
formatInterpretationOutput,
|
|
680
663
|
getMimeType,
|
|
681
664
|
readFileAsBase64,
|
|
682
665
|
runInterpret
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
readAiConfig
|
|
5
|
-
} from "./ai-config.js";
|
|
3
|
+
import { readAiConfig } from "./ai-config.js";
|
|
6
4
|
import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
|
|
7
5
|
import {
|
|
8
6
|
hasProviderCredentials,
|
|
@@ -154,9 +152,7 @@ function resolveSnapshotApiModel(config = readAiConfig()) {
|
|
|
154
152
|
function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
|
|
155
153
|
const selection = resolveSnapshotApiModel(config);
|
|
156
154
|
if (!selection) {
|
|
157
|
-
throw new SnapshotApiUnavailableError(
|
|
158
|
-
noSnapshotApiConfiguredMessage()
|
|
159
|
-
);
|
|
155
|
+
throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
|
|
160
156
|
}
|
|
161
157
|
if (!hasProviderCredentials(selection.provider)) {
|
|
162
158
|
throw new SnapshotApiUnavailableError(
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync
|
|
6
|
+
} from "node:fs";
|
|
2
7
|
import {
|
|
3
8
|
getSessionActionsLogPath,
|
|
4
9
|
getSessionNetworkLogPath
|
|
@@ -56,7 +61,10 @@ function clearNetworkLog(session) {
|
|
|
56
61
|
function parentLogAction(session, entry) {
|
|
57
62
|
try {
|
|
58
63
|
const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
|
|
59
|
-
appendFileSync(
|
|
64
|
+
appendFileSync(
|
|
65
|
+
getSessionActionsLogPath(session),
|
|
66
|
+
JSON.stringify(record) + "\n"
|
|
67
|
+
);
|
|
60
68
|
} catch {
|
|
61
69
|
}
|
|
62
70
|
}
|
|
@@ -79,7 +87,7 @@ function readActionLog(session, opts = {}) {
|
|
|
79
87
|
if (opts.filter) {
|
|
80
88
|
const re = new RegExp(opts.filter, "i");
|
|
81
89
|
entries = entries.filter(
|
|
82
|
-
(e) => re.test(e.action) || re.test(e.selector || "") || re.test(e.value || "") || re.test(e.url || "")
|
|
90
|
+
(e) => re.test(e.action) || re.test(e.selector || "") || re.test(e.bestSemanticSelector || "") || re.test(e.targetSelector || "") || re.test((e.ancestorSelectors || []).join(" ")) || re.test(e.nearbyText || "") || re.test((e.composedPath || []).join(" ")) || re.test(e.value || "") || re.test(e.url || "")
|
|
83
91
|
);
|
|
84
92
|
}
|
|
85
93
|
if (opts.pageId) {
|
|
@@ -94,8 +102,16 @@ function readActionLog(session, opts = {}) {
|
|
|
94
102
|
function formatActionEntry(e) {
|
|
95
103
|
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
96
104
|
const src = e.source.toUpperCase().padEnd(5);
|
|
105
|
+
const displaySelector = e.bestSemanticSelector || e.selector;
|
|
97
106
|
const parts = [`[${time}]`, `[${src}]`, e.action];
|
|
98
|
-
if (
|
|
107
|
+
if (displaySelector) parts.push(displaySelector);
|
|
108
|
+
if (e.targetSelector && e.targetSelector !== displaySelector) {
|
|
109
|
+
parts.push(`target=${e.targetSelector}`);
|
|
110
|
+
}
|
|
111
|
+
if (e.nearbyText) parts.push(`text="${e.nearbyText}"`);
|
|
112
|
+
if (e.coordinates) {
|
|
113
|
+
parts.push(`@(${e.coordinates.x},${e.coordinates.y})`);
|
|
114
|
+
}
|
|
99
115
|
if (e.value) parts.push(`"${e.value}"`);
|
|
100
116
|
if (e.url) parts.push(e.url);
|
|
101
117
|
if (e.duration != null) parts.push(`${e.duration}ms`);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
const EXPERIMENTAL_COMMAND_PREFIX = "experimental";
|
|
3
|
+
const EXPERIMENTAL_GROUP_DESCRIPTION = "Experimental commands";
|
|
2
4
|
function toCamelCase(input2) {
|
|
3
5
|
return input2.replace(
|
|
4
6
|
/-([a-zA-Z0-9])/g,
|
|
@@ -214,7 +216,9 @@ class SimpleCLIApp {
|
|
|
214
216
|
if (isHelpFlag(argsBeforePassthrough[0])) {
|
|
215
217
|
return [];
|
|
216
218
|
}
|
|
217
|
-
const helpFlagIndex = argsBeforePassthrough.findIndex(
|
|
219
|
+
const helpFlagIndex = argsBeforePassthrough.findIndex(
|
|
220
|
+
(arg) => isHelpFlag(arg)
|
|
221
|
+
);
|
|
218
222
|
if (helpFlagIndex >= 0) {
|
|
219
223
|
return argsBeforePassthrough.slice(0, helpFlagIndex);
|
|
220
224
|
}
|
|
@@ -229,7 +233,10 @@ class SimpleCLIApp {
|
|
|
229
233
|
}
|
|
230
234
|
throw new Error(`Unknown command: ${args.join(" ")}`);
|
|
231
235
|
}
|
|
232
|
-
const rawInput = this.parseCommandInput(
|
|
236
|
+
const rawInput = this.parseCommandInput(
|
|
237
|
+
command2,
|
|
238
|
+
args.slice(command2.path.length)
|
|
239
|
+
);
|
|
233
240
|
return {
|
|
234
241
|
routeKey: command2.routeKey,
|
|
235
242
|
rawInput
|
|
@@ -239,7 +246,9 @@ class SimpleCLIApp {
|
|
|
239
246
|
const inputDefinition = command2.input?.getDefinition();
|
|
240
247
|
if (!inputDefinition) {
|
|
241
248
|
if (args.length > 0) {
|
|
242
|
-
throw new Error(
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Unexpected arguments for ${this.name} ${command2.path.join(" ")}.`
|
|
251
|
+
);
|
|
243
252
|
}
|
|
244
253
|
return {
|
|
245
254
|
positionals: [],
|
|
@@ -256,11 +265,17 @@ class SimpleCLIApp {
|
|
|
256
265
|
const arg = args[index];
|
|
257
266
|
if (arg === "--") {
|
|
258
267
|
if (!passthroughEntry) {
|
|
259
|
-
throw new Error(
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Unexpected "--" for ${this.name} ${command2.path.join(" ")}.`
|
|
270
|
+
);
|
|
260
271
|
}
|
|
261
272
|
named["--"] = args.slice(index + 1);
|
|
262
273
|
break;
|
|
263
274
|
}
|
|
275
|
+
if (arg === "-") {
|
|
276
|
+
positionals.push(arg);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
264
279
|
if (arg.startsWith("--")) {
|
|
265
280
|
const [rawName, inlineValue] = splitNamedArg(arg.slice(2));
|
|
266
281
|
const namedEntry = namedSpecs.get(rawName);
|
|
@@ -305,7 +320,11 @@ class SimpleCLIApp {
|
|
|
305
320
|
}
|
|
306
321
|
positionals.push(arg);
|
|
307
322
|
}
|
|
308
|
-
validateParsedPositionals(
|
|
323
|
+
validateParsedPositionals(
|
|
324
|
+
command2,
|
|
325
|
+
inputDefinition.positionals,
|
|
326
|
+
positionals
|
|
327
|
+
);
|
|
309
328
|
validateRequiredNamedArgs(inputDefinition.named, named);
|
|
310
329
|
return {
|
|
311
330
|
positionals,
|
|
@@ -403,7 +422,7 @@ class SimpleCLIApp {
|
|
|
403
422
|
}
|
|
404
423
|
renderRootHelp() {
|
|
405
424
|
const lines = [`Usage: ${this.name} <command>`, "", "Commands:"];
|
|
406
|
-
for (const entry of this.
|
|
425
|
+
for (const entry of this.getRootHelpEntries()) {
|
|
407
426
|
lines.push(formatListEntry(entry.label, entry.description));
|
|
408
427
|
}
|
|
409
428
|
return lines.join("\n");
|
|
@@ -483,6 +502,41 @@ class SimpleCLIApp {
|
|
|
483
502
|
}
|
|
484
503
|
return entries;
|
|
485
504
|
}
|
|
505
|
+
getRootHelpEntries() {
|
|
506
|
+
return this.getImmediateRouteEntries([]).filter((entry) => {
|
|
507
|
+
const token = entry.label.replace(/\s+<subcommand>$/, "");
|
|
508
|
+
const group2 = this.findGroupByPath([token]);
|
|
509
|
+
if (!group2) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
if (token === EXPERIMENTAL_COMMAND_PREFIX) {
|
|
513
|
+
return this.groupHasExperimentalCommand(group2.path);
|
|
514
|
+
}
|
|
515
|
+
return this.groupHasVisibleNonExperimentalCommand(group2.path);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
groupHasVisibleNonExperimentalCommand(path) {
|
|
519
|
+
for (const routeEntry of this.routeEntries) {
|
|
520
|
+
if (routeEntry.kind !== "command") continue;
|
|
521
|
+
if (!pathStartsWith(routeEntry.path, path)) continue;
|
|
522
|
+
const command2 = this.findCommandByPath(routeEntry.path);
|
|
523
|
+
if (command2 && !command2.experimental) {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
groupHasExperimentalCommand(path) {
|
|
530
|
+
for (const routeEntry of this.routeEntries) {
|
|
531
|
+
if (routeEntry.kind !== "command") continue;
|
|
532
|
+
if (!pathStartsWith(routeEntry.path, path)) continue;
|
|
533
|
+
const command2 = this.findCommandByPath(routeEntry.path);
|
|
534
|
+
if (command2?.experimental) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
486
540
|
findBestMatchingCommand(args) {
|
|
487
541
|
let bestMatch = null;
|
|
488
542
|
for (const command2 of this.resolvedCommands.values()) {
|
|
@@ -506,10 +560,7 @@ class SimpleCLIApp {
|
|
|
506
560
|
function splitNamedArg(arg) {
|
|
507
561
|
const separatorIndex = arg.indexOf("=");
|
|
508
562
|
if (separatorIndex < 0) return [arg, void 0];
|
|
509
|
-
return [
|
|
510
|
-
arg.slice(0, separatorIndex),
|
|
511
|
-
arg.slice(separatorIndex + 1)
|
|
512
|
-
];
|
|
563
|
+
return [arg.slice(0, separatorIndex), arg.slice(separatorIndex + 1)];
|
|
513
564
|
}
|
|
514
565
|
function readNamedArgValue(args, index, rawName, displayName, spec, inlineValue, namedSpecs) {
|
|
515
566
|
if (spec.kind === "flag") {
|
|
@@ -549,22 +600,29 @@ function buildNamedArgLookup(namedDefinition) {
|
|
|
549
600
|
return lookup;
|
|
550
601
|
}
|
|
551
602
|
function validateParsedPositionals(command2, definitions, positionals) {
|
|
552
|
-
const variadicDefinition = definitions.find(
|
|
603
|
+
const variadicDefinition = definitions.find(
|
|
604
|
+
(definition) => definition.variadic
|
|
605
|
+
);
|
|
553
606
|
if (!variadicDefinition && positionals.length > definitions.length) {
|
|
554
607
|
throw new Error(`Unexpected arguments for ${command2.path.join(" ")}.`);
|
|
555
608
|
}
|
|
556
609
|
definitions.forEach((definition, index) => {
|
|
557
610
|
const value = definition.variadic ? positionals.slice(index) : positionals[index];
|
|
558
|
-
if (value !== void 0 && (!Array.isArray(value) || value.length > 0))
|
|
611
|
+
if (value !== void 0 && (!Array.isArray(value) || value.length > 0))
|
|
612
|
+
return;
|
|
559
613
|
if (schemaAcceptsUndefined(definition.schema)) return;
|
|
560
614
|
throw new Error(`Missing required argument <${definition.key}>.`);
|
|
561
615
|
});
|
|
562
616
|
}
|
|
563
617
|
function validateInputDefinition(definition) {
|
|
564
|
-
const variadicIndex = definition.positionals.findIndex(
|
|
618
|
+
const variadicIndex = definition.positionals.findIndex(
|
|
619
|
+
(positional2) => positional2.variadic
|
|
620
|
+
);
|
|
565
621
|
if (variadicIndex < 0) return;
|
|
566
622
|
if (variadicIndex !== definition.positionals.length - 1) {
|
|
567
|
-
throw new Error(
|
|
623
|
+
throw new Error(
|
|
624
|
+
"Variadic positional arguments must be the last positional."
|
|
625
|
+
);
|
|
568
626
|
}
|
|
569
627
|
}
|
|
570
628
|
function validateRequiredNamedArgs(definitions, named) {
|
|
@@ -578,59 +636,105 @@ function validateRequiredNamedArgs(definitions, named) {
|
|
|
578
636
|
throw new Error(`Missing required option --${flagName}.`);
|
|
579
637
|
}
|
|
580
638
|
}
|
|
581
|
-
function resolveRouteTree(routes
|
|
639
|
+
function resolveRouteTree(routes) {
|
|
640
|
+
const collected = collectRouteTree(routes);
|
|
641
|
+
const { groups, routeEntries } = buildResolvedRouteEntries(
|
|
642
|
+
collected.commands,
|
|
643
|
+
collected.groupDescriptions
|
|
644
|
+
);
|
|
645
|
+
return {
|
|
646
|
+
commands: collected.commands,
|
|
647
|
+
groups,
|
|
648
|
+
routeEntries
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function collectRouteTree(routes, parentPath = [], parentMiddlewares = []) {
|
|
582
652
|
const resolved = {
|
|
583
653
|
commands: [],
|
|
584
|
-
|
|
585
|
-
routeEntries: []
|
|
654
|
+
groupDescriptions: /* @__PURE__ */ new Map()
|
|
586
655
|
};
|
|
587
656
|
for (const [token, routeValue] of Object.entries(routes)) {
|
|
588
657
|
if (isGroup(routeValue)) {
|
|
589
658
|
const groupPath = [...parentPath, token];
|
|
590
|
-
resolved.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
description: routeValue.description
|
|
594
|
-
});
|
|
595
|
-
resolved.routeEntries.push({
|
|
596
|
-
kind: "group",
|
|
597
|
-
path: groupPath
|
|
598
|
-
});
|
|
599
|
-
const nested = resolveRouteTree(
|
|
600
|
-
routeValue.routes,
|
|
601
|
-
groupPath,
|
|
602
|
-
[...parentMiddlewares, ...routeValue.middlewares]
|
|
659
|
+
resolved.groupDescriptions.set(
|
|
660
|
+
pathToRouteKey(groupPath),
|
|
661
|
+
routeValue.description
|
|
603
662
|
);
|
|
663
|
+
const nested = collectRouteTree(routeValue.routes, groupPath, [
|
|
664
|
+
...parentMiddlewares,
|
|
665
|
+
...routeValue.middlewares
|
|
666
|
+
]);
|
|
604
667
|
resolved.commands.push(...nested.commands);
|
|
605
|
-
|
|
606
|
-
|
|
668
|
+
for (const [routeKey, description] of nested.groupDescriptions) {
|
|
669
|
+
resolved.groupDescriptions.set(routeKey, description);
|
|
670
|
+
}
|
|
607
671
|
continue;
|
|
608
672
|
}
|
|
609
673
|
const command2 = routeValue.getDefinition();
|
|
610
674
|
if (!command2.handler) {
|
|
611
|
-
throw new Error(
|
|
675
|
+
throw new Error(
|
|
676
|
+
`Command "${[...parentPath, token].join(" ")}" is missing a handler.`
|
|
677
|
+
);
|
|
612
678
|
}
|
|
613
|
-
const
|
|
679
|
+
const rawPath = [...parentPath, token];
|
|
680
|
+
const path = command2.config.experimental ? [EXPERIMENTAL_COMMAND_PREFIX, ...rawPath] : rawPath;
|
|
614
681
|
resolved.commands.push({
|
|
615
682
|
routeKey: pathToRouteKey(path),
|
|
616
683
|
path,
|
|
617
684
|
description: command2.config.description,
|
|
685
|
+
experimental: command2.config.experimental,
|
|
618
686
|
input: command2.input,
|
|
619
|
-
middlewares: mergeInheritedMiddlewares(
|
|
687
|
+
middlewares: mergeInheritedMiddlewares(
|
|
688
|
+
parentMiddlewares,
|
|
689
|
+
command2.middlewares
|
|
690
|
+
),
|
|
620
691
|
handler: command2.handler
|
|
621
692
|
});
|
|
622
|
-
|
|
693
|
+
}
|
|
694
|
+
return resolved;
|
|
695
|
+
}
|
|
696
|
+
function buildResolvedRouteEntries(commands, groupDescriptions) {
|
|
697
|
+
const groups = /* @__PURE__ */ new Map();
|
|
698
|
+
const routeEntries = [];
|
|
699
|
+
for (const command2 of commands) {
|
|
700
|
+
for (let depth = 1; depth < command2.path.length; depth += 1) {
|
|
701
|
+
const path = command2.path.slice(0, depth);
|
|
702
|
+
const routeKey = pathToRouteKey(path);
|
|
703
|
+
if (groups.has(routeKey)) continue;
|
|
704
|
+
groups.set(routeKey, {
|
|
705
|
+
routeKey,
|
|
706
|
+
path,
|
|
707
|
+
description: resolveGroupDescription(path, groupDescriptions)
|
|
708
|
+
});
|
|
709
|
+
routeEntries.push({
|
|
710
|
+
kind: "group",
|
|
711
|
+
path
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
routeEntries.push({
|
|
623
715
|
kind: "command",
|
|
624
|
-
path
|
|
716
|
+
path: command2.path
|
|
625
717
|
});
|
|
626
718
|
}
|
|
627
|
-
return
|
|
719
|
+
return {
|
|
720
|
+
groups: [...groups.values()],
|
|
721
|
+
routeEntries
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function resolveGroupDescription(path, groupDescriptions) {
|
|
725
|
+
if (path.length === 1 && path[0] === EXPERIMENTAL_COMMAND_PREFIX) {
|
|
726
|
+
return EXPERIMENTAL_GROUP_DESCRIPTION;
|
|
727
|
+
}
|
|
728
|
+
const originalPath = path[0] === EXPERIMENTAL_COMMAND_PREFIX ? path.slice(1) : path;
|
|
729
|
+
return groupDescriptions.get(pathToRouteKey(originalPath));
|
|
628
730
|
}
|
|
629
731
|
function mergeInheritedMiddlewares(parentMiddlewares, commandMiddlewares) {
|
|
630
732
|
if (parentMiddlewares.length === 0) {
|
|
631
733
|
return [...commandMiddlewares];
|
|
632
734
|
}
|
|
633
|
-
if (commandMiddlewares.length >= parentMiddlewares.length && parentMiddlewares.every(
|
|
735
|
+
if (commandMiddlewares.length >= parentMiddlewares.length && parentMiddlewares.every(
|
|
736
|
+
(middleware, index) => commandMiddlewares[index] === middleware
|
|
737
|
+
)) {
|
|
634
738
|
return [...commandMiddlewares];
|
|
635
739
|
}
|
|
636
740
|
return [...parentMiddlewares, ...commandMiddlewares];
|
|
@@ -653,10 +757,7 @@ function buildInputNormalizer(definition) {
|
|
|
653
757
|
spec.name ? toCamelCase(spec.name) : "",
|
|
654
758
|
...(spec.aliases ?? []).flatMap((alias) => {
|
|
655
759
|
const normalizedAlias = normalizeNamedArgToken(alias);
|
|
656
|
-
return [
|
|
657
|
-
normalizedAlias,
|
|
658
|
-
toCamelCase(normalizedAlias)
|
|
659
|
-
];
|
|
760
|
+
return [normalizedAlias, toCamelCase(normalizedAlias)];
|
|
660
761
|
}),
|
|
661
762
|
toKebabCase(key),
|
|
662
763
|
key
|
package/dist/cli/router.js
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
import { aiCommands } from "./commands/ai.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { browserCommands } from "./commands/browser.js";
|
|
3
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
4
|
+
import { executionCommands } from "./commands/execution.js";
|
|
4
5
|
import { initCommand } from "./commands/init.js";
|
|
5
6
|
import { logCommands } from "./commands/logs.js";
|
|
6
|
-
import {
|
|
7
|
-
import { createSnapshotCommand } from "./commands/snapshot.js";
|
|
7
|
+
import { snapshotCommand } from "./commands/snapshot.js";
|
|
8
8
|
import { SimpleCLI } from "./framework/simple-cli.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return SimpleCLI.define("libretto", buildCLIRoutes(logger), {
|
|
21
|
-
globalNamed: {
|
|
22
|
-
session: sessionOption()
|
|
23
|
-
}
|
|
24
|
-
});
|
|
9
|
+
const cliRoutes = {
|
|
10
|
+
...browserCommands,
|
|
11
|
+
deploy: deployCommand,
|
|
12
|
+
...executionCommands,
|
|
13
|
+
...logCommands,
|
|
14
|
+
ai: aiCommands,
|
|
15
|
+
init: initCommand,
|
|
16
|
+
snapshot: snapshotCommand
|
|
17
|
+
};
|
|
18
|
+
function createCLIApp() {
|
|
19
|
+
return SimpleCLI.define("libretto", cliRoutes);
|
|
25
20
|
}
|
|
26
21
|
export {
|
|
27
|
-
|
|
22
|
+
cliRoutes,
|
|
28
23
|
createCLIApp
|
|
29
24
|
};
|
|
@@ -4,6 +4,8 @@ import { cwd } from "node:process";
|
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import {
|
|
7
|
+
getWorkflowFromModuleExports,
|
|
8
|
+
getWorkflowsFromModuleExports,
|
|
7
9
|
instrumentContext,
|
|
8
10
|
launchBrowser
|
|
9
11
|
} from "../../index.js";
|
|
@@ -14,9 +16,11 @@ import {
|
|
|
14
16
|
getSessionNetworkLogPath,
|
|
15
17
|
getSessionStatePath
|
|
16
18
|
} from "../core/context.js";
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getPauseSignalPaths,
|
|
21
|
+
removeSignalIfExists
|
|
22
|
+
} from "../core/pause-signals.js";
|
|
18
23
|
import { installSessionTelemetry } from "../core/session-telemetry.js";
|
|
19
|
-
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
20
24
|
const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
|
|
21
25
|
const TSCONFIG_HINT = "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
|
|
22
26
|
function isTsxCompileError(error) {
|
|
@@ -41,7 +45,7 @@ function readSessionStatePid(session) {
|
|
|
41
45
|
const statePath = getSessionStatePath(session);
|
|
42
46
|
if (!existsSync(statePath)) return null;
|
|
43
47
|
try {
|
|
44
|
-
return parseSessionStateContent(readFileSync(statePath, "utf8"), statePath).pid;
|
|
48
|
+
return parseSessionStateContent(readFileSync(statePath, "utf8"), statePath).pid ?? null;
|
|
45
49
|
} catch {
|
|
46
50
|
return null;
|
|
47
51
|
}
|
|
@@ -64,11 +68,6 @@ async function waitForFailureSessionRelease(args) {
|
|
|
64
68
|
);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
|
-
function isLoadedLibrettoWorkflow(value) {
|
|
68
|
-
if (!value || typeof value !== "object") return false;
|
|
69
|
-
const candidate = value;
|
|
70
|
-
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.run === "function" && !!candidate.metadata && typeof candidate.metadata === "object";
|
|
71
|
-
}
|
|
72
71
|
function resolveLocalAuthProfilePath(domain) {
|
|
73
72
|
return getProfilePath(normalizeDomain(domain));
|
|
74
73
|
}
|
|
@@ -90,7 +89,7 @@ function getAbsoluteIntegrationPath(integrationPath) {
|
|
|
90
89
|
}
|
|
91
90
|
return absolutePath;
|
|
92
91
|
}
|
|
93
|
-
async function
|
|
92
|
+
async function loadWorkflowByName(absolutePath, workflowName) {
|
|
94
93
|
let loadedModule;
|
|
95
94
|
try {
|
|
96
95
|
loadedModule = await import(pathToFileURL(absolutePath).href);
|
|
@@ -102,37 +101,17 @@ ${TSCONFIG_HINT}` : "";
|
|
|
102
101
|
`Failed to import integration module at ${absolutePath}: ${message}${compileHint}`
|
|
103
102
|
);
|
|
104
103
|
}
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
const detail = availableExports.length > 0 ? ` Available exports: ${availableExports.join(", ")}` : " The module has no exports.";
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Export "${exportName}" was not found in ${absolutePath}.${detail}`
|
|
111
|
-
);
|
|
104
|
+
const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
|
|
105
|
+
if (workflow) {
|
|
106
|
+
return workflow;
|
|
112
107
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
' import { workflow } from "libretto";',
|
|
121
|
-
"",
|
|
122
|
-
` export const ${exportName} = workflow<InputType, OutputType>(`,
|
|
123
|
-
" {},",
|
|
124
|
-
" async (ctx, input) => {",
|
|
125
|
-
" // ctx.page \u2014 Playwright Page instance",
|
|
126
|
-
" // ctx.logger \u2014 MinimalLogger",
|
|
127
|
-
" // ctx.services \u2014 injected dependencies (generic, default {})",
|
|
128
|
-
" // input \u2014 JSON-serializable input matching InputType",
|
|
129
|
-
" return output; // must match OutputType",
|
|
130
|
-
" },",
|
|
131
|
-
" );"
|
|
132
|
-
].join("\n")
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
return targetExport;
|
|
108
|
+
const availableWorkflows = getWorkflowsFromModuleExports(loadedModule).map(
|
|
109
|
+
(candidate) => candidate.name
|
|
110
|
+
);
|
|
111
|
+
const detail = availableWorkflows.length > 0 ? ` Available workflows: ${availableWorkflows.join(", ")}` : ' No workflows found in this file. Export a workflow() instance from "libretto" directly or via `export const workflows = { ... }`.';
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Workflow "${workflowName}" not found in ${absolutePath}.${detail}`
|
|
114
|
+
);
|
|
136
115
|
}
|
|
137
116
|
async function installHeadedWorkflowVisualization(args) {
|
|
138
117
|
await (args.instrument ?? instrumentContext)(args.context, {
|
|
@@ -143,7 +122,7 @@ async function installHeadedWorkflowVisualization(args) {
|
|
|
143
122
|
async function runIntegrationInternal(args, options) {
|
|
144
123
|
const { logger } = options;
|
|
145
124
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
146
|
-
const workflow = await
|
|
125
|
+
const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
|
|
147
126
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
148
127
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
149
128
|
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -151,11 +130,11 @@ async function runIntegrationInternal(args, options) {
|
|
|
151
130
|
await removeSignalIfExists(signalPaths.failedSignalPath);
|
|
152
131
|
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
153
132
|
console.log(
|
|
154
|
-
`Running
|
|
133
|
+
`Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
|
|
155
134
|
);
|
|
156
135
|
const integrationLogger = logger.withScope("integration-run", {
|
|
157
136
|
integrationPath: absolutePath,
|
|
158
|
-
|
|
137
|
+
workflowName: args.workflowName,
|
|
159
138
|
session: args.session
|
|
160
139
|
});
|
|
161
140
|
const authProfileDomain = args.authProfileDomain;
|
|
@@ -172,7 +151,8 @@ async function runIntegrationInternal(args, options) {
|
|
|
172
151
|
const browserSession = await launchBrowser({
|
|
173
152
|
sessionName: args.session,
|
|
174
153
|
headless: args.headless,
|
|
175
|
-
storageStatePath
|
|
154
|
+
storageStatePath,
|
|
155
|
+
viewport: args.viewport
|
|
176
156
|
});
|
|
177
157
|
if (!args.headless && args.visualize !== false) {
|
|
178
158
|
await installHeadedWorkflowVisualization({
|
|
@@ -194,9 +174,9 @@ async function runIntegrationInternal(args, options) {
|
|
|
194
174
|
}
|
|
195
175
|
});
|
|
196
176
|
const workflowContext = {
|
|
177
|
+
session: args.session,
|
|
197
178
|
logger: integrationLogger,
|
|
198
|
-
page: browserSession.page
|
|
199
|
-
services: {}
|
|
179
|
+
page: browserSession.page
|
|
200
180
|
};
|
|
201
181
|
try {
|
|
202
182
|
try {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
const RunIntegrationWorkerRequestSchema = z.object({
|
|
3
3
|
integrationPath: z.string().min(1),
|
|
4
|
-
|
|
4
|
+
workflowName: z.string().min(1),
|
|
5
5
|
session: z.string().min(1),
|
|
6
6
|
params: z.unknown(),
|
|
7
7
|
headless: z.boolean(),
|
|
8
8
|
visualize: z.boolean().default(true),
|
|
9
|
-
authProfileDomain: z.string().optional()
|
|
9
|
+
authProfileDomain: z.string().optional(),
|
|
10
|
+
viewport: z.object({ width: z.number(), height: z.number() }).optional()
|
|
10
11
|
});
|
|
11
12
|
export {
|
|
12
13
|
RunIntegrationWorkerRequestSchema
|
|
@@ -4,10 +4,7 @@ import {
|
|
|
4
4
|
RunIntegrationWorkerRequestSchema
|
|
5
5
|
} from "./run-integration-worker-protocol.js";
|
|
6
6
|
import { runIntegrationFromFileInWorker } from "./run-integration-runtime.js";
|
|
7
|
-
import {
|
|
8
|
-
ensureLibrettoSetup,
|
|
9
|
-
withSessionLogger
|
|
10
|
-
} from "../core/context.js";
|
|
7
|
+
import { ensureLibrettoSetup, withSessionLogger } from "../core/context.js";
|
|
11
8
|
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
12
9
|
function parseWorkerRequest(argv) {
|
|
13
10
|
const rawPayload = argv[2];
|