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
package/src/cli/router.ts CHANGED
@@ -1,28 +1,22 @@
1
- import type { Logger } from "../shared/logger/index.js";
2
1
  import { aiCommands } from "./commands/ai.js";
3
- import { createBrowserCommands } from "./commands/browser.js";
4
- import { createExecutionCommands } from "./commands/execution.js";
2
+ import { browserCommands } from "./commands/browser.js";
3
+ import { deployCommand } from "./commands/deploy.js";
4
+ import { executionCommands } from "./commands/execution.js";
5
5
  import { initCommand } from "./commands/init.js";
6
6
  import { logCommands } from "./commands/logs.js";
7
- import { sessionOption } from "./commands/shared.js";
8
- import { createSnapshotCommand } from "./commands/snapshot.js";
7
+ import { snapshotCommand } from "./commands/snapshot.js";
9
8
  import { SimpleCLI } from "./framework/simple-cli.js";
10
9
 
11
- export function buildCLIRoutes(logger: Logger) {
12
- return {
13
- ...createBrowserCommands(logger),
14
- ...createExecutionCommands(logger),
15
- ...logCommands,
16
- ai: aiCommands,
17
- init: initCommand,
18
- snapshot: createSnapshotCommand(logger),
19
- };
20
- }
10
+ export const cliRoutes = {
11
+ ...browserCommands,
12
+ deploy: deployCommand,
13
+ ...executionCommands,
14
+ ...logCommands,
15
+ ai: aiCommands,
16
+ init: initCommand,
17
+ snapshot: snapshotCommand,
18
+ };
21
19
 
22
- export function createCLIApp(logger: Logger) {
23
- return SimpleCLI.define("libretto", buildCLIRoutes(logger), {
24
- globalNamed: {
25
- session: sessionOption(),
26
- },
27
- });
20
+ export function createCLIApp() {
21
+ return SimpleCLI.define("libretto", cliRoutes);
28
22
  }
@@ -5,8 +5,11 @@ import { cwd } from "node:process";
5
5
  import { isAbsolute, resolve } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import {
8
+ getWorkflowFromModuleExports,
9
+ getWorkflowsFromModuleExports,
8
10
  instrumentContext,
9
11
  launchBrowser,
12
+ type ExportedLibrettoWorkflow,
10
13
  type LibrettoWorkflowContext,
11
14
  } from "../../index.js";
12
15
  import type { LoggerApi } from "../../shared/logger/index.js";
@@ -18,14 +21,14 @@ import {
18
21
  getSessionNetworkLogPath,
19
22
  getSessionStatePath,
20
23
  } from "../core/context.js";
21
- import { getPauseSignalPaths, removeSignalIfExists } from "../core/pause-signals.js";
24
+ import {
25
+ getPauseSignalPaths,
26
+ removeSignalIfExists,
27
+ } from "../core/pause-signals.js";
22
28
  import { installSessionTelemetry } from "../core/session-telemetry.js";
23
29
  import type { RunIntegrationWorkerRequest } from "./run-integration-worker-protocol.js";
24
30
 
25
- const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
26
-
27
- type LoadedLibrettoWorkflow = {
28
- metadata: {};
31
+ type LoadedLibrettoWorkflow = ExportedLibrettoWorkflow & {
29
32
  run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
30
33
  };
31
34
 
@@ -73,7 +76,10 @@ function readSessionStatePid(session: string): number | null {
73
76
  if (!existsSync(statePath)) return null;
74
77
 
75
78
  try {
76
- return parseSessionStateContent(readFileSync(statePath, "utf8"), statePath).pid;
79
+ return (
80
+ parseSessionStateContent(readFileSync(statePath, "utf8"), statePath)
81
+ .pid ?? null
82
+ );
77
83
  } catch {
78
84
  return null;
79
85
  }
@@ -103,17 +109,6 @@ async function waitForFailureSessionRelease(args: {
103
109
  }
104
110
  }
105
111
 
106
- function isLoadedLibrettoWorkflow(value: unknown): value is LoadedLibrettoWorkflow {
107
- if (!value || typeof value !== "object") return false;
108
- const candidate = value as Record<PropertyKey, unknown>;
109
- return (
110
- candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
111
- typeof candidate.run === "function" &&
112
- !!candidate.metadata &&
113
- typeof candidate.metadata === "object"
114
- );
115
- }
116
-
117
112
  function resolveLocalAuthProfilePath(domain: string): string {
118
113
  return getProfilePath(normalizeDomain(domain));
119
114
  }
@@ -144,9 +139,9 @@ function getAbsoluteIntegrationPath(integrationPath: string): string {
144
139
  return absolutePath;
145
140
  }
146
141
 
147
- async function loadWorkflowExport(
142
+ async function loadWorkflowByName(
148
143
  absolutePath: string,
149
- exportName: string,
144
+ workflowName: string,
150
145
  ): Promise<LoadedLibrettoWorkflow> {
151
146
  let loadedModule: Record<string, unknown>;
152
147
  try {
@@ -162,42 +157,23 @@ async function loadWorkflowExport(
162
157
  );
163
158
  }
164
159
 
165
- const targetExport = loadedModule[exportName];
166
- if (!targetExport) {
167
- const availableExports = Object.keys(loadedModule);
168
- const detail =
169
- availableExports.length > 0
170
- ? ` Available exports: ${availableExports.join(", ")}`
171
- : " The module has no exports.";
172
- throw new Error(
173
- `Export "${exportName}" was not found in ${absolutePath}.${detail}`,
174
- );
160
+ const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
161
+ if (workflow) {
162
+ return workflow as LoadedLibrettoWorkflow;
175
163
  }
176
164
 
177
- if (!isLoadedLibrettoWorkflow(targetExport)) {
178
- throw new Error(
179
- [
180
- `Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
181
- "",
182
- 'A workflow must be created using the workflow() function from "libretto":',
183
- "",
184
- ' import { workflow } from "libretto";',
185
- "",
186
- ` export const ${exportName} = workflow<InputType, OutputType>(`,
187
- " {},",
188
- " async (ctx, input) => {",
189
- " // ctx.page — Playwright Page instance",
190
- " // ctx.logger — MinimalLogger",
191
- " // ctx.services — injected dependencies (generic, default {})",
192
- " // input — JSON-serializable input matching InputType",
193
- " return output; // must match OutputType",
194
- " },",
195
- " );",
196
- ].join("\n"),
197
- );
198
- }
165
+ const availableWorkflows = getWorkflowsFromModuleExports(loadedModule).map(
166
+ (candidate) => candidate.name,
167
+ );
168
+
169
+ const detail =
170
+ availableWorkflows.length > 0
171
+ ? ` Available workflows: ${availableWorkflows.join(", ")}`
172
+ : ' No workflows found in this file. Export a workflow() instance from "libretto" directly or via `export const workflows = { ... }`.';
199
173
 
200
- return targetExport;
174
+ throw new Error(
175
+ `Workflow "${workflowName}" not found in ${absolutePath}.${detail}`,
176
+ );
201
177
  }
202
178
 
203
179
  export async function installHeadedWorkflowVisualization(args: {
@@ -219,7 +195,7 @@ async function runIntegrationInternal(
219
195
  ): Promise<RunIntegrationOutcome> {
220
196
  const { logger } = options;
221
197
  const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
222
- const workflow = await loadWorkflowExport(absolutePath, args.exportName);
198
+ const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
223
199
  const signalPaths = getPauseSignalPaths(args.session);
224
200
  await removeSignalIfExists(signalPaths.pausedSignalPath);
225
201
  await removeSignalIfExists(signalPaths.resumeSignalPath);
@@ -228,12 +204,12 @@ async function runIntegrationInternal(
228
204
  const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
229
205
 
230
206
  console.log(
231
- `Running integration "${args.exportName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
207
+ `Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
232
208
  );
233
209
 
234
210
  const integrationLogger = logger.withScope("integration-run", {
235
211
  integrationPath: absolutePath,
236
- integrationExport: args.exportName,
212
+ workflowName: args.workflowName,
237
213
  session: args.session,
238
214
  });
239
215
 
@@ -255,6 +231,7 @@ async function runIntegrationInternal(
255
231
  sessionName: args.session,
256
232
  headless: args.headless,
257
233
  storageStatePath,
234
+ viewport: args.viewport,
258
235
  });
259
236
  if (!args.headless && args.visualize !== false) {
260
237
  await installHeadedWorkflowVisualization({
@@ -277,16 +254,17 @@ async function runIntegrationInternal(
277
254
  });
278
255
 
279
256
  const workflowContext: LibrettoWorkflowContext = {
257
+ session: args.session,
280
258
  logger: integrationLogger,
281
259
  page: browserSession.page,
282
- services: {},
283
260
  };
284
261
 
285
262
  try {
286
263
  try {
287
264
  await workflow.run(workflowContext, args.params ?? {});
288
265
  } catch (error) {
289
- const errorMessage = error instanceof Error ? error.message : String(error);
266
+ const errorMessage =
267
+ error instanceof Error ? error.message : String(error);
290
268
  await writeFile(
291
269
  signalPaths.failedSignalPath,
292
270
  JSON.stringify(
@@ -2,12 +2,13 @@ import { z } from "zod";
2
2
 
3
3
  export const RunIntegrationWorkerRequestSchema = z.object({
4
4
  integrationPath: z.string().min(1),
5
- exportName: z.string().min(1),
5
+ workflowName: z.string().min(1),
6
6
  session: z.string().min(1),
7
7
  params: z.unknown(),
8
8
  headless: z.boolean(),
9
9
  visualize: z.boolean().default(true),
10
10
  authProfileDomain: z.string().optional(),
11
+ viewport: z.object({ width: z.number(), height: z.number() }).optional(),
11
12
  });
12
13
 
13
14
  export type RunIntegrationWorkerRequest = z.infer<
@@ -5,10 +5,7 @@ import {
5
5
  type RunIntegrationWorkerRequest,
6
6
  } from "./run-integration-worker-protocol.js";
7
7
  import { runIntegrationFromFileInWorker } from "./run-integration-runtime.js";
8
- import {
9
- ensureLibrettoSetup,
10
- withSessionLogger,
11
- } from "../core/context.js";
8
+ import { ensureLibrettoSetup, withSessionLogger } from "../core/context.js";
12
9
  import { getPauseSignalPaths } from "../core/pause-signals.js";
13
10
 
14
11
  function parseWorkerRequest(argv: string[]): RunIntegrationWorkerRequest {
package/src/index.ts CHANGED
@@ -2,119 +2,129 @@ import { resolve } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
 
4
4
  // Logger
5
- export { Logger, defaultLogger, type LoggerApi, type MinimalLogger, type LoggerSink, type LogOptions } from "./shared/logger/logger.js";
6
5
  export {
7
- createFileLogSink,
8
- prettyConsoleSink,
9
- jsonlConsoleSink,
6
+ Logger,
7
+ defaultLogger,
8
+ type LoggerApi,
9
+ type MinimalLogger,
10
+ type LoggerSink,
11
+ type LogOptions,
12
+ } from "./shared/logger/logger.js";
13
+ export {
14
+ createFileLogSink,
15
+ prettyConsoleSink,
16
+ jsonlConsoleSink,
10
17
  } from "./shared/logger/sinks.js";
11
18
 
12
19
  // LLM client interface
13
- export type { LLMClient, Message, MessageContentPart } from "./shared/llm/types.js";
20
+ export type {
21
+ LLMClient,
22
+ Message,
23
+ MessageContentPart,
24
+ } from "./shared/llm/types.js";
14
25
  export { createLLMClientFromModel } from "./shared/llm/ai-sdk-adapter.js";
15
26
  export {
16
- SESSION_STATE_VERSION,
17
- SessionStatusSchema,
18
- SessionStateFileSchema,
19
- parseSessionStateData,
20
- parseSessionStateContent,
21
- serializeSessionState,
22
- type SessionStatus,
23
- type SessionState,
24
- type SessionStateFile,
27
+ SESSION_STATE_VERSION,
28
+ SessionStatusSchema,
29
+ SessionStateFileSchema,
30
+ parseSessionStateData,
31
+ parseSessionStateContent,
32
+ serializeSessionState,
33
+ type SessionStatus,
34
+ type SessionState,
35
+ type SessionStateFile,
25
36
  } from "./shared/state/index.js";
26
37
 
27
38
  // Recovery
28
39
  export { executeRecoveryAgent } from "./runtime/recovery/agent.js";
29
40
  export { attemptWithRecovery } from "./runtime/recovery/recovery.js";
30
41
  export {
31
- detectSubmissionError,
32
- type KnownSubmissionError,
33
- type DetectedSubmissionError,
42
+ detectSubmissionError,
43
+ type KnownSubmissionError,
44
+ type DetectedSubmissionError,
34
45
  } from "./runtime/recovery/errors.js";
35
46
 
36
47
  // AI extraction
37
- export { extractFromPage, type ExtractOptions } from "./runtime/extract/extract.js";
48
+ export {
49
+ extractFromPage,
50
+ type ExtractOptions,
51
+ } from "./runtime/extract/extract.js";
38
52
 
39
53
  // Network helpers
40
54
  export {
41
- pageRequest,
42
- type RequestConfig,
43
- type PageRequestOptions,
55
+ pageRequest,
56
+ type RequestConfig,
57
+ type PageRequestOptions,
44
58
  } from "./runtime/network/network.js";
45
59
 
46
60
  // Download helpers
47
61
  export {
48
- downloadViaClick,
49
- downloadAndSave,
50
- type DownloadResult,
51
- type DownloadViaClickOptions,
52
- type SaveDownloadOptions,
62
+ downloadViaClick,
63
+ downloadAndSave,
64
+ type DownloadResult,
65
+ type DownloadViaClickOptions,
66
+ type SaveDownloadOptions,
53
67
  } from "./runtime/download/download.js";
54
68
 
55
69
  // Debug / Pause
56
70
  export { pause } from "./shared/debug/pause.js";
57
71
 
58
- // Config
59
- export {
60
- isDebugMode,
61
- isDryRun,
62
- shouldPauseBeforeMutation,
63
- } from "./shared/config/config.js";
64
-
65
72
  // Instrumentation
66
73
  export {
67
- instrumentPage,
68
- installInstrumentation,
69
- instrumentContext,
70
- type InstrumentationOptions,
71
- type InstrumentedPage,
74
+ instrumentPage,
75
+ installInstrumentation,
76
+ instrumentContext,
77
+ type InstrumentationOptions,
78
+ type InstrumentedPage,
72
79
  } from "./shared/instrumentation/instrument.js";
73
80
 
74
81
  // Visualization
75
82
  export {
76
- ensureGhostCursor,
77
- moveGhostCursor,
78
- ghostClick,
79
- hideGhostCursor,
80
- type GhostCursorOptions,
83
+ ensureGhostCursor,
84
+ moveGhostCursor,
85
+ ghostClick,
86
+ hideGhostCursor,
87
+ type GhostCursorOptions,
81
88
  } from "./shared/visualization/ghost-cursor.js";
82
89
  export {
83
- ensureHighlightLayer,
84
- showHighlight,
85
- clearHighlights,
86
- type HighlightOptions,
90
+ ensureHighlightLayer,
91
+ showHighlight,
92
+ clearHighlights,
93
+ type HighlightOptions,
87
94
  } from "./shared/visualization/highlight.js";
88
95
 
89
96
  // Run helpers
90
97
  export {
91
- launchBrowser,
92
- type LaunchBrowserArgs,
93
- type BrowserSession,
98
+ launchBrowser,
99
+ type LaunchBrowserArgs,
100
+ type BrowserSession,
94
101
  } from "./shared/run/api.js";
95
102
 
96
103
  // Workflow helpers
97
104
  export {
98
- LibrettoWorkflow,
99
- LIBRETTO_WORKFLOW_BRAND,
100
- workflow,
101
- type LibrettoWorkflowMetadata,
102
- type LibrettoWorkflowContext,
103
- type LibrettoWorkflowHandler,
105
+ getWorkflowFromModuleExports,
106
+ getWorkflowsFromModuleExports,
107
+ isLibrettoWorkflow,
108
+ LibrettoWorkflow,
109
+ LIBRETTO_WORKFLOW_BRAND,
110
+ workflow,
111
+ type ExportedLibrettoWorkflow,
112
+ type LibrettoWorkflowContext,
113
+ type LibrettoWorkflowHandler,
104
114
  } from "./shared/workflow/workflow.js";
105
-
106
115
  const isDirectExecution = (): boolean => {
107
- const entryArg = process.argv[1];
108
- if (!entryArg) {
109
- return false;
110
- }
111
- return pathToFileURL(resolve(entryArg)).href === import.meta.url;
116
+ const entryArg = process.argv[1];
117
+ if (!entryArg) {
118
+ return false;
119
+ }
120
+ return pathToFileURL(resolve(entryArg)).href === import.meta.url;
112
121
  };
113
122
 
114
123
  if (isDirectExecution()) {
115
- void import("./cli/index.js").catch((error: unknown) => {
116
- const message = error instanceof Error ? error.stack ?? error.message : String(error);
117
- process.stderr.write(`${message}\n`);
118
- process.exitCode = 1;
119
- });
124
+ void import("./cli/index.js").catch((error: unknown) => {
125
+ const message =
126
+ error instanceof Error ? (error.stack ?? error.message) : String(error);
127
+ process.stderr.write(`${message}\n`);
128
+ process.exitCode = 1;
129
+ });
120
130
  }
@@ -4,16 +4,16 @@ import type { Page, Download } from "playwright";
4
4
  import type { MinimalLogger } from "../../shared/logger/logger.js";
5
5
 
6
6
  export type DownloadResult = {
7
- /** The raw file contents. */
8
- buffer: Buffer;
9
- /** The filename suggested by the server (Content-Disposition header or URL). */
10
- filename: string;
7
+ /** The raw file contents. */
8
+ buffer: Buffer;
9
+ /** The filename suggested by the server (Content-Disposition header or URL). */
10
+ filename: string;
11
11
  };
12
12
 
13
13
  export type DownloadViaClickOptions = {
14
- logger?: MinimalLogger;
15
- /** Timeout in milliseconds for waiting on the download event. Defaults to 30 000. */
16
- timeout?: number;
14
+ logger?: MinimalLogger;
15
+ /** Timeout in milliseconds for waiting on the download event. Defaults to 30 000. */
16
+ timeout?: number;
17
17
  };
18
18
 
19
19
  /**
@@ -24,55 +24,55 @@ export type DownloadViaClickOptions = {
24
24
  * never missed.
25
25
  */
26
26
  export async function downloadViaClick(
27
- page: Page,
28
- selector: string,
29
- options?: DownloadViaClickOptions,
27
+ page: Page,
28
+ selector: string,
29
+ options?: DownloadViaClickOptions,
30
30
  ): Promise<DownloadResult> {
31
- const { logger, timeout = 30_000 } = options ?? {};
31
+ const { logger, timeout = 30_000 } = options ?? {};
32
32
 
33
- const startTime = Date.now();
33
+ const startTime = Date.now();
34
34
 
35
- // 1. Register the download listener BEFORE clicking
36
- const downloadPromise = page.waitForEvent("download", { timeout });
35
+ // 1. Register the download listener BEFORE clicking
36
+ const downloadPromise = page.waitForEvent("download", { timeout });
37
37
 
38
- // 2. Click the element that triggers the download
39
- await page.locator(selector).click();
38
+ // 2. Click the element that triggers the download
39
+ await page.locator(selector).click();
40
40
 
41
- // 3. Await the download event
42
- const download: Download = await downloadPromise;
41
+ // 3. Await the download event
42
+ const download: Download = await downloadPromise;
43
43
 
44
- // 4. Get the suggested filename
45
- const filename = download.suggestedFilename();
44
+ // 4. Get the suggested filename
45
+ const filename = download.suggestedFilename();
46
46
 
47
- // 5. Read the downloaded file into a buffer
48
- const readStream = await download.createReadStream();
49
- if (!readStream) {
50
- throw new Error(
51
- `Download stream unavailable for "${filename}". The browser may have been closed before the download completed.`,
52
- );
53
- }
47
+ // 5. Read the downloaded file into a buffer
48
+ const readStream = await download.createReadStream();
49
+ if (!readStream) {
50
+ throw new Error(
51
+ `Download stream unavailable for "${filename}". The browser may have been closed before the download completed.`,
52
+ );
53
+ }
54
54
 
55
- const chunks: Buffer[] = [];
56
- for await (const chunk of readStream) {
57
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
58
- }
59
- const buffer = Buffer.concat(chunks);
55
+ const chunks: Buffer[] = [];
56
+ for await (const chunk of readStream) {
57
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
58
+ }
59
+ const buffer = Buffer.concat(chunks);
60
60
 
61
- const duration = Date.now() - startTime;
61
+ const duration = Date.now() - startTime;
62
62
 
63
- logger?.info("download:click", {
64
- selector,
65
- filename,
66
- size: buffer.length,
67
- duration,
68
- });
63
+ logger?.info("download:click", {
64
+ selector,
65
+ filename,
66
+ size: buffer.length,
67
+ duration,
68
+ });
69
69
 
70
- return { buffer, filename };
70
+ return { buffer, filename };
71
71
  }
72
72
 
73
73
  export type SaveDownloadOptions = DownloadViaClickOptions & {
74
- /** Absolute or relative path to save the file to. When omitted the suggested filename is used in the current working directory. */
75
- savePath?: string;
74
+ /** Absolute or relative path to save the file to. When omitted the suggested filename is used in the current working directory. */
75
+ savePath?: string;
76
76
  };
77
77
 
78
78
  /**
@@ -80,21 +80,25 @@ export type SaveDownloadOptions = DownloadViaClickOptions & {
80
80
  * downloaded file to disk.
81
81
  */
82
82
  export async function downloadAndSave(
83
- page: Page,
84
- selector: string,
85
- options?: SaveDownloadOptions,
83
+ page: Page,
84
+ selector: string,
85
+ options?: SaveDownloadOptions,
86
86
  ): Promise<DownloadResult & { savedTo: string }> {
87
- const { savePath, ...downloadOpts } = options ?? {};
88
- const { buffer, filename } = await downloadViaClick(page, selector, downloadOpts);
89
-
90
- const dest = resolve(savePath ?? filename);
91
- await writeFile(dest, buffer);
92
-
93
- options?.logger?.info("download:saved", {
94
- filename,
95
- savedTo: dest,
96
- size: buffer.length,
97
- });
98
-
99
- return { buffer, filename, savedTo: dest };
87
+ const { savePath, ...downloadOpts } = options ?? {};
88
+ const { buffer, filename } = await downloadViaClick(
89
+ page,
90
+ selector,
91
+ downloadOpts,
92
+ );
93
+
94
+ const dest = resolve(savePath ?? filename);
95
+ await writeFile(dest, buffer);
96
+
97
+ options?.logger?.info("download:saved", {
98
+ filename,
99
+ savedTo: dest,
100
+ size: buffer.length,
101
+ });
102
+
103
+ return { buffer, filename, savedTo: dest };
100
104
  }
@@ -1,7 +1,7 @@
1
1
  export {
2
- downloadViaClick,
3
- downloadAndSave,
4
- type DownloadResult,
5
- type DownloadViaClickOptions,
6
- type SaveDownloadOptions,
2
+ downloadViaClick,
3
+ downloadAndSave,
4
+ type DownloadResult,
5
+ type DownloadViaClickOptions,
6
+ type SaveDownloadOptions,
7
7
  } from "./download.js";