libretto 0.5.2 → 0.5.3-experimental.0
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.
- package/README.md +3 -2
- package/dist/cli/commands/execution.js +38 -12
- package/dist/cli/framework/simple-cli.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +18 -41
- package/dist/cli/workers/run-integration-worker-protocol.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -0
- package/dist/shared/workflow/workflow.d.ts +14 -3
- package/dist/shared/workflow/workflow.js +50 -3
- package/package.json +7 -4
- package/scripts/check-skills-sync.mjs +1 -1
- package/scripts/generate-changelog.ts +132 -0
- package/scripts/skills-libretto.mjs +1 -1
- package/scripts/sync-skills.mjs +1 -1
- package/skills/libretto/SKILL.md +4 -2
- package/skills/libretto/references/code-generation-rules.md +6 -4
- package/src/cli/commands/execution.ts +39 -11
- package/src/cli/framework/simple-cli.ts +5 -0
- package/src/cli/workers/run-integration-runtime.ts +24 -52
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
- package/src/index.ts +4 -0
- package/src/shared/workflow/workflow.ts +88 -2
- package/scripts/prepare-release.sh +0 -97
package/README.md
CHANGED
|
@@ -63,8 +63,9 @@ You can also use Libretto directly from the command line. All commands accept `-
|
|
|
63
63
|
npx libretto init # interactive; run yourself, not through an agent
|
|
64
64
|
npx libretto open <url> # launch browser and open a URL (headed by default)
|
|
65
65
|
npx libretto snapshot --objective "..." --context "..." # capture PNG + HTML and analyze with an LLM
|
|
66
|
-
npx libretto exec "<code>" # execute Playwright TypeScript against the open page
|
|
67
|
-
npx libretto
|
|
66
|
+
npx libretto exec "<code>" # execute Playwright TypeScript against the open page (single quoted argument)
|
|
67
|
+
echo "<code>" | npx libretto exec - # intentionally read Playwright TypeScript from stdin
|
|
68
|
+
npx libretto run <file> <workflowName> # run an exported workflow from a file
|
|
68
69
|
npx libretto resume # resume a paused workflow
|
|
69
70
|
npx libretto network # view captured network requests
|
|
70
71
|
npx libretto actions # view captured user/agent actions
|
|
@@ -445,9 +445,10 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
445
445
|
);
|
|
446
446
|
const payload = JSON.stringify({
|
|
447
447
|
integrationPath: args.integrationPath,
|
|
448
|
-
|
|
448
|
+
workflowName: args.workflowName,
|
|
449
449
|
session: args.session,
|
|
450
450
|
params: args.params,
|
|
451
|
+
credentials: args.credentials,
|
|
451
452
|
headless: args.headless,
|
|
452
453
|
visualize: args.visualize,
|
|
453
454
|
authProfileDomain: args.authProfileDomain,
|
|
@@ -496,11 +497,19 @@ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-ru
|
|
|
496
497
|
setSessionStatus(args.session, "completed", logger);
|
|
497
498
|
console.log("Integration completed.");
|
|
498
499
|
}
|
|
500
|
+
function readStdinSync() {
|
|
501
|
+
if (process.stdin.isTTY === true) return null;
|
|
502
|
+
try {
|
|
503
|
+
const content = readFileSync(0, "utf8");
|
|
504
|
+
return content.trim().length > 0 ? content : null;
|
|
505
|
+
} catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
499
509
|
const execInput = SimpleCLI.input({
|
|
500
510
|
positionals: [
|
|
501
|
-
SimpleCLI.positional("
|
|
502
|
-
help: "Playwright TypeScript code to execute"
|
|
503
|
-
variadic: true
|
|
511
|
+
SimpleCLI.positional("code", z.string().optional(), {
|
|
512
|
+
help: "Playwright TypeScript code to execute"
|
|
504
513
|
})
|
|
505
514
|
],
|
|
506
515
|
named: {
|
|
@@ -511,28 +520,36 @@ const execInput = SimpleCLI.input({
|
|
|
511
520
|
page: pageOption()
|
|
512
521
|
}
|
|
513
522
|
}).refine(
|
|
514
|
-
(input) => input.
|
|
515
|
-
`Usage: libretto exec <code
|
|
523
|
+
(input) => input.code !== void 0,
|
|
524
|
+
`Usage: libretto exec <code|-> [--session <name>] [--visualize]
|
|
525
|
+
echo '<code>' | libretto exec - [--session <name>] [--visualize]`
|
|
516
526
|
);
|
|
517
527
|
const execCommand = SimpleCLI.command({
|
|
518
528
|
description: "Execute Playwright TypeScript code"
|
|
519
529
|
}).input(execInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
|
|
530
|
+
const code = input.code;
|
|
531
|
+
const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
|
|
532
|
+
if (codeFromArgsOrStdin === null) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
"Missing stdin input for `exec -`. Pipe Playwright code into stdin."
|
|
535
|
+
);
|
|
536
|
+
}
|
|
520
537
|
await runExec(
|
|
521
|
-
|
|
538
|
+
codeFromArgsOrStdin,
|
|
522
539
|
ctx.session,
|
|
523
540
|
ctx.logger,
|
|
524
541
|
input.visualize,
|
|
525
542
|
input.page
|
|
526
543
|
);
|
|
527
544
|
});
|
|
528
|
-
const runUsage = `Usage: libretto run <integrationFile> <
|
|
545
|
+
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--credentials <json>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
529
546
|
const runInput = SimpleCLI.input({
|
|
530
547
|
positionals: [
|
|
531
548
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
532
549
|
help: "Path to the integration file"
|
|
533
550
|
}),
|
|
534
|
-
SimpleCLI.positional("
|
|
535
|
-
help: "
|
|
551
|
+
SimpleCLI.positional("workflowName", z.string().optional(), {
|
|
552
|
+
help: "Workflow name to run (from workflow(name, handler))"
|
|
536
553
|
})
|
|
537
554
|
],
|
|
538
555
|
named: {
|
|
@@ -544,6 +561,9 @@ const runInput = SimpleCLI.input({
|
|
|
544
561
|
name: "params-file",
|
|
545
562
|
help: "Path to a JSON params file"
|
|
546
563
|
}),
|
|
564
|
+
credentials: SimpleCLI.option(z.string().optional(), {
|
|
565
|
+
help: "Inline JSON credentials passed to ctx.credentials"
|
|
566
|
+
}),
|
|
547
567
|
tsconfig: SimpleCLI.option(z.string().optional(), {
|
|
548
568
|
help: "Path to a tsconfig used for workflow module resolution"
|
|
549
569
|
}),
|
|
@@ -562,7 +582,7 @@ const runInput = SimpleCLI.input({
|
|
|
562
582
|
})
|
|
563
583
|
}
|
|
564
584
|
}).refine(
|
|
565
|
-
(input) => Boolean(input.integrationFile && input.
|
|
585
|
+
(input) => Boolean(input.integrationFile && input.workflowName),
|
|
566
586
|
runUsage
|
|
567
587
|
).refine(
|
|
568
588
|
(input) => !(input.params && input.paramsFile),
|
|
@@ -594,6 +614,11 @@ const runCommand = SimpleCLI.command({
|
|
|
594
614
|
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
595
615
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
596
616
|
const params = resolveRunParams(input.params, input.paramsFile);
|
|
617
|
+
const rawCredentials = input.credentials ? parseJsonArg("--credentials", input.credentials) : void 0;
|
|
618
|
+
if (rawCredentials !== void 0 && (typeof rawCredentials !== "object" || rawCredentials === null || Array.isArray(rawCredentials))) {
|
|
619
|
+
throw new Error(`--credentials must be a JSON object (e.g., '{"key": "value"}').`);
|
|
620
|
+
}
|
|
621
|
+
const credentials = rawCredentials;
|
|
597
622
|
const headlessMode = input.headed ? false : input.headless ? true : void 0;
|
|
598
623
|
const visualize = !input.noVisualize;
|
|
599
624
|
const viewport = resolveViewport(
|
|
@@ -603,9 +628,10 @@ const runCommand = SimpleCLI.command({
|
|
|
603
628
|
await runIntegrationFromFile(
|
|
604
629
|
{
|
|
605
630
|
integrationPath: input.integrationFile,
|
|
606
|
-
|
|
631
|
+
workflowName: input.workflowName,
|
|
607
632
|
session: ctx.session,
|
|
608
633
|
params,
|
|
634
|
+
credentials,
|
|
609
635
|
tsconfigPath: input.tsconfig,
|
|
610
636
|
headless: headlessMode ?? false,
|
|
611
637
|
visualize,
|
|
@@ -270,6 +270,10 @@ class SimpleCLIApp {
|
|
|
270
270
|
named["--"] = args.slice(index + 1);
|
|
271
271
|
break;
|
|
272
272
|
}
|
|
273
|
+
if (arg === "-") {
|
|
274
|
+
positionals.push(arg);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
273
277
|
if (arg.startsWith("--")) {
|
|
274
278
|
const [rawName, inlineValue] = splitNamedArg(arg.slice(2));
|
|
275
279
|
const namedEntry = namedSpecs.get(rawName);
|
|
@@ -4,6 +4,8 @@ import { cwd } from "node:process";
|
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import {
|
|
7
|
+
getWorkflowFromModuleExports,
|
|
8
|
+
getWorkflowsFromModuleExports,
|
|
7
9
|
instrumentContext,
|
|
8
10
|
launchBrowser
|
|
9
11
|
} from "../../index.js";
|
|
@@ -19,7 +21,6 @@ import {
|
|
|
19
21
|
removeSignalIfExists
|
|
20
22
|
} from "../core/pause-signals.js";
|
|
21
23
|
import { installSessionTelemetry } from "../core/session-telemetry.js";
|
|
22
|
-
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
23
24
|
const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
|
|
24
25
|
const TSCONFIG_HINT = "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
|
|
25
26
|
function isTsxCompileError(error) {
|
|
@@ -67,11 +68,6 @@ async function waitForFailureSessionRelease(args) {
|
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
function isLoadedLibrettoWorkflow(value) {
|
|
71
|
-
if (!value || typeof value !== "object") return false;
|
|
72
|
-
const candidate = value;
|
|
73
|
-
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.run === "function";
|
|
74
|
-
}
|
|
75
71
|
function resolveLocalAuthProfilePath(domain) {
|
|
76
72
|
return getProfilePath(normalizeDomain(domain));
|
|
77
73
|
}
|
|
@@ -93,7 +89,7 @@ function getAbsoluteIntegrationPath(integrationPath) {
|
|
|
93
89
|
}
|
|
94
90
|
return absolutePath;
|
|
95
91
|
}
|
|
96
|
-
async function
|
|
92
|
+
async function loadWorkflowByName(absolutePath, workflowName) {
|
|
97
93
|
let loadedModule;
|
|
98
94
|
try {
|
|
99
95
|
loadedModule = await import(pathToFileURL(absolutePath).href);
|
|
@@ -105,37 +101,17 @@ ${TSCONFIG_HINT}` : "";
|
|
|
105
101
|
`Failed to import integration module at ${absolutePath}: ${message}${compileHint}`
|
|
106
102
|
);
|
|
107
103
|
}
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
const detail = availableExports.length > 0 ? ` Available exports: ${availableExports.join(", ")}` : " The module has no exports.";
|
|
112
|
-
throw new Error(
|
|
113
|
-
`Export "${exportName}" was not found in ${absolutePath}.${detail}`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
if (!isLoadedLibrettoWorkflow(targetExport)) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
[
|
|
119
|
-
`Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
|
|
120
|
-
"",
|
|
121
|
-
'A workflow must be created using the workflow() function from "libretto":',
|
|
122
|
-
"",
|
|
123
|
-
' import { workflow } from "libretto";',
|
|
124
|
-
"",
|
|
125
|
-
` export const ${exportName} = workflow<InputType, OutputType>(`,
|
|
126
|
-
" async (ctx, input) => {",
|
|
127
|
-
" // ctx.session \u2014 libretto session name",
|
|
128
|
-
" // ctx.page \u2014 Playwright Page instance",
|
|
129
|
-
" // ctx.logger \u2014 MinimalLogger",
|
|
130
|
-
" // ctx.services \u2014 injected dependencies (generic, default {})",
|
|
131
|
-
" // input \u2014 JSON-serializable input matching InputType",
|
|
132
|
-
" return output; // must match OutputType",
|
|
133
|
-
" },",
|
|
134
|
-
" );"
|
|
135
|
-
].join("\n")
|
|
136
|
-
);
|
|
104
|
+
const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
|
|
105
|
+
if (workflow) {
|
|
106
|
+
return workflow;
|
|
137
107
|
}
|
|
138
|
-
|
|
108
|
+
const availableWorkflows = getWorkflowsFromModuleExports(loadedModule).map(
|
|
109
|
+
(candidate) => candidate.name
|
|
110
|
+
);
|
|
111
|
+
const detail = availableWorkflows.length > 0 ? ` Available workflows: ${availableWorkflows.join(", ")}` : ' No workflows found in this file. Export a workflow() instance from "libretto" directly or via `export const workflows = { ... }`.';
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Workflow "${workflowName}" not found in ${absolutePath}.${detail}`
|
|
114
|
+
);
|
|
139
115
|
}
|
|
140
116
|
async function installHeadedWorkflowVisualization(args) {
|
|
141
117
|
await (args.instrument ?? instrumentContext)(args.context, {
|
|
@@ -146,7 +122,7 @@ async function installHeadedWorkflowVisualization(args) {
|
|
|
146
122
|
async function runIntegrationInternal(args, options) {
|
|
147
123
|
const { logger } = options;
|
|
148
124
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
149
|
-
const workflow = await
|
|
125
|
+
const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
|
|
150
126
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
151
127
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
152
128
|
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -154,11 +130,11 @@ async function runIntegrationInternal(args, options) {
|
|
|
154
130
|
await removeSignalIfExists(signalPaths.failedSignalPath);
|
|
155
131
|
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
156
132
|
console.log(
|
|
157
|
-
`Running
|
|
133
|
+
`Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
|
|
158
134
|
);
|
|
159
135
|
const integrationLogger = logger.withScope("integration-run", {
|
|
160
136
|
integrationPath: absolutePath,
|
|
161
|
-
|
|
137
|
+
workflowName: args.workflowName,
|
|
162
138
|
session: args.session
|
|
163
139
|
});
|
|
164
140
|
const authProfileDomain = args.authProfileDomain;
|
|
@@ -201,7 +177,8 @@ async function runIntegrationInternal(args, options) {
|
|
|
201
177
|
session: args.session,
|
|
202
178
|
logger: integrationLogger,
|
|
203
179
|
page: browserSession.page,
|
|
204
|
-
services: {}
|
|
180
|
+
services: {},
|
|
181
|
+
credentials: args.credentials
|
|
205
182
|
};
|
|
206
183
|
try {
|
|
207
184
|
try {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
const RunIntegrationWorkerRequestSchema = z.object({
|
|
3
3
|
integrationPath: z.string().min(1),
|
|
4
|
-
|
|
4
|
+
workflowName: z.string().min(1),
|
|
5
5
|
session: z.string().min(1),
|
|
6
6
|
params: z.unknown(),
|
|
7
|
+
credentials: z.record(z.string(), z.unknown()).optional(),
|
|
7
8
|
headless: z.boolean(),
|
|
8
9
|
visualize: z.boolean().default(true),
|
|
9
10
|
authProfileDomain: z.string().optional(),
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { InstrumentationOptions, InstrumentedPage, installInstrumentation, instr
|
|
|
14
14
|
export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, moveGhostCursor } from './shared/visualization/ghost-cursor.js';
|
|
15
15
|
export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
|
|
16
16
|
export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
|
|
17
|
-
export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, workflow } from './shared/workflow/workflow.js';
|
|
17
|
+
export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow } from './shared/workflow/workflow.js';
|
|
18
18
|
import 'zod';
|
|
19
19
|
import 'ai';
|
|
20
20
|
import 'playwright';
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,9 @@ import {
|
|
|
54
54
|
launchBrowser
|
|
55
55
|
} from "./shared/run/api.js";
|
|
56
56
|
import {
|
|
57
|
+
getWorkflowFromModuleExports,
|
|
58
|
+
getWorkflowsFromModuleExports,
|
|
59
|
+
isLibrettoWorkflow,
|
|
57
60
|
LibrettoWorkflow,
|
|
58
61
|
LIBRETTO_WORKFLOW_BRAND,
|
|
59
62
|
workflow
|
|
@@ -92,11 +95,14 @@ export {
|
|
|
92
95
|
ensureHighlightLayer,
|
|
93
96
|
executeRecoveryAgent,
|
|
94
97
|
extractFromPage,
|
|
98
|
+
getWorkflowFromModuleExports,
|
|
99
|
+
getWorkflowsFromModuleExports,
|
|
95
100
|
ghostClick,
|
|
96
101
|
hideGhostCursor,
|
|
97
102
|
installInstrumentation,
|
|
98
103
|
instrumentContext,
|
|
99
104
|
instrumentPage,
|
|
105
|
+
isLibrettoWorkflow,
|
|
100
106
|
jsonlConsoleSink,
|
|
101
107
|
launchBrowser,
|
|
102
108
|
moveGhostCursor,
|
|
@@ -7,14 +7,25 @@ type LibrettoWorkflowContext<S = {}> = {
|
|
|
7
7
|
page: Page;
|
|
8
8
|
logger: MinimalLogger;
|
|
9
9
|
services: S;
|
|
10
|
+
credentials?: Record<string, unknown>;
|
|
10
11
|
};
|
|
11
12
|
type LibrettoWorkflowHandler<Input = unknown, Output = unknown, S = {}> = (ctx: LibrettoWorkflowContext<S>, input: Input) => Promise<Output>;
|
|
12
13
|
declare class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
|
|
13
14
|
readonly [LIBRETTO_WORKFLOW_BRAND] = true;
|
|
15
|
+
readonly name: string;
|
|
14
16
|
private readonly handler;
|
|
15
|
-
constructor(handler: LibrettoWorkflowHandler<Input, Output, S>);
|
|
17
|
+
constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output, S>);
|
|
16
18
|
run(ctx: LibrettoWorkflowContext<S>, input: Input): Promise<Output>;
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
type ExportedLibrettoWorkflow = {
|
|
21
|
+
readonly [LIBRETTO_WORKFLOW_BRAND]: true;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
type WorkflowModuleExports = Record<string, unknown>;
|
|
26
|
+
declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWorkflow;
|
|
27
|
+
declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
|
|
28
|
+
declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
|
|
29
|
+
declare function workflow<Input = unknown, Output = unknown, S = {}>(name: string, handler: LibrettoWorkflowHandler<Input, Output, S>): LibrettoWorkflow<Input, Output, S>;
|
|
19
30
|
|
|
20
|
-
export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, workflow };
|
|
31
|
+
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
|
|
@@ -1,19 +1,66 @@
|
|
|
1
1
|
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
2
2
|
class LibrettoWorkflow {
|
|
3
3
|
[LIBRETTO_WORKFLOW_BRAND] = true;
|
|
4
|
+
name;
|
|
4
5
|
handler;
|
|
5
|
-
constructor(handler) {
|
|
6
|
+
constructor(name, handler) {
|
|
7
|
+
this.name = name;
|
|
6
8
|
this.handler = handler;
|
|
7
9
|
}
|
|
8
10
|
async run(ctx, input) {
|
|
9
11
|
return this.handler(ctx, input);
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
+
function isLibrettoWorkflow(value) {
|
|
15
|
+
if (!value || typeof value !== "object") return false;
|
|
16
|
+
const candidate = value;
|
|
17
|
+
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.name === "string" && typeof candidate.run === "function";
|
|
18
|
+
}
|
|
19
|
+
function addWorkflowOrThrow(workflowsByName, value) {
|
|
20
|
+
if (!isLibrettoWorkflow(value)) return;
|
|
21
|
+
const existing = workflowsByName.get(value.name);
|
|
22
|
+
if (existing && existing !== value) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Duplicate workflow name: "${value.name}". Each workflow() call must use a unique name.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
workflowsByName.set(value.name, value);
|
|
28
|
+
}
|
|
29
|
+
function getWorkflowsFromModuleExports(moduleExports) {
|
|
30
|
+
const workflowsByName = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const [exportName, value] of Object.entries(moduleExports)) {
|
|
32
|
+
if (exportName === "workflows" && value && typeof value === "object") {
|
|
33
|
+
if (isLibrettoWorkflow(value)) {
|
|
34
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
35
|
+
} else {
|
|
36
|
+
for (const nestedValue of Object.values(
|
|
37
|
+
value
|
|
38
|
+
)) {
|
|
39
|
+
addWorkflowOrThrow(workflowsByName, nestedValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
45
|
+
}
|
|
46
|
+
return [...workflowsByName.values()];
|
|
47
|
+
}
|
|
48
|
+
function getWorkflowFromModuleExports(moduleExports, workflowName) {
|
|
49
|
+
for (const workflow2 of getWorkflowsFromModuleExports(moduleExports)) {
|
|
50
|
+
if (workflow2.name === workflowName) {
|
|
51
|
+
return workflow2;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function workflow(name, handler) {
|
|
57
|
+
return new LibrettoWorkflow(name, handler);
|
|
14
58
|
}
|
|
15
59
|
export {
|
|
16
60
|
LIBRETTO_WORKFLOW_BRAND,
|
|
17
61
|
LibrettoWorkflow,
|
|
62
|
+
getWorkflowFromModuleExports,
|
|
63
|
+
getWorkflowsFromModuleExports,
|
|
64
|
+
isLibrettoWorkflow,
|
|
18
65
|
workflow
|
|
19
66
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3-experimental.0",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,11 +36,9 @@
|
|
|
36
36
|
"build": "tsup --config tsup.config.ts",
|
|
37
37
|
"type-check": "tsc --noEmit",
|
|
38
38
|
"test": "pnpm run build && vitest run",
|
|
39
|
-
"eval": "pnpm run build && vitest run --config vitest.evals.config.ts",
|
|
40
|
-
"benchmark": "pnpm run build && tsx benchmarks/run.ts",
|
|
41
39
|
"test:watch": "vitest",
|
|
42
40
|
"cli": "node dist/index.js",
|
|
43
|
-
"
|
|
41
|
+
"generate-changelog": "tsx scripts/generate-changelog.ts",
|
|
44
42
|
"prepack": "pnpm run build"
|
|
45
43
|
},
|
|
46
44
|
"peerDependencies": {
|
|
@@ -69,8 +67,13 @@
|
|
|
69
67
|
"@ai-sdk/google-vertex": "^4.0.80",
|
|
70
68
|
"@ai-sdk/openai": "^3.0.41",
|
|
71
69
|
"@anthropic-ai/claude-agent-sdk": "^0.2.75",
|
|
70
|
+
"@mariozechner/pi-agent-core": "^0.62.0",
|
|
71
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
72
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
73
|
+
"@sinclair/typebox": "^0.34.48",
|
|
72
74
|
"@types/node": "^25.5.0",
|
|
73
75
|
"glimpseui": "^0.5.1",
|
|
76
|
+
"google-auth-library": "^10.6.1",
|
|
74
77
|
"openai": "^6.29.0",
|
|
75
78
|
"tsup": "^8.5.1",
|
|
76
79
|
"typescript": "^5.9.3",
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { compareSkillDirs, SKILL_DIRS } from "./skills-libretto.mjs";
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const repoRoot = join(__dirname, "..");
|
|
9
|
+
const repoRoot = join(__dirname, "..", "..", "..");
|
|
10
10
|
const result = compareSkillDirs(repoRoot);
|
|
11
11
|
|
|
12
12
|
if (result.ok) {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { Agent, type AgentTool, type AgentEvent } from "@mariozechner/pi-agent-core";
|
|
3
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
|
|
6
|
+
const tag = process.argv[2];
|
|
7
|
+
if (!tag) {
|
|
8
|
+
console.error("Usage: generate-changelog.ts <tag>");
|
|
9
|
+
console.error("Example: generate-changelog.ts v0.5.2");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
14
|
+
console.error("ANTHROPIC_API_KEY is required.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ALLOWED_GH_SUBCOMMANDS = new Set(["pr", "release", "repo", "issue"]);
|
|
19
|
+
const ALLOWED_ACTIONS = new Set(["list", "view", "diff", "status", "checks"]);
|
|
20
|
+
|
|
21
|
+
const ghTool: AgentTool = {
|
|
22
|
+
name: "gh",
|
|
23
|
+
label: "GitHub CLI",
|
|
24
|
+
description: [
|
|
25
|
+
"Run a read-only GitHub CLI command. The arguments are passed directly to `gh`.",
|
|
26
|
+
"Examples: 'release list --limit 5', 'pr list --state merged --json number,title',",
|
|
27
|
+
"'pr view 128 --json title,body,files', 'pr diff 128'.",
|
|
28
|
+
"Only read operations are allowed (list, view, diff, etc.). Mutating commands will be rejected.",
|
|
29
|
+
].join(" "),
|
|
30
|
+
parameters: Type.Object({
|
|
31
|
+
args: Type.String({ description: "Arguments to pass to gh (without the leading 'gh')" }),
|
|
32
|
+
}),
|
|
33
|
+
execute: async (_toolCallId, rawParams) => {
|
|
34
|
+
const params = rawParams as { args: string };
|
|
35
|
+
const args = params.args.trim();
|
|
36
|
+
const parts = args.split(/\s+/);
|
|
37
|
+
const subcommand = parts[0];
|
|
38
|
+
|
|
39
|
+
if (!subcommand || !ALLOWED_GH_SUBCOMMANDS.has(subcommand)) {
|
|
40
|
+
throw new Error(`Subcommand '${subcommand}' is not allowed. Allowed: ${[...ALLOWED_GH_SUBCOMMANDS].join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const action = parts[1];
|
|
44
|
+
if (!action || !ALLOWED_ACTIONS.has(action)) {
|
|
45
|
+
throw new Error(`Action '${action}' is not allowed. Allowed: ${[...ALLOWED_ACTIONS].join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const output = execFileSync("gh", parts, {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
timeout: 300_000,
|
|
52
|
+
maxBuffer: 1024 * 1024,
|
|
53
|
+
});
|
|
54
|
+
return { content: [{ type: "text", text: output }], details: {} };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
57
|
+
throw new Error(`gh command failed: ${message}`);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const agent = new Agent({
|
|
63
|
+
initialState: {
|
|
64
|
+
systemPrompt: [
|
|
65
|
+
`Generate release notes for the ${tag} release of Libretto.`,
|
|
66
|
+
"",
|
|
67
|
+
"Use the gh tool to explore what changed since the previous release.",
|
|
68
|
+
"Useful queries:",
|
|
69
|
+
"- 'release list --limit 5' to find the previous release tag",
|
|
70
|
+
"- 'pr list --state merged --limit 50 --json number,title,body,labels' to find merged PRs",
|
|
71
|
+
"- 'pr diff NUMBER' to see the full diff of a PR (base to head, not individual commits)",
|
|
72
|
+
"- 'pr view NUMBER --json title,body,files' to see PR details",
|
|
73
|
+
"",
|
|
74
|
+
"IMPORTANT: Always read the full PR diff to understand what actually changed.",
|
|
75
|
+
"Do NOT rely solely on PR titles and descriptions — they may be incomplete or misleading.",
|
|
76
|
+
"The diff is the source of truth for what the release note should say.",
|
|
77
|
+
"",
|
|
78
|
+
"Guidelines:",
|
|
79
|
+
"- Write concise, user-facing release notes in markdown.",
|
|
80
|
+
"- Group changes into sections like Features, Fixes, and Improvements. Only include sections that have entries.",
|
|
81
|
+
"- Focus on what changed from the user's perspective, not internal implementation details.",
|
|
82
|
+
"- Do NOT include PR numbers or links.",
|
|
83
|
+
"- Skip PRs labeled 'skip-changelog'.",
|
|
84
|
+
"- Your response must contain ONLY the raw markdown release notes. No preamble like 'Here are the release notes'. No commentary or explanation. No '---' separators. The very first character of your response must be '#'. Example format:",
|
|
85
|
+
"",
|
|
86
|
+
"## Features",
|
|
87
|
+
"",
|
|
88
|
+
"- **Thing**: Description",
|
|
89
|
+
].join("\n"),
|
|
90
|
+
model: getModel("anthropic", "claude-sonnet-4-6"),
|
|
91
|
+
tools: [ghTool],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let finalText = "";
|
|
96
|
+
|
|
97
|
+
agent.subscribe((event: AgentEvent) => {
|
|
98
|
+
if (event.type === "agent_end") {
|
|
99
|
+
const messages = event.messages;
|
|
100
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
101
|
+
const msg = messages[i];
|
|
102
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
103
|
+
for (const block of msg.content) {
|
|
104
|
+
if (typeof block === "object" && "type" in block && block.type === "text" && "text" in block) {
|
|
105
|
+
finalText = block.text as string;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await agent.prompt("Generate the release notes now.");
|
|
115
|
+
|
|
116
|
+
if (!finalText) {
|
|
117
|
+
console.error("Changelog generation failed: no text output from agent.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Strip any preamble before the first markdown heading.
|
|
122
|
+
const headingIndex = finalText.indexOf("\n#");
|
|
123
|
+
if (headingIndex >= 0) {
|
|
124
|
+
finalText = finalText.slice(headingIndex + 1);
|
|
125
|
+
} else if (finalText.startsWith("#")) {
|
|
126
|
+
// Already starts with a heading, keep as-is.
|
|
127
|
+
} else {
|
|
128
|
+
console.error("Changelog generation failed: output does not contain markdown headings.");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.stdout.write(finalText);
|
package/scripts/sync-skills.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { SKILL_DIRS, syncRepoSkills } from "./skills-libretto.mjs";
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const repoRoot = join(__dirname, "..");
|
|
9
|
+
const repoRoot = join(__dirname, "..", "..", "..");
|
|
10
10
|
|
|
11
11
|
syncRepoSkills(repoRoot);
|
|
12
12
|
console.log(`libretto: synced skill mirrors across ${SKILL_DIRS.join(", ")}`);
|
package/skills/libretto/SKILL.md
CHANGED
|
@@ -83,6 +83,7 @@ npx libretto snapshot \
|
|
|
83
83
|
|
|
84
84
|
- Use `exec` for focused inspection and short-lived interaction experiments.
|
|
85
85
|
- Use `exec` to validate selectors, inspect data, or prototype a step before you encode it in the workflow file.
|
|
86
|
+
- Use `exec -` to run multi-line scripts from stdin, especially when the code is too long or complex for a command line argument.
|
|
86
87
|
- Available globals: `page`, `context`, `browser`, `state`, `fetch`, `Buffer`.
|
|
87
88
|
- Let failures throw. Do not hide `exec` failures with `try/catch` or `.catch()`.
|
|
88
89
|
- Do not run multiple `exec` commands in parallel.
|
|
@@ -91,6 +92,7 @@ npx libretto snapshot \
|
|
|
91
92
|
npx libretto exec "return await page.url()"
|
|
92
93
|
npx libretto exec "return await page.locator('button').count()"
|
|
93
94
|
npx libretto exec "await page.locator('button:has-text(\"Continue\")').click()"
|
|
95
|
+
echo "return await page.url()" | npx libretto exec - --session debug-example
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
### `pages`
|
|
@@ -113,8 +115,8 @@ npx libretto exec --session debug-example --page <page-id> "return await page.ur
|
|
|
113
115
|
- Re-run the same workflow after each fix to verify the browser behavior end to end.
|
|
114
116
|
|
|
115
117
|
```bash
|
|
116
|
-
npx libretto run ./integration.ts
|
|
117
|
-
npx libretto run ./integration.ts
|
|
118
|
+
npx libretto run ./integration.ts workflowName --headless --params '{"status":"open"}'
|
|
119
|
+
npx libretto run ./integration.ts workflowName --auth-profile app.example.com
|
|
118
120
|
```
|
|
119
121
|
|
|
120
122
|
### `resume`
|
|
@@ -6,7 +6,7 @@ Follow the user's existing codebase conventions, abstractions, and patterns when
|
|
|
6
6
|
|
|
7
7
|
## Workflow File Structure
|
|
8
8
|
|
|
9
|
-
Generated files must export a `workflow()` instance so they can be run via `npx libretto run <file> <
|
|
9
|
+
Generated files must export a `workflow()` instance so they can be run via `npx libretto run <file> <workflowName>`. Import `workflow` and its types from `"libretto"`:
|
|
10
10
|
|
|
11
11
|
```typescript
|
|
12
12
|
import { workflow, pause, type LibrettoWorkflowContext } from "libretto";
|
|
@@ -23,7 +23,8 @@ type Output = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export const myWorkflow = workflow<Input, Output>(
|
|
26
|
-
|
|
26
|
+
"myWorkflow",
|
|
27
|
+
async (ctx: LibrettoWorkflowContext, input): Promise<Output> => {
|
|
27
28
|
const { session, page, logger } = ctx;
|
|
28
29
|
|
|
29
30
|
logger.info("workflow-start", { session, query: input.query });
|
|
@@ -37,8 +38,8 @@ export const myWorkflow = workflow<Input, Output>(
|
|
|
37
38
|
|
|
38
39
|
Key points:
|
|
39
40
|
|
|
40
|
-
-
|
|
41
|
-
- `
|
|
41
|
+
- `workflow(name, handler)` takes a unique workflow name and returns the workflow object that Libretto can run.
|
|
42
|
+
- `npx libretto run ./file.ts myWorkflow` resolves `myWorkflow` from the workflows exported by `./file.ts`, so export or re-export the workflow from that file directly or through a `workflows` object, and make sure the run argument matches the name passed to `workflow("myWorkflow", ...)`.
|
|
42
43
|
- `ctx` provides `session`, `page`, `logger`, and `services` (generic, default `{}`)
|
|
43
44
|
- `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI
|
|
44
45
|
- Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
|
|
@@ -57,6 +58,7 @@ import { type Transaction } from "./db";
|
|
|
57
58
|
type MyServices = { tx?: Transaction };
|
|
58
59
|
|
|
59
60
|
export const myWorkflow = workflow<Input, Output, MyServices>(
|
|
61
|
+
"myWorkflow",
|
|
60
62
|
async (ctx, input) => {
|
|
61
63
|
if (ctx.services.tx) {
|
|
62
64
|
await ctx.services.tx.insert(/* ... */);
|
|
@@ -605,9 +605,10 @@ async function runIntegrationFromFile(
|
|
|
605
605
|
);
|
|
606
606
|
const payload = JSON.stringify({
|
|
607
607
|
integrationPath: args.integrationPath,
|
|
608
|
-
|
|
608
|
+
workflowName: args.workflowName,
|
|
609
609
|
session: args.session,
|
|
610
610
|
params: args.params,
|
|
611
|
+
credentials: args.credentials,
|
|
611
612
|
headless: args.headless,
|
|
612
613
|
visualize: args.visualize,
|
|
613
614
|
authProfileDomain: args.authProfileDomain,
|
|
@@ -656,11 +657,20 @@ async function runIntegrationFromFile(
|
|
|
656
657
|
console.log("Integration completed.");
|
|
657
658
|
}
|
|
658
659
|
|
|
660
|
+
function readStdinSync(): string | null {
|
|
661
|
+
if (process.stdin.isTTY === true) return null;
|
|
662
|
+
try {
|
|
663
|
+
const content = readFileSync(0, "utf8");
|
|
664
|
+
return content.trim().length > 0 ? content : null;
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
659
670
|
export const execInput = SimpleCLI.input({
|
|
660
671
|
positionals: [
|
|
661
|
-
SimpleCLI.positional("
|
|
672
|
+
SimpleCLI.positional("code", z.string().optional(), {
|
|
662
673
|
help: "Playwright TypeScript code to execute",
|
|
663
|
-
variadic: true,
|
|
664
674
|
}),
|
|
665
675
|
],
|
|
666
676
|
named: {
|
|
@@ -671,8 +681,8 @@ export const execInput = SimpleCLI.input({
|
|
|
671
681
|
page: pageOption(),
|
|
672
682
|
},
|
|
673
683
|
}).refine(
|
|
674
|
-
(input) => input.
|
|
675
|
-
`Usage: libretto exec <code> [--session <name>] [--visualize]`,
|
|
684
|
+
(input) => input.code !== undefined,
|
|
685
|
+
`Usage: libretto exec <code|-> [--session <name>] [--visualize]\n echo '<code>' | libretto exec - [--session <name>] [--visualize]`,
|
|
676
686
|
);
|
|
677
687
|
|
|
678
688
|
export const execCommand = SimpleCLI.command({
|
|
@@ -681,8 +691,15 @@ export const execCommand = SimpleCLI.command({
|
|
|
681
691
|
.input(execInput)
|
|
682
692
|
.use(withRequiredSession())
|
|
683
693
|
.handle(async ({ input, ctx }) => {
|
|
694
|
+
const code = input.code!;
|
|
695
|
+
const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
|
|
696
|
+
if (codeFromArgsOrStdin === null) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
"Missing stdin input for `exec -`. Pipe Playwright code into stdin.",
|
|
699
|
+
);
|
|
700
|
+
}
|
|
684
701
|
await runExec(
|
|
685
|
-
|
|
702
|
+
codeFromArgsOrStdin,
|
|
686
703
|
ctx.session,
|
|
687
704
|
ctx.logger,
|
|
688
705
|
input.visualize,
|
|
@@ -690,15 +707,15 @@ export const execCommand = SimpleCLI.command({
|
|
|
690
707
|
);
|
|
691
708
|
});
|
|
692
709
|
|
|
693
|
-
const runUsage = `Usage: libretto run <integrationFile> <
|
|
710
|
+
const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--credentials <json>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
|
|
694
711
|
|
|
695
712
|
export const runInput = SimpleCLI.input({
|
|
696
713
|
positionals: [
|
|
697
714
|
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
698
715
|
help: "Path to the integration file",
|
|
699
716
|
}),
|
|
700
|
-
SimpleCLI.positional("
|
|
701
|
-
help: "
|
|
717
|
+
SimpleCLI.positional("workflowName", z.string().optional(), {
|
|
718
|
+
help: "Workflow name to run (from workflow(name, handler))",
|
|
702
719
|
}),
|
|
703
720
|
],
|
|
704
721
|
named: {
|
|
@@ -710,6 +727,9 @@ export const runInput = SimpleCLI.input({
|
|
|
710
727
|
name: "params-file",
|
|
711
728
|
help: "Path to a JSON params file",
|
|
712
729
|
}),
|
|
730
|
+
credentials: SimpleCLI.option(z.string().optional(), {
|
|
731
|
+
help: "Inline JSON credentials passed to ctx.credentials",
|
|
732
|
+
}),
|
|
713
733
|
tsconfig: SimpleCLI.option(z.string().optional(), {
|
|
714
734
|
help: "Path to a tsconfig used for workflow module resolution",
|
|
715
735
|
}),
|
|
@@ -729,7 +749,7 @@ export const runInput = SimpleCLI.input({
|
|
|
729
749
|
},
|
|
730
750
|
})
|
|
731
751
|
.refine(
|
|
732
|
-
(input) => Boolean(input.integrationFile && input.
|
|
752
|
+
(input) => Boolean(input.integrationFile && input.workflowName),
|
|
733
753
|
runUsage,
|
|
734
754
|
)
|
|
735
755
|
.refine(
|
|
@@ -772,6 +792,13 @@ export const runCommand = SimpleCLI.command({
|
|
|
772
792
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
773
793
|
|
|
774
794
|
const params = resolveRunParams(input.params, input.paramsFile);
|
|
795
|
+
const rawCredentials = input.credentials
|
|
796
|
+
? parseJsonArg("--credentials", input.credentials)
|
|
797
|
+
: undefined;
|
|
798
|
+
if (rawCredentials !== undefined && (typeof rawCredentials !== "object" || rawCredentials === null || Array.isArray(rawCredentials))) {
|
|
799
|
+
throw new Error("--credentials must be a JSON object (e.g., '{\"key\": \"value\"}').");
|
|
800
|
+
}
|
|
801
|
+
const credentials = rawCredentials as Record<string, unknown> | undefined;
|
|
775
802
|
const headlessMode = input.headed
|
|
776
803
|
? false
|
|
777
804
|
: input.headless
|
|
@@ -786,9 +813,10 @@ export const runCommand = SimpleCLI.command({
|
|
|
786
813
|
await runIntegrationFromFile(
|
|
787
814
|
{
|
|
788
815
|
integrationPath: input.integrationFile!,
|
|
789
|
-
|
|
816
|
+
workflowName: input.workflowName!,
|
|
790
817
|
session: ctx.session,
|
|
791
818
|
params,
|
|
819
|
+
credentials,
|
|
792
820
|
tsconfigPath: input.tsconfig,
|
|
793
821
|
headless: headlessMode ?? false,
|
|
794
822
|
visualize,
|
|
@@ -579,6 +579,11 @@ export class SimpleCLIApp {
|
|
|
579
579
|
break;
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
if (arg === "-") {
|
|
583
|
+
positionals.push(arg);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
582
587
|
if (arg.startsWith("--")) {
|
|
583
588
|
const [rawName, inlineValue] = splitNamedArg(arg.slice(2));
|
|
584
589
|
const namedEntry = namedSpecs.get(rawName);
|
|
@@ -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
|
-
|
|
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
|
|
142
|
+
async function loadWorkflowByName(
|
|
153
143
|
absolutePath: string,
|
|
154
|
-
|
|
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
|
|
171
|
-
if (
|
|
172
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
)"
|