libretto 0.5.2 → 0.5.3-experimental.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.
@@ -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";
@@ -25,9 +28,7 @@ import {
25
28
  import { installSessionTelemetry } from "../core/session-telemetry.js";
26
29
  import type { RunIntegrationWorkerRequest } from "./run-integration-worker-protocol.js";
27
30
 
28
- const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
29
-
30
- type LoadedLibrettoWorkflow = {
31
+ type LoadedLibrettoWorkflow = ExportedLibrettoWorkflow & {
31
32
  run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
32
33
  };
33
34
 
@@ -108,17 +109,6 @@ async function waitForFailureSessionRelease(args: {
108
109
  }
109
110
  }
110
111
 
111
- function isLoadedLibrettoWorkflow(
112
- value: unknown,
113
- ): value is LoadedLibrettoWorkflow {
114
- if (!value || typeof value !== "object") return false;
115
- const candidate = value as Record<PropertyKey, unknown>;
116
- return (
117
- candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
118
- typeof candidate.run === "function"
119
- );
120
- }
121
-
122
112
  function resolveLocalAuthProfilePath(domain: string): string {
123
113
  return getProfilePath(normalizeDomain(domain));
124
114
  }
@@ -149,9 +139,9 @@ function getAbsoluteIntegrationPath(integrationPath: string): string {
149
139
  return absolutePath;
150
140
  }
151
141
 
152
- async function loadWorkflowExport(
142
+ async function loadWorkflowByName(
153
143
  absolutePath: string,
154
- exportName: string,
144
+ workflowName: string,
155
145
  ): Promise<LoadedLibrettoWorkflow> {
156
146
  let loadedModule: Record<string, unknown>;
157
147
  try {
@@ -167,42 +157,23 @@ async function loadWorkflowExport(
167
157
  );
168
158
  }
169
159
 
170
- const targetExport = loadedModule[exportName];
171
- if (!targetExport) {
172
- const availableExports = Object.keys(loadedModule);
173
- const detail =
174
- availableExports.length > 0
175
- ? ` Available exports: ${availableExports.join(", ")}`
176
- : " The module has no exports.";
177
- throw new Error(
178
- `Export "${exportName}" was not found in ${absolutePath}.${detail}`,
179
- );
160
+ const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
161
+ if (workflow) {
162
+ return workflow as LoadedLibrettoWorkflow;
180
163
  }
181
164
 
182
- if (!isLoadedLibrettoWorkflow(targetExport)) {
183
- throw new Error(
184
- [
185
- `Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
186
- "",
187
- 'A workflow must be created using the workflow() function from "libretto":',
188
- "",
189
- ' import { workflow } from "libretto";',
190
- "",
191
- ` export const ${exportName} = workflow<InputType, OutputType>(`,
192
- " async (ctx, input) => {",
193
- " // ctx.session — libretto session name",
194
- " // ctx.page — Playwright Page instance",
195
- " // ctx.logger — MinimalLogger",
196
- " // ctx.services — injected dependencies (generic, default {})",
197
- " // input — JSON-serializable input matching InputType",
198
- " return output; // must match OutputType",
199
- " },",
200
- " );",
201
- ].join("\n"),
202
- );
203
- }
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 = { ... }`.';
204
173
 
205
- return targetExport;
174
+ throw new Error(
175
+ `Workflow "${workflowName}" not found in ${absolutePath}.${detail}`,
176
+ );
206
177
  }
207
178
 
208
179
  export async function installHeadedWorkflowVisualization(args: {
@@ -224,7 +195,7 @@ async function runIntegrationInternal(
224
195
  ): Promise<RunIntegrationOutcome> {
225
196
  const { logger } = options;
226
197
  const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
227
- const workflow = await loadWorkflowExport(absolutePath, args.exportName);
198
+ const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
228
199
  const signalPaths = getPauseSignalPaths(args.session);
229
200
  await removeSignalIfExists(signalPaths.pausedSignalPath);
230
201
  await removeSignalIfExists(signalPaths.resumeSignalPath);
@@ -233,12 +204,12 @@ async function runIntegrationInternal(
233
204
  const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
234
205
 
235
206
  console.log(
236
- `Running integration "${args.exportName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
207
+ `Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
237
208
  );
238
209
 
239
210
  const integrationLogger = logger.withScope("integration-run", {
240
211
  integrationPath: absolutePath,
241
- integrationExport: args.exportName,
212
+ workflowName: args.workflowName,
242
213
  session: args.session,
243
214
  });
244
215
 
@@ -287,6 +258,7 @@ async function runIntegrationInternal(
287
258
  logger: integrationLogger,
288
259
  page: browserSession.page,
289
260
  services: {},
261
+ credentials: args.credentials,
290
262
  };
291
263
 
292
264
  try {
@@ -2,9 +2,10 @@ 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
+ credentials: z.record(z.string(), z.unknown()).optional(),
8
9
  headless: z.boolean(),
9
10
  visualize: z.boolean().default(true),
10
11
  authProfileDomain: z.string().optional(),
package/src/index.ts CHANGED
@@ -102,9 +102,13 @@ export {
102
102
 
103
103
  // Workflow helpers
104
104
  export {
105
+ getWorkflowFromModuleExports,
106
+ getWorkflowsFromModuleExports,
107
+ isLibrettoWorkflow,
105
108
  LibrettoWorkflow,
106
109
  LIBRETTO_WORKFLOW_BRAND,
107
110
  workflow,
111
+ type ExportedLibrettoWorkflow,
108
112
  type LibrettoWorkflowContext,
109
113
  type LibrettoWorkflowHandler,
110
114
  } from "./shared/workflow/workflow.js";
@@ -8,6 +8,7 @@ export type LibrettoWorkflowContext<S = {}> = {
8
8
  page: Page;
9
9
  logger: MinimalLogger;
10
10
  services: S;
11
+ credentials?: Record<string, unknown>;
11
12
  };
12
13
 
13
14
  export type LibrettoWorkflowHandler<
@@ -18,9 +19,14 @@ export type LibrettoWorkflowHandler<
18
19
 
19
20
  export class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
20
21
  public readonly [LIBRETTO_WORKFLOW_BRAND] = true;
22
+ public readonly name: string;
21
23
  private readonly handler: LibrettoWorkflowHandler<Input, Output, S>;
22
24
 
23
- constructor(handler: LibrettoWorkflowHandler<Input, Output, S>) {
25
+ constructor(
26
+ name: string,
27
+ handler: LibrettoWorkflowHandler<Input, Output, S>,
28
+ ) {
29
+ this.name = name;
24
30
  this.handler = handler;
25
31
  }
26
32
 
@@ -29,8 +35,88 @@ export class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
29
35
  }
30
36
  }
31
37
 
38
+ export type ExportedLibrettoWorkflow = {
39
+ readonly [LIBRETTO_WORKFLOW_BRAND]: true;
40
+ readonly name: string;
41
+ run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
42
+ };
43
+
44
+ type WorkflowModuleExports = Record<string, unknown>;
45
+
46
+ // Use the workflow brand instead of `instanceof` so imported workflows are
47
+ // still recognized after loading the integration module dynamically.
48
+ export function isLibrettoWorkflow(
49
+ value: unknown,
50
+ ): value is ExportedLibrettoWorkflow {
51
+ if (!value || typeof value !== "object") return false;
52
+ const candidate = value as Record<PropertyKey, unknown>;
53
+ return (
54
+ candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
55
+ typeof candidate.name === "string" &&
56
+ typeof candidate.run === "function"
57
+ );
58
+ }
59
+
60
+ function addWorkflowOrThrow(
61
+ workflowsByName: Map<string, ExportedLibrettoWorkflow>,
62
+ value: unknown,
63
+ ): void {
64
+ if (!isLibrettoWorkflow(value)) return;
65
+
66
+ // Re-exporting the same workflow object is fine, but two distinct workflow
67
+ // instances cannot claim the same runtime name.
68
+ const existing = workflowsByName.get(value.name);
69
+ if (existing && existing !== value) {
70
+ throw new Error(
71
+ `Duplicate workflow name: "${value.name}". Each workflow() call must use a unique name.`,
72
+ );
73
+ }
74
+
75
+ workflowsByName.set(value.name, value);
76
+ }
77
+
78
+ export function getWorkflowsFromModuleExports(
79
+ moduleExports: WorkflowModuleExports,
80
+ ): ExportedLibrettoWorkflow[] {
81
+ const workflowsByName = new Map<string, ExportedLibrettoWorkflow>();
82
+
83
+ for (const [exportName, value] of Object.entries(moduleExports)) {
84
+ if (exportName === "workflows" && value && typeof value === "object") {
85
+ // Support both `export const workflows = workflow(...)` and
86
+ // `export const workflows = { myWorkflow }`.
87
+ if (isLibrettoWorkflow(value)) {
88
+ addWorkflowOrThrow(workflowsByName, value);
89
+ } else {
90
+ for (const nestedValue of Object.values(
91
+ value as Record<string, unknown>,
92
+ )) {
93
+ addWorkflowOrThrow(workflowsByName, nestedValue);
94
+ }
95
+ }
96
+ continue;
97
+ }
98
+
99
+ addWorkflowOrThrow(workflowsByName, value);
100
+ }
101
+
102
+ return [...workflowsByName.values()];
103
+ }
104
+
105
+ export function getWorkflowFromModuleExports(
106
+ moduleExports: WorkflowModuleExports,
107
+ workflowName: string,
108
+ ): ExportedLibrettoWorkflow | null {
109
+ for (const workflow of getWorkflowsFromModuleExports(moduleExports)) {
110
+ if (workflow.name === workflowName) {
111
+ return workflow;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
32
117
  export function workflow<Input = unknown, Output = unknown, S = {}>(
118
+ name: string,
33
119
  handler: LibrettoWorkflowHandler<Input, Output, S>,
34
120
  ): LibrettoWorkflow<Input, Output, S> {
35
- return new LibrettoWorkflow(handler);
121
+ return new LibrettoWorkflow(name, handler);
36
122
  }
@@ -1,97 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- usage() {
5
- cat <<'EOF'
6
- Usage: scripts/prepare-release.sh [patch|minor|major]
7
-
8
- Creates a release PR branch from main, bumps package.json, pushes the branch,
9
- and opens a pull request targeting main.
10
- EOF
11
- }
12
-
13
- bump="${1:-patch}"
14
-
15
- case "$bump" in
16
- patch|minor|major)
17
- ;;
18
- -h|--help|help)
19
- usage
20
- exit 0
21
- ;;
22
- *)
23
- echo "Invalid bump type: $bump" >&2
24
- usage >&2
25
- exit 1
26
- ;;
27
- esac
28
-
29
- if ! command -v gh >/dev/null 2>&1; then
30
- echo "gh CLI is required." >&2
31
- exit 1
32
- fi
33
-
34
- if [ -n "$(git status --porcelain)" ]; then
35
- echo "Working tree must be clean before preparing a release." >&2
36
- exit 1
37
- fi
38
-
39
- current_branch="$(git branch --show-current)"
40
- if [ "$current_branch" != "main" ]; then
41
- echo "Switching from $current_branch to main."
42
- fi
43
-
44
- git fetch origin
45
- git checkout main
46
- git pull --ff-only origin main
47
-
48
- pnpm install --frozen-lockfile
49
- pnpm type-check
50
- pnpm test
51
-
52
- current_version="$(node -p "require('./package.json').version")"
53
- next_version="$(node -e '
54
- const [major, minor, patch] = process.argv[1].split(".").map(Number)
55
- const bump = process.argv[2]
56
-
57
- let next
58
- if (bump === "major") next = [major + 1, 0, 0]
59
- else if (bump === "minor") next = [major, minor + 1, 0]
60
- else next = [major, minor, patch + 1]
61
-
62
- process.stdout.write(next.join("."))
63
- ' "$current_version" "$bump")"
64
- branch_name="tk-release-v${next_version}"
65
-
66
- if git show-ref --verify --quiet "refs/heads/${branch_name}"; then
67
- echo "Local branch ${branch_name} already exists." >&2
68
- exit 1
69
- fi
70
-
71
- if git ls-remote --exit-code --heads origin "${branch_name}" >/dev/null 2>&1; then
72
- echo "Remote branch ${branch_name} already exists." >&2
73
- exit 1
74
- fi
75
-
76
- npm version "$next_version" --no-git-tag-version >/dev/null
77
-
78
- git checkout -b "$branch_name"
79
- git add package.json
80
- git commit -m "release: v${next_version}"
81
- git push -u origin "$branch_name"
82
-
83
- gh pr create \
84
- --base main \
85
- --head "$branch_name" \
86
- --title "release: v${next_version}" \
87
- --body "$(cat <<EOF
88
- ## Summary
89
-
90
- - release libretto v${next_version}
91
-
92
- ## Verification
93
-
94
- - pnpm type-check
95
- - pnpm test
96
- EOF
97
- )"