libretto 0.3.0 → 0.3.1

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 (162) hide show
  1. package/README.md +41 -125
  2. package/dist/cli/cli.js +298 -0
  3. package/dist/cli/commands/ai.js +21 -0
  4. package/dist/cli/commands/browser.js +82 -0
  5. package/dist/cli/commands/execution.js +490 -0
  6. package/dist/cli/commands/init.js +166 -0
  7. package/dist/cli/commands/logs.js +93 -0
  8. package/dist/cli/commands/snapshot.js +106 -0
  9. package/dist/cli/core/ai-config.js +149 -0
  10. package/dist/cli/core/browser.js +652 -0
  11. package/dist/cli/core/context.js +117 -0
  12. package/dist/cli/core/pause-signals.js +29 -0
  13. package/dist/cli/core/session-telemetry.js +491 -0
  14. package/dist/cli/core/session.js +183 -0
  15. package/dist/cli/core/snapshot-analyzer.js +492 -0
  16. package/dist/cli/core/telemetry.js +362 -0
  17. package/dist/cli/index.js +13 -0
  18. package/dist/cli/workers/run-integration-runtime.js +234 -0
  19. package/dist/cli/workers/run-integration-worker-protocol.js +12 -0
  20. package/dist/cli/workers/run-integration-worker.js +67 -0
  21. package/dist/index.cjs +144 -0
  22. package/dist/index.d.cts +21 -0
  23. package/dist/index.d.ts +21 -0
  24. package/dist/index.js +114 -0
  25. package/dist/runtime/download/download.cjs +70 -0
  26. package/dist/runtime/download/download.d.cts +35 -0
  27. package/dist/runtime/download/download.d.ts +35 -0
  28. package/dist/runtime/download/download.js +45 -0
  29. package/dist/runtime/download/index.cjs +30 -0
  30. package/dist/runtime/download/index.d.cts +3 -0
  31. package/dist/runtime/download/index.d.ts +3 -0
  32. package/dist/runtime/download/index.js +8 -0
  33. package/dist/runtime/extract/extract.cjs +88 -0
  34. package/dist/runtime/extract/extract.d.cts +23 -0
  35. package/dist/runtime/extract/extract.d.ts +23 -0
  36. package/dist/runtime/extract/extract.js +64 -0
  37. package/dist/runtime/extract/index.cjs +28 -0
  38. package/dist/runtime/extract/index.d.cts +5 -0
  39. package/dist/runtime/extract/index.d.ts +5 -0
  40. package/dist/runtime/extract/index.js +4 -0
  41. package/dist/runtime/network/index.cjs +28 -0
  42. package/dist/runtime/network/index.d.cts +4 -0
  43. package/dist/runtime/network/index.d.ts +4 -0
  44. package/dist/runtime/network/index.js +6 -0
  45. package/dist/runtime/network/network.cjs +91 -0
  46. package/dist/runtime/network/network.d.cts +28 -0
  47. package/dist/runtime/network/network.d.ts +28 -0
  48. package/dist/runtime/network/network.js +67 -0
  49. package/dist/runtime/recovery/agent.cjs +223 -0
  50. package/dist/runtime/recovery/agent.d.cts +13 -0
  51. package/dist/runtime/recovery/agent.d.ts +13 -0
  52. package/dist/runtime/recovery/agent.js +199 -0
  53. package/dist/runtime/recovery/errors.cjs +124 -0
  54. package/dist/runtime/recovery/errors.d.cts +31 -0
  55. package/dist/runtime/recovery/errors.d.ts +31 -0
  56. package/dist/runtime/recovery/errors.js +100 -0
  57. package/dist/runtime/recovery/index.cjs +34 -0
  58. package/dist/runtime/recovery/index.d.cts +7 -0
  59. package/dist/runtime/recovery/index.d.ts +7 -0
  60. package/dist/runtime/recovery/index.js +10 -0
  61. package/dist/runtime/recovery/recovery.cjs +55 -0
  62. package/dist/runtime/recovery/recovery.d.cts +12 -0
  63. package/dist/runtime/recovery/recovery.d.ts +12 -0
  64. package/dist/runtime/recovery/recovery.js +31 -0
  65. package/dist/shared/config/config.cjs +44 -0
  66. package/dist/shared/config/config.d.cts +10 -0
  67. package/dist/shared/config/config.d.ts +10 -0
  68. package/dist/shared/config/config.js +18 -0
  69. package/dist/shared/config/index.cjs +32 -0
  70. package/dist/shared/config/index.d.cts +1 -0
  71. package/dist/shared/config/index.d.ts +1 -0
  72. package/dist/shared/config/index.js +10 -0
  73. package/dist/shared/debug/index.cjs +30 -0
  74. package/dist/shared/debug/index.d.cts +1 -0
  75. package/dist/shared/debug/index.d.ts +1 -0
  76. package/dist/shared/debug/index.js +5 -0
  77. package/dist/shared/debug/pause.cjs +90 -0
  78. package/dist/shared/debug/pause.d.cts +16 -0
  79. package/dist/shared/debug/pause.d.ts +16 -0
  80. package/dist/shared/debug/pause.js +55 -0
  81. package/dist/shared/instrumentation/errors.cjs +81 -0
  82. package/dist/shared/instrumentation/errors.d.cts +12 -0
  83. package/dist/shared/instrumentation/errors.d.ts +12 -0
  84. package/dist/shared/instrumentation/errors.js +57 -0
  85. package/dist/shared/instrumentation/index.cjs +35 -0
  86. package/dist/shared/instrumentation/index.d.cts +6 -0
  87. package/dist/shared/instrumentation/index.d.ts +6 -0
  88. package/dist/shared/instrumentation/index.js +12 -0
  89. package/dist/shared/instrumentation/instrument.cjs +206 -0
  90. package/dist/shared/instrumentation/instrument.d.cts +32 -0
  91. package/dist/shared/instrumentation/instrument.d.ts +32 -0
  92. package/dist/shared/instrumentation/instrument.js +190 -0
  93. package/dist/shared/llm/ai-sdk-adapter.cjs +67 -0
  94. package/dist/shared/llm/ai-sdk-adapter.d.cts +22 -0
  95. package/dist/shared/llm/ai-sdk-adapter.d.ts +22 -0
  96. package/dist/shared/llm/ai-sdk-adapter.js +43 -0
  97. package/dist/shared/llm/client.cjs +139 -0
  98. package/dist/shared/llm/client.d.cts +6 -0
  99. package/dist/shared/llm/client.d.ts +6 -0
  100. package/dist/shared/llm/client.js +115 -0
  101. package/dist/shared/llm/index.cjs +31 -0
  102. package/dist/shared/llm/index.d.cts +5 -0
  103. package/dist/shared/llm/index.d.ts +5 -0
  104. package/dist/shared/llm/index.js +6 -0
  105. package/dist/shared/llm/types.cjs +16 -0
  106. package/dist/shared/llm/types.d.cts +66 -0
  107. package/dist/shared/llm/types.d.ts +66 -0
  108. package/dist/shared/llm/types.js +0 -0
  109. package/dist/shared/logger/index.cjs +37 -0
  110. package/dist/shared/logger/index.d.cts +2 -0
  111. package/dist/shared/logger/index.d.ts +2 -0
  112. package/dist/shared/logger/index.js +13 -0
  113. package/dist/shared/logger/logger.cjs +232 -0
  114. package/dist/shared/logger/logger.d.cts +86 -0
  115. package/dist/shared/logger/logger.d.ts +86 -0
  116. package/dist/shared/logger/logger.js +207 -0
  117. package/dist/shared/logger/sinks.cjs +160 -0
  118. package/dist/shared/logger/sinks.d.cts +9 -0
  119. package/dist/shared/logger/sinks.d.ts +9 -0
  120. package/dist/shared/logger/sinks.js +124 -0
  121. package/dist/shared/paths/paths.cjs +104 -0
  122. package/dist/shared/paths/paths.d.cts +10 -0
  123. package/dist/shared/paths/paths.d.ts +10 -0
  124. package/dist/shared/paths/paths.js +73 -0
  125. package/dist/shared/run/api.cjs +28 -0
  126. package/dist/shared/run/api.d.cts +2 -0
  127. package/dist/shared/run/api.d.ts +2 -0
  128. package/dist/shared/run/api.js +4 -0
  129. package/dist/shared/run/browser.cjs +98 -0
  130. package/dist/shared/run/browser.d.cts +22 -0
  131. package/dist/shared/run/browser.d.ts +22 -0
  132. package/dist/shared/run/browser.js +74 -0
  133. package/dist/shared/state/index.cjs +38 -0
  134. package/dist/shared/state/index.d.cts +2 -0
  135. package/dist/shared/state/index.d.ts +2 -0
  136. package/dist/shared/state/index.js +16 -0
  137. package/dist/shared/state/session-state.cjs +85 -0
  138. package/dist/shared/state/session-state.d.cts +34 -0
  139. package/dist/shared/state/session-state.d.ts +34 -0
  140. package/dist/shared/state/session-state.js +56 -0
  141. package/dist/shared/visualization/ghost-cursor.cjs +174 -0
  142. package/dist/shared/visualization/ghost-cursor.d.cts +37 -0
  143. package/dist/shared/visualization/ghost-cursor.d.ts +37 -0
  144. package/dist/shared/visualization/ghost-cursor.js +145 -0
  145. package/dist/shared/visualization/highlight.cjs +134 -0
  146. package/dist/shared/visualization/highlight.d.cts +22 -0
  147. package/dist/shared/visualization/highlight.d.ts +22 -0
  148. package/dist/shared/visualization/highlight.js +108 -0
  149. package/dist/shared/visualization/index.cjs +45 -0
  150. package/dist/shared/visualization/index.d.cts +3 -0
  151. package/dist/shared/visualization/index.d.ts +3 -0
  152. package/dist/shared/visualization/index.js +24 -0
  153. package/dist/shared/workflow/workflow.cjs +47 -0
  154. package/dist/shared/workflow/workflow.d.cts +21 -0
  155. package/dist/shared/workflow/workflow.d.ts +21 -0
  156. package/dist/shared/workflow/workflow.js +21 -0
  157. package/package.json +11 -70
  158. package/bin/libretto.mjs +0 -18
  159. package/scripts/postinstall.mjs +0 -48
  160. /package/{skill → .agents/skills/libretto}/SKILL.md +0 -0
  161. /package/{skill → .agents/skills/libretto}/code-generation-rules.md +0 -0
  162. /package/{skill → .agents/skills/libretto}/integration-approach-selection.md +0 -0
@@ -0,0 +1,490 @@
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import * as moduleBuiltin from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+ import { installInstrumentation } from "../../shared/instrumentation/index.js";
6
+ import {
7
+ connect,
8
+ disconnectBrowser
9
+ } from "../core/browser.js";
10
+ import { getPauseSignalPaths } from "../core/pause-signals.js";
11
+ import {
12
+ assertSessionAvailableForStart,
13
+ clearSessionState,
14
+ readSessionState,
15
+ readSessionStateOrThrow,
16
+ setSessionStatus
17
+ } from "../core/session.js";
18
+ import {
19
+ readActionLog,
20
+ readNetworkLog,
21
+ wrapPageForActionLogging
22
+ } from "../core/telemetry.js";
23
+ const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
24
+ const require2 = moduleBuiltin.createRequire(import.meta.url);
25
+ const tsxCliPath = require2.resolve("tsx/cli");
26
+ function withSuppressedStripTypeScriptWarning(action) {
27
+ const mutableProcess = process;
28
+ const originalEmitWarning = mutableProcess.emitWarning;
29
+ mutableProcess.emitWarning = (...args) => {
30
+ const warning = args[0];
31
+ const typeOrOptions = args[1];
32
+ const warningMessage = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
33
+ const warningType = typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type ?? "" : "";
34
+ if (warningType === "ExperimentalWarning" && warningMessage.includes("stripTypeScriptTypes")) {
35
+ return;
36
+ }
37
+ originalEmitWarning(...args);
38
+ };
39
+ try {
40
+ return action();
41
+ } finally {
42
+ mutableProcess.emitWarning = originalEmitWarning;
43
+ }
44
+ }
45
+ function compileTypeScriptExecFunction(code, helperNames) {
46
+ if (!stripTypeScriptTypes) return null;
47
+ const wrappedSource = `(async function __librettoExec(${helperNames.join(", ")}) {
48
+ ${code}
49
+ })`;
50
+ const jsSource = withSuppressedStripTypeScriptWarning(
51
+ () => stripTypeScriptTypes(wrappedSource, { mode: "strip" })
52
+ );
53
+ const createFunction = new Function(
54
+ `return ${jsSource}`
55
+ );
56
+ return createFunction();
57
+ }
58
+ function compileExecFunction(code, helperNames) {
59
+ const typeStripped = compileTypeScriptExecFunction(code, helperNames);
60
+ if (typeStripped) return typeStripped;
61
+ const AsyncFunction = Object.getPrototypeOf(async function() {
62
+ }).constructor;
63
+ return new AsyncFunction(...helperNames, code);
64
+ }
65
+ async function runExec(code, session, logger, visualize = false, pageId) {
66
+ readSessionStateOrThrow(session);
67
+ logger.info("exec-start", {
68
+ session,
69
+ codeLength: code.length,
70
+ codePreview: code.slice(0, 200),
71
+ visualize,
72
+ pageId
73
+ });
74
+ const { browser, context, page, pageId: resolvedPageId } = await connect(
75
+ session,
76
+ logger,
77
+ 1e4,
78
+ {
79
+ pageId,
80
+ requireSinglePage: true
81
+ }
82
+ );
83
+ const STALL_THRESHOLD_MS = 6e4;
84
+ let lastActivityTs = Date.now();
85
+ const onActivity = () => {
86
+ lastActivityTs = Date.now();
87
+ };
88
+ const stallInterval = setInterval(() => {
89
+ const silenceMs = Date.now() - lastActivityTs;
90
+ if (silenceMs >= STALL_THRESHOLD_MS) {
91
+ logger.warn("exec-stall-warning", {
92
+ session,
93
+ silenceMs,
94
+ codePreview: code.slice(0, 200)
95
+ });
96
+ console.warn(
97
+ `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 exec may be hung (code: ${code.slice(0, 100)}...)`
98
+ );
99
+ }
100
+ }, STALL_THRESHOLD_MS);
101
+ const execStartTs = Date.now();
102
+ const sigintHandler = () => {
103
+ logger.info("exec-interrupted", {
104
+ session,
105
+ duration: Date.now() - execStartTs,
106
+ codePreview: code.slice(0, 200)
107
+ });
108
+ };
109
+ process.on("SIGINT", sigintHandler);
110
+ wrapPageForActionLogging(page, session, resolvedPageId, onActivity);
111
+ if (visualize) {
112
+ await installInstrumentation(page, { visualize: true, logger });
113
+ }
114
+ try {
115
+ const execState = {};
116
+ const networkLog = (opts = {}) => {
117
+ return readNetworkLog(session, opts);
118
+ };
119
+ const actionLog = (opts = {}) => {
120
+ return readActionLog(session, opts);
121
+ };
122
+ const helpers = {
123
+ page,
124
+ context,
125
+ state: execState,
126
+ browser,
127
+ networkLog,
128
+ actionLog,
129
+ console,
130
+ setTimeout,
131
+ setInterval,
132
+ clearTimeout,
133
+ clearInterval,
134
+ fetch,
135
+ URL,
136
+ Buffer
137
+ };
138
+ const helperNames = Object.keys(helpers);
139
+ const fn = compileExecFunction(code, helperNames);
140
+ const result = await fn(...Object.values(helpers));
141
+ logger.info("exec-success", { session, hasResult: result !== void 0 });
142
+ if (result !== void 0) {
143
+ console.log(
144
+ typeof result === "string" ? result : JSON.stringify(result, null, 2)
145
+ );
146
+ }
147
+ } catch (err) {
148
+ logger.error("exec-error", {
149
+ error: err,
150
+ session,
151
+ codePreview: code.slice(0, 200)
152
+ });
153
+ throw err;
154
+ } finally {
155
+ clearInterval(stallInterval);
156
+ process.removeListener("SIGINT", sigintHandler);
157
+ disconnectBrowser(browser, logger, session);
158
+ }
159
+ }
160
+ function parseJsonArg(label, raw) {
161
+ try {
162
+ return JSON.parse(raw);
163
+ } catch (error) {
164
+ throw new Error(
165
+ `Invalid JSON in ${label}: ${error instanceof Error ? error.message : String(error)}`
166
+ );
167
+ }
168
+ }
169
+ function isProcessRunning(pid) {
170
+ try {
171
+ process.kill(pid, 0);
172
+ return true;
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+ async function stopExistingFailedRunSession(session, logger) {
178
+ const existingState = readSessionState(session, logger);
179
+ if (!existingState || existingState.status !== "failed") {
180
+ return;
181
+ }
182
+ logger.info("run-release-existing-failed-session", {
183
+ session,
184
+ pid: existingState.pid,
185
+ port: existingState.port
186
+ });
187
+ clearSessionState(session, logger);
188
+ const stopDeadline = Date.now() + 3e3;
189
+ while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
190
+ await new Promise((resolveWait) => setTimeout(resolveWait, 100));
191
+ }
192
+ if (isProcessRunning(existingState.pid)) {
193
+ logger.warn("run-release-existing-failed-session-timeout", {
194
+ session,
195
+ pid: existingState.pid
196
+ });
197
+ console.warn(
198
+ `Existing failed workflow process for session "${session}" (pid ${existingState.pid}) is still shutting down; continuing.`
199
+ );
200
+ return;
201
+ }
202
+ console.log(
203
+ `Closed existing failed workflow process for session "${session}" (pid ${existingState.pid}).`
204
+ );
205
+ }
206
+ function readJsonFileIfExists(path) {
207
+ if (!existsSync(path)) return null;
208
+ try {
209
+ return JSON.parse(readFileSync(path, "utf8"));
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+ function readFailureSignal(path) {
215
+ const raw = readJsonFileIfExists(path);
216
+ if (!raw || typeof raw !== "object") return null;
217
+ const message = raw.message;
218
+ if (typeof message !== "string") return null;
219
+ const phase = raw.phase;
220
+ return {
221
+ message,
222
+ phase: phase === "setup" || phase === "workflow" ? phase : void 0
223
+ };
224
+ }
225
+ async function waitForFailureSignal(path, timeoutMs = 1e3) {
226
+ const deadline = Date.now() + timeoutMs;
227
+ while (Date.now() < deadline) {
228
+ const failure = readFailureSignal(path);
229
+ if (failure) return failure;
230
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
231
+ }
232
+ return readFailureSignal(path);
233
+ }
234
+ function streamOutputSince(path, offset) {
235
+ if (!existsSync(path)) return offset;
236
+ const output = readFileSync(path);
237
+ if (output.length <= offset) return output.length;
238
+ process.stdout.write(output.subarray(offset));
239
+ return output.length;
240
+ }
241
+ function clearSignalIfExists(path) {
242
+ if (!existsSync(path)) return;
243
+ try {
244
+ unlinkSync(path);
245
+ } catch {
246
+ }
247
+ }
248
+ async function waitForWorkflowOutcome(args) {
249
+ const signalPaths = getPauseSignalPaths(args.session);
250
+ if (args.pid <= 0) {
251
+ return { status: "exited" };
252
+ }
253
+ let outputOffset = 0;
254
+ while (true) {
255
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
256
+ if (existsSync(signalPaths.failedSignalPath)) {
257
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
258
+ const failure = await waitForFailureSignal(signalPaths.failedSignalPath);
259
+ return {
260
+ status: "failed",
261
+ message: failure?.message,
262
+ failurePhase: failure?.phase
263
+ };
264
+ }
265
+ if (existsSync(signalPaths.completedSignalPath)) {
266
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
267
+ return { status: "completed" };
268
+ }
269
+ if (existsSync(signalPaths.pausedSignalPath)) {
270
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
271
+ return { status: "paused" };
272
+ }
273
+ if (!isProcessRunning(args.pid)) {
274
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
275
+ return { status: "exited" };
276
+ }
277
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
278
+ }
279
+ }
280
+ async function runResume(session, logger) {
281
+ const state = readSessionStateOrThrow(session);
282
+ const {
283
+ pausedSignalPath,
284
+ resumeSignalPath,
285
+ completedSignalPath,
286
+ failedSignalPath,
287
+ outputSignalPath
288
+ } = getPauseSignalPaths(session);
289
+ if (!existsSync(pausedSignalPath)) {
290
+ throw new Error(
291
+ `Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause() first.`
292
+ );
293
+ }
294
+ if (!isProcessRunning(state.pid)) {
295
+ throw new Error(
296
+ `No active paused workflow found for session "${session}" (worker pid ${state.pid} is not running).`
297
+ );
298
+ }
299
+ clearSignalIfExists(pausedSignalPath);
300
+ clearSignalIfExists(outputSignalPath);
301
+ clearSignalIfExists(completedSignalPath);
302
+ clearSignalIfExists(failedSignalPath);
303
+ setSessionStatus(session, "active", logger);
304
+ writeFileSync(
305
+ resumeSignalPath,
306
+ JSON.stringify(
307
+ {
308
+ resumedAt: (/* @__PURE__ */ new Date()).toISOString(),
309
+ sourcePid: process.pid
310
+ },
311
+ null,
312
+ 2
313
+ ),
314
+ "utf8"
315
+ );
316
+ console.log(`Resume signal sent for session "${session}".`);
317
+ const outcome = await waitForWorkflowOutcome({
318
+ session,
319
+ pid: state.pid
320
+ });
321
+ if (outcome.status === "completed") {
322
+ setSessionStatus(session, "completed", logger);
323
+ console.log("Integration completed.");
324
+ return;
325
+ }
326
+ if (outcome.status === "failed") {
327
+ setSessionStatus(session, "failed", logger);
328
+ throw new Error(
329
+ outcome.message ? `Workflow failed after resume: ${outcome.message}` : "Workflow failed after resume."
330
+ );
331
+ }
332
+ if (outcome.status === "exited") {
333
+ setSessionStatus(session, "exited", logger);
334
+ throw new Error(
335
+ `Workflow process for session "${session}" exited before reporting completion or pause.`
336
+ );
337
+ }
338
+ setSessionStatus(session, "paused", logger);
339
+ console.log("Workflow paused.");
340
+ }
341
+ async function runIntegrationFromFile(args, logger) {
342
+ await stopExistingFailedRunSession(args.session, logger);
343
+ assertSessionAvailableForStart(args.session, logger);
344
+ const signalPaths = getPauseSignalPaths(args.session);
345
+ clearSignalIfExists(signalPaths.pausedSignalPath);
346
+ clearSignalIfExists(signalPaths.resumeSignalPath);
347
+ clearSignalIfExists(signalPaths.completedSignalPath);
348
+ clearSignalIfExists(signalPaths.failedSignalPath);
349
+ clearSignalIfExists(signalPaths.outputSignalPath);
350
+ const workerEntryPath = fileURLToPath(
351
+ new URL("../workers/run-integration-worker.js", import.meta.url)
352
+ );
353
+ const payload = JSON.stringify({
354
+ integrationPath: args.integrationPath,
355
+ exportName: args.exportName,
356
+ session: args.session,
357
+ params: args.params,
358
+ headless: args.headless,
359
+ authProfileDomain: args.authProfileDomain
360
+ });
361
+ const worker = spawn(process.execPath, [
362
+ tsxCliPath,
363
+ ...args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : [],
364
+ workerEntryPath,
365
+ payload
366
+ ], {
367
+ detached: true,
368
+ stdio: "ignore",
369
+ env: process.env
370
+ });
371
+ worker.unref();
372
+ const outcome = await waitForWorkflowOutcome({
373
+ session: args.session,
374
+ pid: worker.pid ?? 0
375
+ });
376
+ if (outcome.status === "paused") {
377
+ setSessionStatus(args.session, "paused", logger);
378
+ console.log("Workflow paused.");
379
+ return;
380
+ }
381
+ if (outcome.status === "failed") {
382
+ setSessionStatus(args.session, "failed", logger);
383
+ const message = outcome.message ?? "Workflow failed during run.";
384
+ if (outcome.failurePhase === "workflow") {
385
+ throw new Error(
386
+ `${message}
387
+ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-run the workflow.`
388
+ );
389
+ }
390
+ throw new Error(message);
391
+ }
392
+ if (outcome.status === "exited") {
393
+ setSessionStatus(args.session, "exited", logger);
394
+ throw new Error(
395
+ "Workflow process exited before reporting completion or pause during run."
396
+ );
397
+ }
398
+ setSessionStatus(args.session, "completed", logger);
399
+ }
400
+ function registerExecutionCommands(yargs, logger) {
401
+ return yargs.command(
402
+ "exec [code..]",
403
+ "Execute Playwright TypeScript code",
404
+ (cmd) => cmd.option("visualize", { type: "boolean", default: false }).option("page", { type: "string" }),
405
+ async (argv) => {
406
+ const codeParts = Array.isArray(argv.code) ? argv.code : argv.code ? [String(argv.code)] : [];
407
+ const code = codeParts.join(" ");
408
+ if (!code) {
409
+ throw new Error(
410
+ "Usage: libretto-cli exec <code> [--session <name>] [--visualize]"
411
+ );
412
+ }
413
+ await runExec(
414
+ code,
415
+ String(argv.session),
416
+ logger,
417
+ Boolean(argv.visualize),
418
+ argv.page ? String(argv.page) : void 0
419
+ );
420
+ }
421
+ ).command(
422
+ "run [integrationFile] [integrationExport]",
423
+ "Run an exported Libretto workflow from a file",
424
+ (cmd) => cmd.option("params", { type: "string" }).option("params-file", { type: "string" }).option("tsconfig", { type: "string" }).option("headed", { type: "boolean", default: false }).option("headless", { type: "boolean", default: false }).option("auth-profile", { type: "string", describe: "Domain for local auth profile (e.g. apps.example.com)" }),
425
+ async (argv) => {
426
+ const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]";
427
+ const integrationPath = argv.integrationFile;
428
+ const exportName = argv.integrationExport;
429
+ const legacyDebug = argv.debug;
430
+ if (legacyDebug !== void 0) {
431
+ throw new Error(
432
+ "The --debug flag has been removed. Run the command without --debug."
433
+ );
434
+ }
435
+ if (!integrationPath || !exportName) {
436
+ throw new Error(usage);
437
+ }
438
+ const session = String(argv.session);
439
+ const rawInlineParams = argv.params;
440
+ const paramsFile = argv["params-file"];
441
+ if (rawInlineParams && paramsFile) {
442
+ throw new Error("Pass either --params or --params-file, not both.");
443
+ }
444
+ const params = (() => {
445
+ if (paramsFile) {
446
+ let content;
447
+ try {
448
+ content = readFileSync(paramsFile, "utf8");
449
+ } catch {
450
+ throw new Error(
451
+ `Could not read --params-file "${paramsFile}". Ensure the file exists and is readable.`
452
+ );
453
+ }
454
+ return parseJsonArg("--params-file", content);
455
+ }
456
+ if (rawInlineParams) {
457
+ return parseJsonArg("--params", rawInlineParams);
458
+ }
459
+ return {};
460
+ })();
461
+ const hasHeadedFlag = Boolean(argv.headed);
462
+ const hasHeadlessFlag = Boolean(argv.headless);
463
+ if (hasHeadedFlag && hasHeadlessFlag) {
464
+ throw new Error("Cannot pass both --headed and --headless.");
465
+ }
466
+ const headlessMode = hasHeadedFlag ? false : hasHeadlessFlag ? true : void 0;
467
+ const authProfileDomain = argv["auth-profile"];
468
+ const tsconfigPath = argv.tsconfig;
469
+ await runIntegrationFromFile({
470
+ integrationPath,
471
+ exportName,
472
+ session,
473
+ params,
474
+ headless: headlessMode ?? false,
475
+ authProfileDomain,
476
+ tsconfigPath
477
+ }, logger);
478
+ }
479
+ ).command(
480
+ "resume",
481
+ "Resume a paused workflow for the current session",
482
+ (cmd) => cmd,
483
+ async (argv) => {
484
+ await runResume(String(argv.session), logger);
485
+ }
486
+ );
487
+ }
488
+ export {
489
+ registerExecutionCommands
490
+ };
@@ -0,0 +1,166 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { join, delimiter, extname } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import {
5
+ AI_CONFIG_PRESETS,
6
+ AiPresetSchema,
7
+ formatCommandPrefix,
8
+ readAiConfig
9
+ } from "../core/ai-config.js";
10
+ const AI_RUNTIME_PRESETS = AiPresetSchema.options;
11
+ function getPresetCommand(preset) {
12
+ return AI_CONFIG_PRESETS[preset][0] ?? "";
13
+ }
14
+ function isRunnableFile(filePath) {
15
+ try {
16
+ const stats = statSync(filePath);
17
+ if (!stats.isFile()) return false;
18
+ if (process.platform === "win32") {
19
+ const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
20
+ const extensions = pathExt.split(";").map((ext) => ext.trim().toUpperCase()).filter(Boolean);
21
+ const fileExt = extname(filePath).toUpperCase();
22
+ return extensions.includes(fileExt);
23
+ }
24
+ accessSync(filePath, constants.X_OK);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ function isCommandDefined(command) {
31
+ if (!command) return false;
32
+ if (command.includes("/") || command.includes("\\")) {
33
+ return isRunnableFile(command);
34
+ }
35
+ const pathEnv = process.env.PATH ?? "";
36
+ if (!pathEnv) return false;
37
+ const pathEntries = pathEnv.split(delimiter).filter(Boolean);
38
+ if (process.platform === "win32") {
39
+ const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
40
+ const extensions = pathExt.split(";").map((ext) => ext.trim()).filter(Boolean);
41
+ const hasExtension = /\.[^./\\]+$/.test(command);
42
+ const candidates = hasExtension ? [command] : extensions.map(
43
+ (ext) => ext.startsWith(".") ? `${command}${ext}` : `${command}.${ext}`
44
+ );
45
+ return pathEntries.some(
46
+ (dir) => candidates.some((candidate) => isRunnableFile(join(dir, candidate)))
47
+ );
48
+ }
49
+ return pathEntries.some((dir) => isRunnableFile(join(dir, command)));
50
+ }
51
+ function detectAvailableAiRuntimeCommands() {
52
+ return AI_RUNTIME_PRESETS.filter(
53
+ (preset) => isCommandDefined(getPresetCommand(preset))
54
+ );
55
+ }
56
+ function printAiConfigureCommands(prefix = " ") {
57
+ for (const preset of AI_RUNTIME_PRESETS) {
58
+ console.log(`${prefix}npx libretto ai configure ${preset}`);
59
+ }
60
+ }
61
+ function printDifferentAnalyzerHint(prefix = " ") {
62
+ console.log(
63
+ `${prefix}Use npx libretto ai configure <gemini|claude|codex> to configure a different AI analyzer.`
64
+ );
65
+ }
66
+ function installBrowsers() {
67
+ console.log("\nInstalling Playwright Chromium...");
68
+ const result = spawnSync("npx", ["playwright", "install", "chromium"], {
69
+ stdio: "inherit",
70
+ shell: true
71
+ });
72
+ if (result.status === 0) {
73
+ console.log(" \u2713 Playwright Chromium installed");
74
+ } else {
75
+ console.error(
76
+ " \u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
77
+ );
78
+ }
79
+ }
80
+ function checkAiRuntimeConfiguration() {
81
+ let config = null;
82
+ let configReadError = null;
83
+ try {
84
+ config = readAiConfig();
85
+ } catch (error) {
86
+ configReadError = error instanceof Error ? error.message : String(error);
87
+ }
88
+ const availableCommands = detectAvailableAiRuntimeCommands();
89
+ console.log("\nAI runtime configuration:");
90
+ console.log(
91
+ " Libretto can use your coding agent as a subagent to analyze snapshots and other page signals."
92
+ );
93
+ console.log(
94
+ " This is optional, but it significantly improves page understanding and debugging performance."
95
+ );
96
+ if (configReadError) {
97
+ console.log(` \u2717 Could not read AI config: ${configReadError}`);
98
+ console.log(" Reconfigure with:");
99
+ printAiConfigureCommands(" ");
100
+ printDifferentAnalyzerHint(" ");
101
+ return;
102
+ }
103
+ if (config) {
104
+ const configuredCommand = config.commandPrefix[0];
105
+ if (!isCommandDefined(configuredCommand)) {
106
+ console.log(
107
+ ` \u2717 Configured command not found: ${configuredCommand ?? "(empty)"}`
108
+ );
109
+ if (availableCommands.length > 0) {
110
+ console.log(
111
+ ` Detected available commands: ${availableCommands.join(", ")}`
112
+ );
113
+ } else {
114
+ console.log(
115
+ " No codex, claude, or gemini analyzer command was detected on PATH."
116
+ );
117
+ }
118
+ console.log(" Reconfigure with:");
119
+ printAiConfigureCommands(" ");
120
+ printDifferentAnalyzerHint(" ");
121
+ return;
122
+ }
123
+ console.log(
124
+ ` \u2713 Configured (${config.preset}): ${formatCommandPrefix(config.commandPrefix)}`
125
+ );
126
+ console.log(" Analysis commands are ready to use.");
127
+ printDifferentAnalyzerHint(" ");
128
+ return;
129
+ }
130
+ console.log(" \u2717 No AI config set.");
131
+ if (availableCommands.length > 0) {
132
+ console.log(
133
+ ` Detected available commands: ${availableCommands.join(", ")}`
134
+ );
135
+ } else {
136
+ console.log(" No codex, claude, or gemini analyzer command was detected on PATH.");
137
+ }
138
+ console.log(" Configure one with:");
139
+ printAiConfigureCommands(" ");
140
+ printDifferentAnalyzerHint(" ");
141
+ console.log(" Optionally provide a custom command prefix with '-- ...'.");
142
+ }
143
+ function registerInitCommand(yargs) {
144
+ return yargs.command(
145
+ "init",
146
+ "Initialize libretto in the current project",
147
+ (cmd) => cmd.option("skip-browsers", {
148
+ type: "boolean",
149
+ default: false,
150
+ describe: "Skip Playwright Chromium installation"
151
+ }),
152
+ (argv) => {
153
+ console.log("Initializing libretto...\n");
154
+ if (!argv["skip-browsers"]) {
155
+ installBrowsers();
156
+ } else {
157
+ console.log("\nSkipping browser installation (--skip-browsers)");
158
+ }
159
+ checkAiRuntimeConfiguration();
160
+ console.log("\n\u2713 libretto init complete");
161
+ }
162
+ );
163
+ }
164
+ export {
165
+ registerInitCommand
166
+ };