libretto 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +109 -35
  2. package/dist/cli/cli.js +22 -97
  3. package/dist/cli/commands/browser.js +86 -59
  4. package/dist/cli/commands/execution.js +199 -86
  5. package/dist/cli/commands/init.js +34 -29
  6. package/dist/cli/commands/logs.js +4 -5
  7. package/dist/cli/commands/shared.js +30 -29
  8. package/dist/cli/commands/snapshot.js +26 -39
  9. package/dist/cli/core/ai-config.js +21 -4
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +207 -37
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +434 -174
  14. package/dist/cli/core/session.js +21 -8
  15. package/dist/cli/core/snapshot-analyzer.js +14 -31
  16. package/dist/cli/core/snapshot-api-config.js +2 -6
  17. package/dist/cli/core/telemetry.js +20 -4
  18. package/dist/cli/framework/simple-cli.js +45 -25
  19. package/dist/cli/router.js +14 -21
  20. package/dist/cli/workers/run-integration-runtime.js +24 -5
  21. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  22. package/dist/cli/workers/run-integration-worker.js +1 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.js +7 -10
  25. package/dist/runtime/download/download.js +5 -1
  26. package/dist/runtime/extract/extract.js +11 -2
  27. package/dist/runtime/network/network.js +8 -1
  28. package/dist/runtime/recovery/agent.js +6 -2
  29. package/dist/runtime/recovery/errors.js +3 -1
  30. package/dist/runtime/recovery/recovery.js +3 -1
  31. package/dist/shared/condense-dom/condense-dom.js +17 -69
  32. package/dist/shared/config/config.d.ts +1 -9
  33. package/dist/shared/config/config.js +0 -18
  34. package/dist/shared/config/index.d.ts +2 -1
  35. package/dist/shared/config/index.js +0 -10
  36. package/dist/shared/debug/pause.js +9 -3
  37. package/dist/shared/dom-semantics.d.ts +8 -0
  38. package/dist/shared/dom-semantics.js +69 -0
  39. package/dist/shared/instrumentation/instrument.js +101 -5
  40. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  41. package/dist/shared/llm/client.js +3 -1
  42. package/dist/shared/logger/index.js +4 -1
  43. package/dist/shared/run/api.js +3 -1
  44. package/dist/shared/run/browser.js +47 -3
  45. package/dist/shared/state/session-state.d.ts +2 -1
  46. package/dist/shared/state/session-state.js +5 -2
  47. package/dist/shared/visualization/ghost-cursor.js +36 -14
  48. package/dist/shared/visualization/highlight.js +9 -6
  49. package/dist/shared/workflow/workflow.d.ts +4 -5
  50. package/dist/shared/workflow/workflow.js +3 -5
  51. package/package.json +6 -2
  52. package/scripts/check-skills-sync.mjs +25 -0
  53. package/scripts/compare-eval-summary.mjs +47 -0
  54. package/scripts/postinstall.mjs +15 -15
  55. package/scripts/prepare-release.sh +97 -0
  56. package/scripts/skills-libretto.mjs +103 -0
  57. package/scripts/summarize-evals.mjs +135 -0
  58. package/scripts/sync-skills.mjs +12 -0
  59. package/skills/libretto/SKILL.md +132 -54
  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 +210 -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/execution.ts +233 -102
  69. package/src/cli/commands/init.ts +37 -33
  70. package/src/cli/commands/logs.ts +7 -7
  71. package/src/cli/commands/shared.ts +36 -37
  72. package/src/cli/commands/snapshot.ts +44 -59
  73. package/src/cli/core/ai-config.ts +24 -4
  74. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  75. package/src/cli/core/browser.ts +260 -49
  76. package/src/cli/core/context.ts +7 -2
  77. package/src/cli/core/session-telemetry.ts +449 -197
  78. package/src/cli/core/session.ts +21 -7
  79. package/src/cli/core/snapshot-analyzer.ts +26 -46
  80. package/src/cli/core/snapshot-api-config.ts +170 -175
  81. package/src/cli/core/telemetry.ts +39 -4
  82. package/src/cli/framework/simple-cli.ts +144 -77
  83. package/src/cli/router.ts +13 -21
  84. package/src/cli/workers/run-integration-runtime.ts +36 -9
  85. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  86. package/src/cli/workers/run-integration-worker.ts +1 -4
  87. package/src/index.ts +73 -66
  88. package/src/runtime/download/download.ts +62 -58
  89. package/src/runtime/download/index.ts +5 -5
  90. package/src/runtime/extract/extract.ts +71 -61
  91. package/src/runtime/network/index.ts +3 -3
  92. package/src/runtime/network/network.ts +99 -93
  93. package/src/runtime/recovery/agent.ts +217 -212
  94. package/src/runtime/recovery/errors.ts +107 -104
  95. package/src/runtime/recovery/index.ts +3 -3
  96. package/src/runtime/recovery/recovery.ts +38 -35
  97. package/src/shared/condense-dom/condense-dom.ts +27 -82
  98. package/src/shared/config/config.ts +0 -19
  99. package/src/shared/config/index.ts +0 -5
  100. package/src/shared/debug/pause.ts +57 -51
  101. package/src/shared/dom-semantics.ts +68 -0
  102. package/src/shared/instrumentation/errors.ts +64 -62
  103. package/src/shared/instrumentation/index.ts +5 -5
  104. package/src/shared/instrumentation/instrument.ts +339 -209
  105. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  106. package/src/shared/llm/client.ts +181 -174
  107. package/src/shared/llm/types.ts +39 -39
  108. package/src/shared/logger/index.ts +11 -4
  109. package/src/shared/logger/logger.ts +312 -306
  110. package/src/shared/logger/sinks.ts +118 -114
  111. package/src/shared/paths/paths.ts +50 -49
  112. package/src/shared/paths/repo-root.ts +17 -17
  113. package/src/shared/run/api.ts +5 -1
  114. package/src/shared/run/browser.ts +65 -3
  115. package/src/shared/state/index.ts +9 -9
  116. package/src/shared/state/session-state.ts +46 -43
  117. package/src/shared/visualization/ghost-cursor.ts +180 -149
  118. package/src/shared/visualization/highlight.ts +89 -86
  119. package/src/shared/visualization/index.ts +13 -13
  120. package/src/shared/workflow/workflow.ts +19 -25
  121. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  122. 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") {
@@ -498,18 +609,24 @@ async function runIntegrationFromFile(
498
609
  session: args.session,
499
610
  params: args.params,
500
611
  headless: args.headless,
612
+ visualize: args.visualize,
501
613
  authProfileDomain: args.authProfileDomain,
614
+ viewport: args.viewport,
502
615
  } satisfies RunIntegrationWorkerRequest);
503
- const worker = spawn(process.execPath, [
504
- tsxCliPath,
505
- ...(args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : []),
506
- workerEntryPath,
507
- payload,
508
- ], {
509
- detached: true,
510
- stdio: "ignore",
511
- env: process.env,
512
- });
616
+ const worker = spawn(
617
+ process.execPath,
618
+ [
619
+ tsxCliPath,
620
+ ...(args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : []),
621
+ workerEntryPath,
622
+ payload,
623
+ ],
624
+ {
625
+ detached: true,
626
+ stdio: "ignore",
627
+ env: process.env,
628
+ },
629
+ );
513
630
  worker.unref();
514
631
  const outcome = await waitForWorkflowOutcome({
515
632
  session: args.session,
@@ -548,7 +665,9 @@ export const execInput = SimpleCLI.input({
548
665
  ],
549
666
  named: {
550
667
  session: sessionOption(),
551
- visualize: SimpleCLI.flag({ help: "Enable ghost cursor + highlight visualization" }),
668
+ visualize: SimpleCLI.flag({
669
+ help: "Enable ghost cursor + highlight visualization",
670
+ }),
552
671
  page: pageOption(),
553
672
  },
554
673
  }).refine(
@@ -556,26 +675,22 @@ export const execInput = SimpleCLI.input({
556
675
  `Usage: libretto exec <code> [--session <name>] [--visualize]`,
557
676
  );
558
677
 
559
- export function createExecCommand(logger: LoggerApi) {
560
- return SimpleCLI.command({
561
- description: "Execute Playwright TypeScript code",
562
- })
563
- .input(execInput)
564
- .use(resolveSessionMiddleware)
565
- .use(loadSessionStateMiddleware)
566
- .handle(async ({ input, ctx }) => {
567
- await runExec(
568
- input.codeParts.join(" "),
569
- ctx.session,
570
- logger,
571
- input.visualize,
572
- input.page,
573
- );
574
- });
575
- }
678
+ export const execCommand = SimpleCLI.command({
679
+ description: "Execute Playwright TypeScript code",
680
+ })
681
+ .input(execInput)
682
+ .use(withRequiredSession())
683
+ .handle(async ({ input, ctx }) => {
684
+ await runExec(
685
+ input.codeParts.join(" "),
686
+ ctx.session,
687
+ ctx.logger,
688
+ input.visualize,
689
+ input.page,
690
+ );
691
+ });
576
692
 
577
- const runUsage =
578
- `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]`;
693
+ const runUsage = `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
579
694
 
580
695
  export const runInput = SimpleCLI.input({
581
696
  positionals: [
@@ -600,18 +715,31 @@ export const runInput = SimpleCLI.input({
600
715
  }),
601
716
  headed: SimpleCLI.flag({ help: "Run in headed mode" }),
602
717
  headless: SimpleCLI.flag({ help: "Run in headless mode" }),
718
+ noVisualize: SimpleCLI.flag({
719
+ name: "no-visualize",
720
+ help: "Disable ghost cursor + highlight visualization in headed mode",
721
+ }),
603
722
  authProfile: SimpleCLI.option(z.string().optional(), {
604
723
  name: "auth-profile",
605
724
  help: "Domain for local auth profile (e.g. apps.example.com)",
606
725
  }),
726
+ viewport: SimpleCLI.option(z.string().optional(), {
727
+ help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
728
+ }),
607
729
  },
608
730
  })
609
731
  .refine(
610
732
  (input) => Boolean(input.integrationFile && input.integrationExport),
611
733
  runUsage,
612
734
  )
613
- .refine((input) => !(input.params && input.paramsFile), "Pass either --params or --params-file, not both.")
614
- .refine((input) => !(input.headed && input.headless), "Cannot pass both --headed and --headless.");
735
+ .refine(
736
+ (input) => !(input.params && input.paramsFile),
737
+ "Pass either --params or --params-file, not both.",
738
+ )
739
+ .refine(
740
+ (input) => !(input.headed && input.headless),
741
+ "Cannot pass both --headed and --headless.",
742
+ );
615
743
 
616
744
  function resolveRunParams(
617
745
  rawInlineParams: string | undefined,
@@ -634,34 +762,42 @@ function resolveRunParams(
634
762
  return {};
635
763
  }
636
764
 
637
- export function createRunCommand(logger: LoggerApi) {
638
- return SimpleCLI.command({
639
- description: "Run an exported Libretto workflow from a file",
640
- })
641
- .input(runInput)
642
- .use(resolveSessionMiddleware)
643
- .handle(async ({ input, ctx }) => {
644
- await stopExistingFailedRunSession(ctx.session, logger);
645
- assertSessionAvailableForStart(ctx.session, logger);
646
-
647
- const params = resolveRunParams(input.params, input.paramsFile);
648
- const headlessMode = input.headed
649
- ? false
650
- : input.headless
651
- ? true
652
- : undefined;
653
-
654
- await runIntegrationFromFile({
765
+ export const runCommand = SimpleCLI.command({
766
+ description: "Run an exported Libretto workflow from a file",
767
+ })
768
+ .input(runInput)
769
+ .use(withAutoSession())
770
+ .handle(async ({ input, ctx }) => {
771
+ await stopExistingFailedRunSession(ctx.session, ctx.logger);
772
+ assertSessionAvailableForStart(ctx.session, ctx.logger);
773
+
774
+ const params = resolveRunParams(input.params, input.paramsFile);
775
+ const headlessMode = input.headed
776
+ ? false
777
+ : input.headless
778
+ ? true
779
+ : undefined;
780
+ const visualize = !input.noVisualize;
781
+ const viewport = resolveViewport(
782
+ parseViewportArg(input.viewport),
783
+ ctx.logger,
784
+ );
785
+
786
+ await runIntegrationFromFile(
787
+ {
655
788
  integrationPath: input.integrationFile!,
656
789
  exportName: input.integrationExport!,
657
790
  session: ctx.session,
658
791
  params,
659
792
  tsconfigPath: input.tsconfig,
660
793
  headless: headlessMode ?? false,
794
+ visualize,
661
795
  authProfileDomain: input.authProfile,
662
- }, logger);
663
- });
664
- }
796
+ viewport,
797
+ },
798
+ ctx.logger,
799
+ );
800
+ });
665
801
 
666
802
  export const resumeInput = SimpleCLI.input({
667
803
  positionals: [],
@@ -670,22 +806,17 @@ export const resumeInput = SimpleCLI.input({
670
806
  },
671
807
  });
672
808
 
673
- export function createResumeCommand(logger: LoggerApi) {
674
- return SimpleCLI.command({
675
- description: "Resume a paused workflow for the current session",
676
- })
677
- .input(resumeInput)
678
- .use(resolveSessionMiddleware)
679
- .use(loadSessionStateMiddleware)
680
- .handle(async ({ ctx }) => {
681
- await runResume(ctx.session, logger, ctx.sessionState);
682
- });
683
- }
809
+ export const resumeCommand = SimpleCLI.command({
810
+ description: "Resume a paused workflow for the current session",
811
+ })
812
+ .input(resumeInput)
813
+ .use(withRequiredSession())
814
+ .handle(async ({ ctx }) => {
815
+ await runResume(ctx.session, ctx.logger, ctx.sessionState);
816
+ });
684
817
 
685
- export function createExecutionCommands(logger: LoggerApi) {
686
- return {
687
- exec: createExecCommand(logger),
688
- run: createRunCommand(logger),
689
- resume: createResumeCommand(logger),
690
- };
691
- }
818
+ export const executionCommands = {
819
+ exec: execCommand,
820
+ run: runCommand,
821
+ resume: resumeCommand,
822
+ };
@@ -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
 
15
23
  type ProviderChoice = {
16
24
  key: string;
@@ -42,7 +50,8 @@ const PROVIDER_CHOICES: ProviderChoice[] = [
42
50
  key: "4",
43
51
  label: "Google Vertex AI",
44
52
  envVar: "GOOGLE_CLOUD_PROJECT",
45
- envHint: "Requires gcloud auth application-default login and a GCP project ID",
53
+ envHint:
54
+ "Requires gcloud auth application-default login and a GCP project ID",
46
55
  },
47
56
  ];
48
57
 
@@ -57,16 +66,6 @@ function promptUser(
57
66
  });
58
67
  }
59
68
 
60
- function askYesNo(question: string): Promise<boolean> {
61
- const rl = createInterface({ input: process.stdin, output: process.stdout });
62
- return new Promise((resolve) => {
63
- rl.question(`${question} (y/N) `, (answer) => {
64
- rl.close();
65
- resolve(answer.trim().toLowerCase() === "y");
66
- });
67
- });
68
- }
69
-
70
69
  function safeReadAiConfig(): ReturnType<typeof readAiConfig> {
71
70
  try {
72
71
  return readAiConfig();
@@ -101,7 +100,9 @@ function printSnapshotApiStatus(): void {
101
100
 
102
101
  if (selection && hasProviderCredentials(selection.provider)) {
103
102
  console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
104
- console.log(" Snapshot objectives will use the API analyzer by default.");
103
+ console.log(
104
+ " Snapshot objectives will use the API analyzer by default.",
105
+ );
105
106
  console.log(" No further action required.");
106
107
  return;
107
108
  }
@@ -117,7 +118,9 @@ function printSnapshotApiStatus(): void {
117
118
  console.log(
118
119
  " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
119
120
  );
120
- console.log(" Run `npx libretto init` interactively to set up credentials.");
121
+ console.log(
122
+ " Run `npx libretto init` interactively to set up credentials.",
123
+ );
121
124
  }
122
125
 
123
126
  async function runInteractiveApiSetup(): Promise<void> {
@@ -132,7 +135,9 @@ async function runInteractiveApiSetup(): Promise<void> {
132
135
 
133
136
  if (selection && hasProviderCredentials(selection.provider)) {
134
137
  console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
135
- console.log(" Snapshot objectives will use the API analyzer by default.");
138
+ console.log(
139
+ " Snapshot objectives will use the API analyzer by default.",
140
+ );
136
141
  return;
137
142
  }
138
143
 
@@ -144,7 +149,9 @@ async function runInteractiveApiSetup(): Promise<void> {
144
149
  });
145
150
 
146
151
  try {
147
- console.log(" Which API provider would you like to use for snapshot analysis?\n");
152
+ console.log(
153
+ " Which API provider would you like to use for snapshot analysis?\n",
154
+ );
148
155
  for (const choice of PROVIDER_CHOICES) {
149
156
  console.log(` ${choice.key}) ${choice.label}`);
150
157
  }
@@ -153,7 +160,9 @@ async function runInteractiveApiSetup(): Promise<void> {
153
160
  const answer = await promptUser(rl, " Choice: ");
154
161
 
155
162
  if (answer.toLowerCase() === "s" || !answer) {
156
- console.log("\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.");
163
+ console.log(
164
+ "\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.",
165
+ );
157
166
  console.log(" Or add credentials directly to your .env file:");
158
167
  console.log(" OPENAI_API_KEY=...");
159
168
  console.log(" ANTHROPIC_API_KEY=...");
@@ -251,26 +260,17 @@ function detectAgentDirs(root: string): string[] {
251
260
  return dirs;
252
261
  }
253
262
 
254
- async function copySkills(): Promise<void> {
263
+ function copySkills(): void {
255
264
  const agentDirs = detectAgentDirs(REPO_ROOT);
256
265
 
257
266
  if (agentDirs.length === 0) {
258
- console.log("\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.");
267
+ console.log(
268
+ "\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.",
269
+ );
259
270
  return;
260
271
  }
261
272
 
262
273
  const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
263
- const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
264
- // Say "Overwrite" if skills already exist in ANY target dir — skills must
265
- // be identical across coding agents, so we always copy to all of them.
266
- const existing = destinations.filter((d) => existsSync(d));
267
- const verb = existing.length > 0 ? "Overwrite" : "Install";
268
-
269
- const proceed = await askYesNo(`\n${verb} libretto skills in ${dirNames}?`);
270
- if (!proceed) {
271
- console.log(" Skipping skill copy.");
272
- return;
273
- }
274
274
 
275
275
  let sourceDir: string;
276
276
  try {
@@ -289,7 +289,9 @@ async function copySkills(): Promise<void> {
289
289
  }
290
290
  cpSync(sourceDir, skillDest, { recursive: true });
291
291
  const fileCount = readdirSync(skillDest).length;
292
- console.log(` ✓ Copied ${fileCount} skill files to ${name}/skills/libretto/`);
292
+ console.log(
293
+ ` ✓ Copied ${fileCount} skill files to ${name}/skills/libretto/`,
294
+ );
293
295
  }
294
296
  }
295
297
 
@@ -316,10 +318,12 @@ export const initCommand = SimpleCLI.command({
316
318
  console.log("\nSkipping browser installation (--skip-browsers)");
317
319
  }
318
320
 
321
+ copySkills();
322
+
319
323
  if (process.stdin.isTTY) {
320
- await copySkills();
321
324
  await runInteractiveApiSetup();
322
325
  } else {
326
+ loadSnapshotEnv();
323
327
  printSnapshotApiStatus();
324
328
  }
325
329