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.
Files changed (126) hide show
  1. package/README.md +114 -37
  2. package/README.template.md +160 -0
  3. package/dist/cli/cli.js +22 -97
  4. package/dist/cli/commands/browser.js +86 -59
  5. package/dist/cli/commands/deploy.js +148 -0
  6. package/dist/cli/commands/execution.js +218 -96
  7. package/dist/cli/commands/init.js +34 -29
  8. package/dist/cli/commands/logs.js +4 -5
  9. package/dist/cli/commands/shared.js +30 -29
  10. package/dist/cli/commands/snapshot.js +26 -39
  11. package/dist/cli/core/ai-config.js +21 -4
  12. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  13. package/dist/cli/core/browser.js +207 -37
  14. package/dist/cli/core/context.js +4 -1
  15. package/dist/cli/core/deploy-artifact.js +687 -0
  16. package/dist/cli/core/session-telemetry.js +434 -174
  17. package/dist/cli/core/session.js +21 -8
  18. package/dist/cli/core/snapshot-analyzer.js +14 -31
  19. package/dist/cli/core/snapshot-api-config.js +2 -6
  20. package/dist/cli/core/telemetry.js +20 -4
  21. package/dist/cli/framework/simple-cli.js +144 -43
  22. package/dist/cli/router.js +16 -21
  23. package/dist/cli/workers/run-integration-runtime.js +25 -45
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/cli/workers/run-integration-worker.js +1 -4
  26. package/dist/index.d.ts +1 -2
  27. package/dist/index.js +13 -10
  28. package/dist/runtime/download/download.js +5 -1
  29. package/dist/runtime/extract/extract.js +11 -2
  30. package/dist/runtime/network/network.js +8 -1
  31. package/dist/runtime/recovery/agent.js +6 -2
  32. package/dist/runtime/recovery/errors.js +3 -1
  33. package/dist/runtime/recovery/recovery.js +3 -1
  34. package/dist/shared/condense-dom/condense-dom.js +17 -69
  35. package/dist/shared/config/config.d.ts +1 -9
  36. package/dist/shared/config/config.js +0 -18
  37. package/dist/shared/config/index.d.ts +2 -1
  38. package/dist/shared/config/index.js +0 -10
  39. package/dist/shared/debug/pause.js +9 -3
  40. package/dist/shared/dom-semantics.d.ts +8 -0
  41. package/dist/shared/dom-semantics.js +69 -0
  42. package/dist/shared/instrumentation/instrument.js +101 -5
  43. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  44. package/dist/shared/llm/client.js +3 -1
  45. package/dist/shared/logger/index.js +4 -1
  46. package/dist/shared/run/api.js +3 -1
  47. package/dist/shared/run/browser.js +47 -3
  48. package/dist/shared/state/session-state.d.ts +2 -1
  49. package/dist/shared/state/session-state.js +5 -2
  50. package/dist/shared/visualization/ghost-cursor.js +36 -14
  51. package/dist/shared/visualization/highlight.js +9 -6
  52. package/dist/shared/workflow/workflow.d.ts +18 -10
  53. package/dist/shared/workflow/workflow.js +50 -5
  54. package/package.json +14 -6
  55. package/scripts/generate-changelog.ts +132 -0
  56. package/scripts/postinstall.mjs +4 -3
  57. package/scripts/skills-libretto.mjs +2 -88
  58. package/scripts/summarize-evals.mjs +32 -10
  59. package/skills/libretto/SKILL.md +132 -62
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +176 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/deploy.ts +198 -0
  69. package/src/cli/commands/execution.ts +251 -111
  70. package/src/cli/commands/init.ts +37 -33
  71. package/src/cli/commands/logs.ts +7 -7
  72. package/src/cli/commands/shared.ts +36 -37
  73. package/src/cli/commands/snapshot.ts +44 -59
  74. package/src/cli/core/ai-config.ts +24 -4
  75. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  76. package/src/cli/core/browser.ts +260 -49
  77. package/src/cli/core/context.ts +7 -2
  78. package/src/cli/core/deploy-artifact.ts +938 -0
  79. package/src/cli/core/session-telemetry.ts +449 -197
  80. package/src/cli/core/session.ts +21 -7
  81. package/src/cli/core/snapshot-analyzer.ts +26 -46
  82. package/src/cli/core/snapshot-api-config.ts +170 -175
  83. package/src/cli/core/telemetry.ts +39 -4
  84. package/src/cli/framework/simple-cli.ts +281 -98
  85. package/src/cli/router.ts +15 -21
  86. package/src/cli/workers/run-integration-runtime.ts +35 -57
  87. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  88. package/src/cli/workers/run-integration-worker.ts +1 -4
  89. package/src/index.ts +77 -67
  90. package/src/runtime/download/download.ts +62 -58
  91. package/src/runtime/download/index.ts +5 -5
  92. package/src/runtime/extract/extract.ts +71 -61
  93. package/src/runtime/network/index.ts +3 -3
  94. package/src/runtime/network/network.ts +99 -93
  95. package/src/runtime/recovery/agent.ts +217 -212
  96. package/src/runtime/recovery/errors.ts +107 -104
  97. package/src/runtime/recovery/index.ts +3 -3
  98. package/src/runtime/recovery/recovery.ts +38 -35
  99. package/src/shared/condense-dom/condense-dom.ts +27 -82
  100. package/src/shared/config/config.ts +0 -19
  101. package/src/shared/config/index.ts +0 -5
  102. package/src/shared/debug/pause.ts +57 -51
  103. package/src/shared/dom-semantics.ts +68 -0
  104. package/src/shared/instrumentation/errors.ts +64 -62
  105. package/src/shared/instrumentation/index.ts +5 -5
  106. package/src/shared/instrumentation/instrument.ts +339 -209
  107. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  108. package/src/shared/llm/client.ts +181 -174
  109. package/src/shared/llm/types.ts +39 -39
  110. package/src/shared/logger/index.ts +11 -4
  111. package/src/shared/logger/logger.ts +312 -306
  112. package/src/shared/logger/sinks.ts +118 -114
  113. package/src/shared/paths/paths.ts +50 -49
  114. package/src/shared/paths/repo-root.ts +17 -17
  115. package/src/shared/run/api.ts +5 -1
  116. package/src/shared/run/browser.ts +65 -3
  117. package/src/shared/state/index.ts +9 -9
  118. package/src/shared/state/session-state.ts +46 -43
  119. package/src/shared/visualization/ghost-cursor.ts +180 -149
  120. package/src/shared/visualization/highlight.ts +89 -86
  121. package/src/shared/visualization/index.ts +13 -13
  122. package/src/shared/workflow/workflow.ts +107 -30
  123. package/scripts/check-skills-sync.mjs +0 -23
  124. package/scripts/prepare-release.sh +0 -97
  125. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
  126. 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(this.command, args, logger, stdinText);
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(condensedHtmlContent.length),
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(condensedHtmlContent, availableHtmlTokens * 4);
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 { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
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(getSessionActionsLogPath(session), JSON.stringify(record) + "\n");
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 (e.selector) parts.push(e.selector);
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((arg) => isHelpFlag(arg));
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(command2, args.slice(command2.path.length));
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(`Unexpected arguments for ${this.name} ${command2.path.join(" ")}.`);
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(`Unexpected "--" for ${this.name} ${command2.path.join(" ")}.`);
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(command2, inputDefinition.positionals, positionals);
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.getImmediateRouteEntries([])) {
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((definition) => definition.variadic);
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)) return;
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((positional2) => positional2.variadic);
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("Variadic positional arguments must be the last positional.");
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, parentPath = [], parentMiddlewares = []) {
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
- groups: [],
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.groups.push({
591
- routeKey: pathToRouteKey(groupPath),
592
- path: groupPath,
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
- resolved.groups.push(...nested.groups);
606
- resolved.routeEntries.push(...nested.routeEntries);
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(`Command "${[...parentPath, token].join(" ")}" is missing a handler.`);
675
+ throw new Error(
676
+ `Command "${[...parentPath, token].join(" ")}" is missing a handler.`
677
+ );
612
678
  }
613
- const path = [...parentPath, token];
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(parentMiddlewares, command2.middlewares),
687
+ middlewares: mergeInheritedMiddlewares(
688
+ parentMiddlewares,
689
+ command2.middlewares
690
+ ),
620
691
  handler: command2.handler
621
692
  });
622
- resolved.routeEntries.push({
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 resolved;
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((middleware, index) => commandMiddlewares[index] === middleware)) {
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
@@ -1,29 +1,24 @@
1
1
  import { aiCommands } from "./commands/ai.js";
2
- import { createBrowserCommands } from "./commands/browser.js";
3
- import { createExecutionCommands } from "./commands/execution.js";
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 { sessionOption } from "./commands/shared.js";
7
- import { createSnapshotCommand } from "./commands/snapshot.js";
7
+ import { snapshotCommand } from "./commands/snapshot.js";
8
8
  import { SimpleCLI } from "./framework/simple-cli.js";
9
- function buildCLIRoutes(logger) {
10
- return {
11
- ...createBrowserCommands(logger),
12
- ...createExecutionCommands(logger),
13
- ...logCommands,
14
- ai: aiCommands,
15
- init: initCommand,
16
- snapshot: createSnapshotCommand(logger)
17
- };
18
- }
19
- function createCLIApp(logger) {
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
- buildCLIRoutes,
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 { getPauseSignalPaths, removeSignalIfExists } from "../core/pause-signals.js";
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 loadWorkflowExport(absolutePath, exportName) {
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 targetExport = loadedModule[exportName];
106
- if (!targetExport) {
107
- const availableExports = Object.keys(loadedModule);
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
- if (!isLoadedLibrettoWorkflow(targetExport)) {
114
- throw new Error(
115
- [
116
- `Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
117
- "",
118
- 'A workflow must be created using the workflow() function from "libretto":',
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 loadWorkflowExport(absolutePath, args.exportName);
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 integration "${args.exportName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
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
- integrationExport: args.exportName,
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
- exportName: z.string().min(1),
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];