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
@@ -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: code.length,
130
- codePreview: code.slice(0, 200),
214
+ codeLength: cleanedCode.length,
215
+ codePreview: cleanedCode.slice(0, 200),
131
216
  visualize,
132
217
  pageId,
133
218
  });
134
- const { browser, context, page, pageId: resolvedPageId } = await connect(
135
- session,
136
- logger,
137
- 10000,
138
- {
139
- pageId,
140
- requireSinglePage: true,
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: code.slice(0, 200),
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: ${code.slice(0, 100)}...)`,
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: code.slice(0, 200),
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: { last?: number; filter?: string; method?: string; pageId?: string } = {},
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(code, helperNames);
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: code.slice(0, 200),
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(signalPaths.outputSignalPath, outputOffset);
468
+ outputOffset = streamOutputSince(
469
+ signalPaths.outputSignalPath,
470
+ outputOffset,
471
+ );
375
472
 
376
473
  if (existsSync(signalPaths.failedSignalPath)) {
377
- outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
378
- const failureDetails = await waitForFailureDetails(signalPaths.failedSignalPath);
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(signalPaths.outputSignalPath, outputOffset);
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(signalPaths.outputSignalPath, outputOffset);
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(signalPaths.outputSignalPath, outputOffset);
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") {
@@ -494,23 +605,28 @@ async function runIntegrationFromFile(
494
605
  );
495
606
  const payload = JSON.stringify({
496
607
  integrationPath: args.integrationPath,
497
- exportName: args.exportName,
608
+ workflowName: args.workflowName,
498
609
  session: args.session,
499
610
  params: args.params,
500
611
  headless: args.headless,
501
612
  visualize: args.visualize,
502
613
  authProfileDomain: args.authProfileDomain,
614
+ viewport: args.viewport,
503
615
  } satisfies RunIntegrationWorkerRequest);
504
- const worker = spawn(process.execPath, [
505
- tsxCliPath,
506
- ...(args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : []),
507
- workerEntryPath,
508
- payload,
509
- ], {
510
- detached: true,
511
- stdio: "ignore",
512
- env: process.env,
513
- });
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
+ );
514
630
  worker.unref();
515
631
  const outcome = await waitForWorkflowOutcome({
516
632
  session: args.session,
@@ -540,51 +656,65 @@ async function runIntegrationFromFile(
540
656
  console.log("Integration completed.");
541
657
  }
542
658
 
659
+ function readStdinSync(): string | null {
660
+ if (process.stdin.isTTY === true) return null;
661
+ try {
662
+ const content = readFileSync(0, "utf8");
663
+ return content.trim().length > 0 ? content : null;
664
+ } catch {
665
+ return null;
666
+ }
667
+ }
668
+
543
669
  export const execInput = SimpleCLI.input({
544
670
  positionals: [
545
- SimpleCLI.positional("codeParts", z.array(z.string()).default([]), {
671
+ SimpleCLI.positional("code", z.string().optional(), {
546
672
  help: "Playwright TypeScript code to execute",
547
- variadic: true,
548
673
  }),
549
674
  ],
550
675
  named: {
551
676
  session: sessionOption(),
552
- visualize: SimpleCLI.flag({ help: "Enable ghost cursor + highlight visualization" }),
677
+ visualize: SimpleCLI.flag({
678
+ help: "Enable ghost cursor + highlight visualization",
679
+ }),
553
680
  page: pageOption(),
554
681
  },
555
682
  }).refine(
556
- (input) => input.codeParts.length > 0,
557
- `Usage: libretto exec <code> [--session <name>] [--visualize]`,
683
+ (input) => input.code !== undefined,
684
+ `Usage: libretto exec <code|-> [--session <name>] [--visualize]\n echo '<code>' | libretto exec - [--session <name>] [--visualize]`,
558
685
  );
559
686
 
560
- export function createExecCommand(logger: LoggerApi) {
561
- return SimpleCLI.command({
562
- description: "Execute Playwright TypeScript code",
563
- })
564
- .input(execInput)
565
- .use(resolveSessionMiddleware)
566
- .use(loadSessionStateMiddleware)
567
- .handle(async ({ input, ctx }) => {
568
- await runExec(
569
- input.codeParts.join(" "),
570
- ctx.session,
571
- logger,
572
- input.visualize,
573
- input.page,
687
+ export const execCommand = SimpleCLI.command({
688
+ description: "Execute Playwright TypeScript code",
689
+ })
690
+ .input(execInput)
691
+ .use(withRequiredSession())
692
+ .handle(async ({ input, ctx }) => {
693
+ const code = input.code!;
694
+ const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
695
+ if (codeFromArgsOrStdin === null) {
696
+ throw new Error(
697
+ "Missing stdin input for `exec -`. Pipe Playwright code into stdin.",
574
698
  );
575
- });
576
- }
699
+ }
700
+ await runExec(
701
+ codeFromArgsOrStdin,
702
+ ctx.session,
703
+ ctx.logger,
704
+ input.visualize,
705
+ input.page,
706
+ );
707
+ });
577
708
 
578
- const runUsage =
579
- `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize]`;
709
+ const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
580
710
 
581
711
  export const runInput = SimpleCLI.input({
582
712
  positionals: [
583
713
  SimpleCLI.positional("integrationFile", z.string().optional(), {
584
714
  help: "Path to the integration file",
585
715
  }),
586
- SimpleCLI.positional("integrationExport", z.string().optional(), {
587
- help: "Named workflow export to run",
716
+ SimpleCLI.positional("workflowName", z.string().optional(), {
717
+ help: "Workflow name to run (from workflow(name, handler))",
588
718
  }),
589
719
  ],
590
720
  named: {
@@ -609,14 +739,23 @@ export const runInput = SimpleCLI.input({
609
739
  name: "auth-profile",
610
740
  help: "Domain for local auth profile (e.g. apps.example.com)",
611
741
  }),
742
+ viewport: SimpleCLI.option(z.string().optional(), {
743
+ help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
744
+ }),
612
745
  },
613
746
  })
614
747
  .refine(
615
- (input) => Boolean(input.integrationFile && input.integrationExport),
748
+ (input) => Boolean(input.integrationFile && input.workflowName),
616
749
  runUsage,
617
750
  )
618
- .refine((input) => !(input.params && input.paramsFile), "Pass either --params or --params-file, not both.")
619
- .refine((input) => !(input.headed && input.headless), "Cannot pass both --headed and --headless.");
751
+ .refine(
752
+ (input) => !(input.params && input.paramsFile),
753
+ "Pass either --params or --params-file, not both.",
754
+ )
755
+ .refine(
756
+ (input) => !(input.headed && input.headless),
757
+ "Cannot pass both --headed and --headless.",
758
+ );
620
759
 
621
760
  function resolveRunParams(
622
761
  rawInlineParams: string | undefined,
@@ -639,36 +778,42 @@ function resolveRunParams(
639
778
  return {};
640
779
  }
641
780
 
642
- export function createRunCommand(logger: LoggerApi) {
643
- return SimpleCLI.command({
644
- description: "Run an exported Libretto workflow from a file",
645
- })
646
- .input(runInput)
647
- .use(resolveSessionMiddleware)
648
- .handle(async ({ input, ctx }) => {
649
- await stopExistingFailedRunSession(ctx.session, logger);
650
- assertSessionAvailableForStart(ctx.session, logger);
651
-
652
- const params = resolveRunParams(input.params, input.paramsFile);
653
- const headlessMode = input.headed
654
- ? false
655
- : input.headless
656
- ? true
657
- : undefined;
658
- const visualize = !input.noVisualize;
659
-
660
- await runIntegrationFromFile({
781
+ export const runCommand = SimpleCLI.command({
782
+ description: "Run an exported Libretto workflow from a file",
783
+ })
784
+ .input(runInput)
785
+ .use(withAutoSession())
786
+ .handle(async ({ input, ctx }) => {
787
+ await stopExistingFailedRunSession(ctx.session, ctx.logger);
788
+ assertSessionAvailableForStart(ctx.session, ctx.logger);
789
+
790
+ const params = resolveRunParams(input.params, input.paramsFile);
791
+ const headlessMode = input.headed
792
+ ? false
793
+ : input.headless
794
+ ? true
795
+ : undefined;
796
+ const visualize = !input.noVisualize;
797
+ const viewport = resolveViewport(
798
+ parseViewportArg(input.viewport),
799
+ ctx.logger,
800
+ );
801
+
802
+ await runIntegrationFromFile(
803
+ {
661
804
  integrationPath: input.integrationFile!,
662
- exportName: input.integrationExport!,
805
+ workflowName: input.workflowName!,
663
806
  session: ctx.session,
664
807
  params,
665
808
  tsconfigPath: input.tsconfig,
666
809
  headless: headlessMode ?? false,
667
810
  visualize,
668
811
  authProfileDomain: input.authProfile,
669
- }, logger);
670
- });
671
- }
812
+ viewport,
813
+ },
814
+ ctx.logger,
815
+ );
816
+ });
672
817
 
673
818
  export const resumeInput = SimpleCLI.input({
674
819
  positionals: [],
@@ -677,22 +822,17 @@ export const resumeInput = SimpleCLI.input({
677
822
  },
678
823
  });
679
824
 
680
- export function createResumeCommand(logger: LoggerApi) {
681
- return SimpleCLI.command({
682
- description: "Resume a paused workflow for the current session",
683
- })
684
- .input(resumeInput)
685
- .use(resolveSessionMiddleware)
686
- .use(loadSessionStateMiddleware)
687
- .handle(async ({ ctx }) => {
688
- await runResume(ctx.session, logger, ctx.sessionState);
689
- });
690
- }
825
+ export const resumeCommand = SimpleCLI.command({
826
+ description: "Resume a paused workflow for the current session",
827
+ })
828
+ .input(resumeInput)
829
+ .use(withRequiredSession())
830
+ .handle(async ({ ctx }) => {
831
+ await runResume(ctx.session, ctx.logger, ctx.sessionState);
832
+ });
691
833
 
692
- export function createExecutionCommands(logger: LoggerApi) {
693
- return {
694
- exec: createExecCommand(logger),
695
- run: createRunCommand(logger),
696
- resume: createResumeCommand(logger),
697
- };
698
- }
834
+ export const executionCommands = {
835
+ exec: execCommand,
836
+ run: runCommand,
837
+ resume: resumeCommand,
838
+ };