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.
@@ -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);
@@ -11,7 +11,7 @@ import {
11
11
  import { relative, resolve, join } from "node:path";
12
12
 
13
13
  export const SKILL_DIRS = [
14
- "skills/libretto",
14
+ "packages/libretto/skills/libretto",
15
15
  ".agents/skills/libretto",
16
16
  ".claude/skills/libretto",
17
17
  ];
@@ -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(", ")}`);
@@ -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 main --headless --params '{"status":"open"}'
117
- npx libretto run ./integration.ts main --auth-profile app.example.com
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> <exportName>`. Import `workflow` and its types from `"libretto"`:
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
- async (ctx, input): Promise<Output> => {
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
- - The named export (e.g., `myWorkflow`) is what you pass as the second arg to `npx libretto run ./file.ts myWorkflow`
41
- - `workflow(handler)` returns a branded workflow object with a `.run(ctx, input)` method. The CLI expects that contract.
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
- exportName: args.exportName,
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("codeParts", z.array(z.string()).default([]), {
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.codeParts.length > 0,
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
- input.codeParts.join(" "),
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> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
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("integrationExport", z.string().optional(), {
701
- help: "Named workflow export to run",
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.integrationExport),
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
- exportName: input.integrationExport!,
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() {