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