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.
- package/README.md +3 -2
- package/dist/cli/commands/deploy.js +162 -0
- package/dist/cli/commands/execution.js +38 -12
- package/dist/cli/framework/simple-cli.js +6 -0
- package/dist/cli/router.js +3 -1
- 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/deploy.ts +209 -0
- package/src/cli/commands/execution.ts +39 -11
- package/src/cli/framework/simple-cli.ts +9 -0
- package/src/cli/router.ts +2 -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
|
@@ -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(/* ... */);
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
12
|
+
|
|
13
|
+
type DeploymentStatus = "building" | "ready" | "failed";
|
|
14
|
+
|
|
15
|
+
type DeploymentResponse = {
|
|
16
|
+
json: {
|
|
17
|
+
deployment_id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
version: number;
|
|
20
|
+
status: DeploymentStatus;
|
|
21
|
+
workflows?: string[] | null;
|
|
22
|
+
build_error?: string | null;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function getConfig() {
|
|
27
|
+
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
28
|
+
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
29
|
+
|
|
30
|
+
if (!apiUrl) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"LIBRETTO_API_URL environment variable is required.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"LIBRETTO_API_KEY environment variable is required.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function postJson(
|
|
45
|
+
apiUrl: string,
|
|
46
|
+
apiKey: string,
|
|
47
|
+
path: string,
|
|
48
|
+
input: Record<string, unknown> = {},
|
|
49
|
+
): Promise<Response> {
|
|
50
|
+
return fetch(`${apiUrl}${path}`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"x-api-key": apiKey,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({ json: input }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildSourceTarball(sourceDir: string): string {
|
|
61
|
+
const absSourceDir = resolve(sourceDir);
|
|
62
|
+
|
|
63
|
+
const pkgJsonPath = join(absSourceDir, "package.json");
|
|
64
|
+
try {
|
|
65
|
+
readFileSync(pkgJsonPath, "utf8");
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`No package.json found in ${absSourceDir}. Deploy source must contain a package.json.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const dir = join(tmpdir(), `libretto-deploy-${Date.now()}`);
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
cpSync(absSourceDir, dir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const tarPath = join(dir, "source.tar.gz");
|
|
78
|
+
execSync(
|
|
79
|
+
`tar czf "${tarPath}" --exclude=source.tar.gz --exclude=node_modules --exclude=.git -C "${dir}" .`,
|
|
80
|
+
);
|
|
81
|
+
return readFileSync(tarPath).toString("base64");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function pollDeployment(
|
|
85
|
+
apiUrl: string,
|
|
86
|
+
apiKey: string,
|
|
87
|
+
deploymentId: string,
|
|
88
|
+
pollIntervalMs: number,
|
|
89
|
+
maxWaitMs: number,
|
|
90
|
+
): Promise<DeploymentResponse["json"]> {
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
let status: DeploymentStatus = "building";
|
|
93
|
+
let deployment: DeploymentResponse["json"] | undefined;
|
|
94
|
+
|
|
95
|
+
while (status === "building" && Date.now() - start < maxWaitMs) {
|
|
96
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
97
|
+
|
|
98
|
+
const res = await postJson(apiUrl, apiKey, "/v1/deployments/get", {
|
|
99
|
+
id: deploymentId,
|
|
100
|
+
});
|
|
101
|
+
const body = (await res.json()) as DeploymentResponse;
|
|
102
|
+
if (res.status !== 200) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Failed to get deployment status (${res.status}): ${JSON.stringify(body)}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
status = body.json.status;
|
|
108
|
+
deployment = body.json;
|
|
109
|
+
process.stdout.write(".");
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
|
|
113
|
+
if (!deployment) {
|
|
114
|
+
throw new Error("Deployment timed out before receiving a status update.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return deployment;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const deployInput = SimpleCLI.input({
|
|
121
|
+
positionals: [
|
|
122
|
+
SimpleCLI.positional("sourceDir", z.string().default("."), {
|
|
123
|
+
help: "Path to source directory (default: current directory)",
|
|
124
|
+
}),
|
|
125
|
+
],
|
|
126
|
+
named: {
|
|
127
|
+
name: SimpleCLI.option(z.string(), {
|
|
128
|
+
help: "Deployment name",
|
|
129
|
+
}),
|
|
130
|
+
description: SimpleCLI.option(z.string().optional(), {
|
|
131
|
+
help: "Deployment description",
|
|
132
|
+
}),
|
|
133
|
+
entryPoint: SimpleCLI.option(z.string().optional(), {
|
|
134
|
+
name: "entry-point",
|
|
135
|
+
help: "Entry point file (default: index.ts)",
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const deployCommand = SimpleCLI.command({
|
|
141
|
+
description: "[experimental] Deploy workflows to the hosted platform",
|
|
142
|
+
experimental: true,
|
|
143
|
+
})
|
|
144
|
+
.input(deployInput)
|
|
145
|
+
.handle(async ({ input }) => {
|
|
146
|
+
const { apiUrl, apiKey } = getConfig();
|
|
147
|
+
|
|
148
|
+
console.log(`Packaging source from ${resolve(input.sourceDir)}...`);
|
|
149
|
+
const source = buildSourceTarball(input.sourceDir);
|
|
150
|
+
|
|
151
|
+
const createPayload: Record<string, unknown> = {
|
|
152
|
+
name: input.name,
|
|
153
|
+
source,
|
|
154
|
+
};
|
|
155
|
+
if (input.description) createPayload.description = input.description;
|
|
156
|
+
if (input.entryPoint) createPayload.entry_point = input.entryPoint;
|
|
157
|
+
|
|
158
|
+
console.log("Uploading deployment...");
|
|
159
|
+
const res = await postJson(
|
|
160
|
+
apiUrl,
|
|
161
|
+
apiKey,
|
|
162
|
+
"/v1/deployments/create",
|
|
163
|
+
createPayload,
|
|
164
|
+
);
|
|
165
|
+
const body = (await res.json()) as DeploymentResponse;
|
|
166
|
+
if (res.status !== 200) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Failed to create deployment (${res.status}): ${JSON.stringify(body)}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const { deployment_id, name, version, status } = body.json;
|
|
173
|
+
console.log(
|
|
174
|
+
`Deployment created: ${name} v${version} (${deployment_id})`,
|
|
175
|
+
);
|
|
176
|
+
console.log(`Status: ${status}`);
|
|
177
|
+
|
|
178
|
+
if (status === "building") {
|
|
179
|
+
process.stdout.write("Waiting for build");
|
|
180
|
+
const deployment = await pollDeployment(
|
|
181
|
+
apiUrl,
|
|
182
|
+
apiKey,
|
|
183
|
+
deployment_id,
|
|
184
|
+
10_000,
|
|
185
|
+
5 * 60 * 1000,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (deployment.status === "failed") {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Build failed: ${deployment.build_error ?? "unknown error"}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (deployment.status === "ready") {
|
|
195
|
+
console.log(`Build complete.`);
|
|
196
|
+
if (deployment.workflows?.length) {
|
|
197
|
+
console.log(
|
|
198
|
+
`Workflows: ${deployment.workflows.join(", ")}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
console.log(
|
|
203
|
+
`Build still in progress (timed out waiting). Check status with deployment ID: ${deployment_id}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return deployment_id;
|
|
209
|
+
});
|
|
@@ -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,
|
|
@@ -4,6 +4,7 @@ type RecordUnknown = Record<string, unknown>;
|
|
|
4
4
|
|
|
5
5
|
export type SimpleCLICommandConfig = {
|
|
6
6
|
description: string;
|
|
7
|
+
experimental?: boolean;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
export type SimpleCLIInputRaw = {
|
|
@@ -111,6 +112,7 @@ export type SimpleCLIResolvedCommand = {
|
|
|
111
112
|
};
|
|
112
113
|
|
|
113
114
|
type InternalResolvedCommand = SimpleCLIResolvedCommand & {
|
|
115
|
+
experimental?: boolean;
|
|
114
116
|
input?: SimpleCLIInput<unknown>;
|
|
115
117
|
middlewares: AnySimpleCLIMiddleware[];
|
|
116
118
|
handler: SimpleCLIHandler<unknown, SimpleCLIContext, unknown>;
|
|
@@ -579,6 +581,11 @@ export class SimpleCLIApp {
|
|
|
579
581
|
break;
|
|
580
582
|
}
|
|
581
583
|
|
|
584
|
+
if (arg === "-") {
|
|
585
|
+
positionals.push(arg);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
582
589
|
if (arg.startsWith("--")) {
|
|
583
590
|
const [rawName, inlineValue] = splitNamedArg(arg.slice(2));
|
|
584
591
|
const namedEntry = namedSpecs.get(rawName);
|
|
@@ -853,6 +860,7 @@ export class SimpleCLIApp {
|
|
|
853
860
|
}
|
|
854
861
|
|
|
855
862
|
const command = this.findCommandByPath(routeEntry.path);
|
|
863
|
+
if (command?.experimental) continue;
|
|
856
864
|
entries.push({
|
|
857
865
|
label: token,
|
|
858
866
|
description: command?.description,
|
|
@@ -1072,6 +1080,7 @@ function resolveRouteTree(
|
|
|
1072
1080
|
routeKey: pathToRouteKey(path),
|
|
1073
1081
|
path,
|
|
1074
1082
|
description: command.config.description,
|
|
1083
|
+
experimental: command.config.experimental,
|
|
1075
1084
|
input: command.input,
|
|
1076
1085
|
middlewares: mergeInheritedMiddlewares(
|
|
1077
1086
|
parentMiddlewares,
|
package/src/cli/router.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { aiCommands } from "./commands/ai.js";
|
|
2
2
|
import { browserCommands } from "./commands/browser.js";
|
|
3
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
3
4
|
import { executionCommands } from "./commands/execution.js";
|
|
4
5
|
import { initCommand } from "./commands/init.js";
|
|
5
6
|
import { logCommands } from "./commands/logs.js";
|
|
@@ -13,6 +14,7 @@ export const cliRoutes = {
|
|
|
13
14
|
ai: aiCommands,
|
|
14
15
|
init: initCommand,
|
|
15
16
|
snapshot: snapshotCommand,
|
|
17
|
+
deploy: deployCommand,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
export function createCLIApp() {
|