libretto 0.5.5 → 0.6.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 +23 -10
- package/README.template.md +23 -10
- package/dist/cli/cli.js +10 -0
- package/dist/cli/commands/ai.js +77 -2
- package/dist/cli/commands/browser.js +98 -8
- package/dist/cli/commands/execution.js +152 -56
- package/dist/cli/commands/setup.js +390 -0
- package/dist/cli/commands/snapshot.js +2 -2
- package/dist/cli/commands/status.js +62 -0
- package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
- package/dist/cli/core/api-snapshot-analyzer.js +7 -5
- package/dist/cli/core/browser.js +202 -36
- package/dist/cli/core/{ai-config.js → config.js} +14 -79
- package/dist/cli/core/context.js +1 -25
- package/dist/cli/core/deploy-artifact.js +121 -61
- package/dist/cli/core/providers/browserbase.js +53 -0
- package/dist/cli/core/providers/index.js +48 -0
- package/dist/cli/core/providers/kernel.js +46 -0
- package/dist/cli/core/providers/libretto-cloud.js +58 -0
- package/dist/cli/core/readonly-exec.js +231 -0
- package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
- package/dist/cli/core/session.js +53 -0
- package/dist/cli/core/skill-version.js +73 -0
- package/dist/cli/core/telemetry.js +1 -54
- package/dist/cli/index.js +1 -7
- package/dist/cli/router.js +4 -4
- package/dist/cli/workers/run-integration-runtime.js +19 -13
- package/dist/cli/workers/run-integration-worker-protocol.js +5 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -2
- package/dist/runtime/extract/extract.d.ts +2 -2
- package/dist/runtime/extract/extract.js +4 -2
- package/dist/runtime/extract/index.d.ts +1 -1
- package/dist/runtime/recovery/agent.d.ts +2 -3
- package/dist/runtime/recovery/agent.js +5 -3
- package/dist/runtime/recovery/errors.d.ts +2 -3
- package/dist/runtime/recovery/errors.js +4 -2
- package/dist/runtime/recovery/index.d.ts +1 -2
- package/dist/runtime/recovery/recovery.d.ts +2 -3
- package/dist/runtime/recovery/recovery.js +3 -3
- package/dist/shared/debug/pause.js +4 -21
- package/dist/shared/run/api.d.ts +2 -0
- package/dist/shared/run/browser.d.ts +9 -1
- package/dist/shared/run/browser.js +43 -3
- package/dist/shared/state/index.d.ts +1 -1
- package/dist/shared/state/index.js +2 -0
- package/dist/shared/state/session-state.d.ts +20 -1
- package/dist/shared/state/session-state.js +12 -2
- package/dist/shared/workflow/workflow.d.ts +2 -1
- package/dist/shared/workflow/workflow.js +16 -9
- package/package.json +17 -16
- package/scripts/postinstall.mjs +13 -11
- package/scripts/skills-libretto.mjs +14 -4
- package/skills/AGENTS.md +11 -0
- package/skills/libretto/SKILL.md +30 -9
- package/skills/libretto/references/auth-profiles.md +1 -1
- package/skills/libretto/references/code-generation-rules.md +3 -3
- package/skills/libretto/references/configuration-file-reference.md +11 -6
- package/skills/libretto-readonly/SKILL.md +95 -0
- package/src/cli/cli.ts +10 -0
- package/src/cli/commands/ai.ts +111 -1
- package/src/cli/commands/browser.ts +111 -9
- package/src/cli/commands/execution.ts +181 -74
- package/src/cli/commands/setup.ts +516 -0
- package/src/cli/commands/snapshot.ts +2 -2
- package/src/cli/commands/status.ts +79 -0
- package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
- package/src/cli/core/api-snapshot-analyzer.ts +7 -5
- package/src/cli/core/browser.ts +242 -35
- package/src/cli/core/{ai-config.ts → config.ts} +14 -108
- package/src/cli/core/context.ts +1 -45
- package/src/cli/core/deploy-artifact.ts +141 -71
- package/src/cli/core/providers/browserbase.ts +57 -0
- package/src/cli/core/providers/index.ts +62 -0
- package/src/cli/core/providers/kernel.ts +49 -0
- package/src/cli/core/providers/libretto-cloud.ts +61 -0
- package/src/cli/core/providers/types.ts +9 -0
- package/src/cli/core/readonly-exec.ts +284 -0
- package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
- package/src/cli/core/session.ts +75 -2
- package/src/cli/core/skill-version.ts +93 -0
- package/src/cli/core/telemetry.ts +0 -52
- package/src/cli/index.ts +0 -6
- package/src/cli/router.ts +4 -4
- package/src/cli/workers/run-integration-runtime.ts +18 -16
- package/src/cli/workers/run-integration-worker-protocol.ts +4 -1
- package/src/index.ts +1 -7
- package/src/runtime/extract/extract.ts +6 -5
- package/src/runtime/recovery/agent.ts +5 -4
- package/src/runtime/recovery/errors.ts +4 -3
- package/src/runtime/recovery/recovery.ts +4 -4
- package/src/shared/debug/pause.ts +4 -23
- package/src/shared/run/browser.ts +50 -1
- package/src/shared/state/index.ts +2 -0
- package/src/shared/state/session-state.ts +10 -0
- package/src/shared/workflow/workflow.ts +24 -13
- package/dist/cli/commands/init.js +0 -286
- package/dist/cli/commands/logs.js +0 -117
- package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
- package/dist/shared/llm/ai-sdk-adapter.js +0 -49
- package/dist/shared/llm/client.d.ts +0 -13
- package/dist/shared/llm/index.d.ts +0 -5
- package/dist/shared/llm/index.js +0 -6
- package/dist/shared/llm/types.d.ts +0 -67
- package/src/cli/commands/init.ts +0 -331
- package/src/cli/commands/logs.ts +0 -128
- package/src/shared/llm/ai-sdk-adapter.ts +0 -81
- package/src/shared/llm/index.ts +0 -3
- package/src/shared/llm/types.ts +0 -63
- /package/dist/{shared/llm → cli/core/providers}/types.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { SessionAccessModeSchema } from "../../shared/state/index.js";
|
|
4
5
|
import { LIBRETTO_CONFIG_PATH } from "./context.js";
|
|
5
6
|
|
|
6
7
|
export const CURRENT_CONFIG_VERSION = 1;
|
|
@@ -41,34 +42,12 @@ export const LibrettoConfigSchema = z
|
|
|
41
42
|
ai: AiConfigSchema.optional(),
|
|
42
43
|
viewport: ViewportConfigSchema.optional(),
|
|
43
44
|
windowPosition: WindowPositionConfigSchema.optional(),
|
|
45
|
+
provider: z.string().optional(),
|
|
46
|
+
sessionMode: SessionAccessModeSchema.optional(),
|
|
44
47
|
})
|
|
45
48
|
.passthrough();
|
|
46
49
|
export type LibrettoConfig = z.infer<typeof LibrettoConfigSchema>;
|
|
47
50
|
|
|
48
|
-
/** Default models for each provider shorthand accepted by `ai configure`. */
|
|
49
|
-
const DEFAULT_MODELS: Record<string, string> = {
|
|
50
|
-
openai: "openai/gpt-5.4",
|
|
51
|
-
anthropic: "anthropic/claude-sonnet-4-6",
|
|
52
|
-
gemini: "google/gemini-3-flash-preview",
|
|
53
|
-
vertex: "vertex/gemini-2.5-pro",
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const PROVIDER_ALIASES: Record<string, string> = {
|
|
57
|
-
claude: DEFAULT_MODELS.anthropic,
|
|
58
|
-
google: DEFAULT_MODELS.gemini,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const CONFIGURE_PROVIDERS = [
|
|
62
|
-
"openai",
|
|
63
|
-
"anthropic",
|
|
64
|
-
"gemini",
|
|
65
|
-
"vertex",
|
|
66
|
-
] as const;
|
|
67
|
-
|
|
68
|
-
function formatConfigureProviders(separator = " | "): string {
|
|
69
|
-
return CONFIGURE_PROVIDERS.join(separator);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
51
|
function formatConfigIssues(error: z.ZodError): string {
|
|
73
52
|
return error.issues
|
|
74
53
|
.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`)
|
|
@@ -91,6 +70,7 @@ function formatExpectedConfigExample(): string {
|
|
|
91
70
|
x: 1600,
|
|
92
71
|
y: 120,
|
|
93
72
|
},
|
|
73
|
+
sessionMode: "write-access",
|
|
94
74
|
},
|
|
95
75
|
null,
|
|
96
76
|
2,
|
|
@@ -105,10 +85,10 @@ function invalidConfigError(configPath: string, detail?: string): Error {
|
|
|
105
85
|
"Expected config example:",
|
|
106
86
|
formatExpectedConfigExample(),
|
|
107
87
|
"Notes:",
|
|
108
|
-
' - "ai", "viewport", and "
|
|
88
|
+
' - "ai", "viewport", "windowPosition", and "sessionMode" are optional.',
|
|
109
89
|
' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
|
|
110
90
|
"Fix the file to match this shape, or delete it and rerun:",
|
|
111
|
-
` npx libretto ai configure
|
|
91
|
+
` npx libretto ai configure openai | anthropic | gemini | vertex`,
|
|
112
92
|
]
|
|
113
93
|
.filter(Boolean)
|
|
114
94
|
.join("\n"),
|
|
@@ -162,7 +142,14 @@ export function writeAiConfig(
|
|
|
162
142
|
model: string,
|
|
163
143
|
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
164
144
|
): AiConfig {
|
|
165
|
-
|
|
145
|
+
let librettoConfig: LibrettoConfig;
|
|
146
|
+
try {
|
|
147
|
+
librettoConfig = readLibrettoConfig(configPath);
|
|
148
|
+
} catch {
|
|
149
|
+
// Existing config is malformed — start fresh so repair flows can
|
|
150
|
+
// overwrite a broken file instead of throwing.
|
|
151
|
+
librettoConfig = { version: CURRENT_CONFIG_VERSION };
|
|
152
|
+
}
|
|
166
153
|
const ai = AiConfigSchema.parse({
|
|
167
154
|
model,
|
|
168
155
|
updatedAt: new Date().toISOString(),
|
|
@@ -192,84 +179,3 @@ export function clearAiConfig(
|
|
|
192
179
|
);
|
|
193
180
|
return true;
|
|
194
181
|
}
|
|
195
|
-
|
|
196
|
-
function printAiConfig(config: AiConfig, configPath: string): void {
|
|
197
|
-
console.log(`Model: ${config.model}`);
|
|
198
|
-
console.log(`Config file: ${configPath}`);
|
|
199
|
-
console.log(`Updated at: ${config.updatedAt}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Resolve the model string from a `ai configure` argument.
|
|
204
|
-
* Accepts a provider shorthand ("openai", "anthropic", "gemini", "vertex")
|
|
205
|
-
* or a full provider/model-id string ("openai/gpt-4o", "anthropic/claude-sonnet-4-6").
|
|
206
|
-
*/
|
|
207
|
-
function resolveModelFromInput(input: string): string | null {
|
|
208
|
-
const trimmed = input.trim();
|
|
209
|
-
if (!trimmed) return null;
|
|
210
|
-
|
|
211
|
-
// Full model string (contains a slash)
|
|
212
|
-
if (trimmed.includes("/")) return trimmed;
|
|
213
|
-
|
|
214
|
-
// Provider shorthand
|
|
215
|
-
const normalized = trimmed.toLowerCase();
|
|
216
|
-
return DEFAULT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function runAiConfigure(
|
|
220
|
-
input: {
|
|
221
|
-
preset?: string;
|
|
222
|
-
clear?: boolean;
|
|
223
|
-
},
|
|
224
|
-
options: {
|
|
225
|
-
configureCommandName?: string;
|
|
226
|
-
configPath?: string;
|
|
227
|
-
} = {},
|
|
228
|
-
): void {
|
|
229
|
-
const configureCommandName =
|
|
230
|
-
options.configureCommandName ?? "npx libretto ai configure";
|
|
231
|
-
const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
|
|
232
|
-
|
|
233
|
-
const presetArg = input.preset?.trim();
|
|
234
|
-
|
|
235
|
-
if (!presetArg && !input.clear) {
|
|
236
|
-
const config = readAiConfig(configPath);
|
|
237
|
-
if (!config) {
|
|
238
|
-
console.log(
|
|
239
|
-
`No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
|
|
240
|
-
);
|
|
241
|
-
console.log(
|
|
242
|
-
"Provider credentials still come from your shell or .env file.",
|
|
243
|
-
);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
printAiConfig(config, configPath);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (input.clear) {
|
|
251
|
-
const removed = clearAiConfig(configPath);
|
|
252
|
-
if (removed) {
|
|
253
|
-
console.log(`Cleared AI config: ${configPath}`);
|
|
254
|
-
} else {
|
|
255
|
-
console.log("No AI config was set.");
|
|
256
|
-
}
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const model = resolveModelFromInput(presetArg!);
|
|
261
|
-
if (!model) {
|
|
262
|
-
console.log(
|
|
263
|
-
`Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>\n` +
|
|
264
|
-
` ${configureCommandName}\n` +
|
|
265
|
-
` ${configureCommandName} --clear`,
|
|
266
|
-
);
|
|
267
|
-
throw new Error(
|
|
268
|
-
`Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`,
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const config = writeAiConfig(model, configPath);
|
|
273
|
-
console.log("AI config saved.");
|
|
274
|
-
printAiConfig(config, configPath);
|
|
275
|
-
}
|
package/src/cli/core/context.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { Logger, createFileLogSink } from "../../shared/logger/index.js";
|
|
2
|
-
import type { LLMClient } from "../../shared/llm/index.js";
|
|
3
|
-
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
4
2
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
3
|
import { join } from "node:path";
|
|
6
4
|
import { resolveLibrettoRepoRoot } from "../../shared/paths/repo-root.js";
|
|
@@ -72,13 +70,6 @@ export function createLoggerForSession(session: string): Logger {
|
|
|
72
70
|
);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
export async function closeLogger(
|
|
76
|
-
logger: Logger | null | undefined,
|
|
77
|
-
): Promise<void> {
|
|
78
|
-
if (!logger) return;
|
|
79
|
-
await logger.close();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
73
|
export async function withSessionLogger<T>(
|
|
83
74
|
session: string,
|
|
84
75
|
run: (logger: Logger) => Promise<T>,
|
|
@@ -87,41 +78,6 @@ export async function withSessionLogger<T>(
|
|
|
87
78
|
try {
|
|
88
79
|
return await run(logger);
|
|
89
80
|
} finally {
|
|
90
|
-
await
|
|
81
|
+
await logger.close();
|
|
91
82
|
}
|
|
92
83
|
}
|
|
93
|
-
|
|
94
|
-
let llmClientFactory:
|
|
95
|
-
| ((logger: LoggerApi, model: string) => Promise<LLMClient>)
|
|
96
|
-
| null = null;
|
|
97
|
-
|
|
98
|
-
export function setLLMClientFactory(
|
|
99
|
-
factory: (logger: LoggerApi, model: string) => Promise<LLMClient>,
|
|
100
|
-
): void {
|
|
101
|
-
llmClientFactory = factory;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function getLLMClientFactory():
|
|
105
|
-
| ((logger: LoggerApi, model: string) => Promise<LLMClient>)
|
|
106
|
-
| null {
|
|
107
|
-
return llmClientFactory;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function maybeConfigureLLMClientFactoryFromEnv(): void {
|
|
111
|
-
if (llmClientFactory) return;
|
|
112
|
-
|
|
113
|
-
const hasAnyCreds =
|
|
114
|
-
process.env.GOOGLE_CLOUD_PROJECT ||
|
|
115
|
-
process.env.GCLOUD_PROJECT ||
|
|
116
|
-
process.env.ANTHROPIC_API_KEY ||
|
|
117
|
-
process.env.OPENAI_API_KEY ||
|
|
118
|
-
process.env.GEMINI_API_KEY ||
|
|
119
|
-
process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
120
|
-
|
|
121
|
-
if (!hasAnyCreds) return;
|
|
122
|
-
|
|
123
|
-
setLLMClientFactory(async (_logger, model) => {
|
|
124
|
-
const { createLLMClient } = await import("../../shared/llm/index.js");
|
|
125
|
-
return createLLMClient(model);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
@@ -10,11 +10,17 @@ import {
|
|
|
10
10
|
rmSync,
|
|
11
11
|
writeFileSync,
|
|
12
12
|
} from "node:fs";
|
|
13
|
+
import { createRequire, Module } from "node:module";
|
|
13
14
|
import { tmpdir } from "node:os";
|
|
14
15
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { gzipSync } from "node:zlib";
|
|
17
18
|
import { build } from "esbuild";
|
|
19
|
+
import {
|
|
20
|
+
getWorkflowFromModuleExports,
|
|
21
|
+
getWorkflowsFromModuleExports,
|
|
22
|
+
LIBRETTO_WORKFLOW_BRAND,
|
|
23
|
+
} from "../../shared/workflow/workflow.js";
|
|
18
24
|
|
|
19
25
|
type PackageManifest = {
|
|
20
26
|
name?: string;
|
|
@@ -81,6 +87,7 @@ const CURRENT_LIBRETTO_VERSION = readCurrentLibrettoVersion();
|
|
|
81
87
|
const CURRENT_LIBRETTO_PACKAGE_DIR = fileURLToPath(
|
|
82
88
|
new URL("../../..", import.meta.url),
|
|
83
89
|
);
|
|
90
|
+
const require = createRequire(import.meta.url);
|
|
84
91
|
|
|
85
92
|
function readCurrentLibrettoVersion(): string {
|
|
86
93
|
const packageJsonPath = fileURLToPath(
|
|
@@ -654,46 +661,141 @@ function formatBuildError(error: unknown): string {
|
|
|
654
661
|
.join("\n");
|
|
655
662
|
}
|
|
656
663
|
|
|
657
|
-
function
|
|
658
|
-
|
|
664
|
+
function getGeneratedWorkflowExportName(index: number): string {
|
|
665
|
+
return `workflow_${index}`;
|
|
666
|
+
}
|
|
659
667
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
exportNames.add(entry[1]!);
|
|
668
|
+
function getPackageNameFromImportPath(importPath: string): string {
|
|
669
|
+
if (importPath.startsWith("@")) {
|
|
670
|
+
return importPath.split("/").slice(0, 2).join("/");
|
|
664
671
|
}
|
|
672
|
+
return importPath.split("/")[0] ?? importPath;
|
|
673
|
+
}
|
|
665
674
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
675
|
+
function createExternalDiscoveryStub(): object {
|
|
676
|
+
const stub = (() => createExternalDiscoveryStub()) as unknown as ((
|
|
677
|
+
...args: unknown[]
|
|
678
|
+
) => object) &
|
|
679
|
+
Record<PropertyKey, unknown>;
|
|
680
|
+
|
|
681
|
+
return new Proxy(stub, {
|
|
682
|
+
apply: () => createExternalDiscoveryStub(),
|
|
683
|
+
construct: () => createExternalDiscoveryStub(),
|
|
684
|
+
get: (_target, property) => {
|
|
685
|
+
if (property === "__esModule") {
|
|
686
|
+
return true;
|
|
672
687
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
);
|
|
676
|
-
if (aliasMatch?.[2]) {
|
|
677
|
-
exportNames.add(aliasMatch[2]);
|
|
678
|
-
continue;
|
|
688
|
+
if (property === "default") {
|
|
689
|
+
return createExternalDiscoveryStub();
|
|
679
690
|
}
|
|
680
|
-
if (
|
|
681
|
-
|
|
691
|
+
if (property === Symbol.toPrimitive) {
|
|
692
|
+
return () => "";
|
|
682
693
|
}
|
|
683
|
-
|
|
694
|
+
return createExternalDiscoveryStub();
|
|
695
|
+
},
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function createDiscoveryLibrettoModule(workflowNames: Set<string>): object {
|
|
700
|
+
const moduleShape: Record<PropertyKey, unknown> = {
|
|
701
|
+
LIBRETTO_WORKFLOW_BRAND,
|
|
702
|
+
workflow: (name: string) => {
|
|
703
|
+
workflowNames.add(name);
|
|
704
|
+
return {
|
|
705
|
+
[LIBRETTO_WORKFLOW_BRAND]: true,
|
|
706
|
+
name,
|
|
707
|
+
async run() {
|
|
708
|
+
return undefined;
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
return new Proxy(moduleShape, {
|
|
715
|
+
get(target, property) {
|
|
716
|
+
if (property in target) {
|
|
717
|
+
return target[property];
|
|
718
|
+
}
|
|
719
|
+
if (property === "__esModule") {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
return createExternalDiscoveryStub();
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function discoverBundledWorkflowNames(args: {
|
|
728
|
+
absEntryPoint: string;
|
|
729
|
+
absSourceDir: string;
|
|
730
|
+
bundleBuffer: Buffer;
|
|
731
|
+
externalPackages: ReadonlySet<string>;
|
|
732
|
+
}): string[] {
|
|
733
|
+
const discoveryPath = join(
|
|
734
|
+
args.absSourceDir,
|
|
735
|
+
`.libretto-deploy-discovery-${process.pid}-${Date.now()}.cjs`,
|
|
736
|
+
);
|
|
737
|
+
const originalRequire = Module.prototype.require;
|
|
738
|
+
const workflowNames = new Set<string>();
|
|
739
|
+
const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowNames);
|
|
740
|
+
let loadedModuleExports: Record<string, unknown> | null = null;
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
writeFileSync(discoveryPath, args.bundleBuffer);
|
|
744
|
+
Module.prototype.require = function patchedRequire(id: string) {
|
|
745
|
+
const packageName = getPackageNameFromImportPath(id);
|
|
746
|
+
if (packageName === "libretto") {
|
|
747
|
+
return discoveryLibrettoModule;
|
|
748
|
+
}
|
|
749
|
+
if (packageName !== "libretto" && args.externalPackages.has(packageName)) {
|
|
750
|
+
return createExternalDiscoveryStub();
|
|
751
|
+
}
|
|
752
|
+
return originalRequire.call(this, id);
|
|
753
|
+
};
|
|
754
|
+
loadedModuleExports = require(discoveryPath) as Record<string, unknown>;
|
|
755
|
+
} catch (error) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
`Failed to evaluate deploy entry point ${args.absEntryPoint} while discovering workflows.\n${formatBuildError(error)}`,
|
|
758
|
+
);
|
|
759
|
+
} finally {
|
|
760
|
+
Module.prototype.require = originalRequire;
|
|
761
|
+
delete (require.cache as Record<string, unknown> | undefined)?.[
|
|
762
|
+
discoveryPath
|
|
763
|
+
];
|
|
764
|
+
rmSync(discoveryPath, { force: true });
|
|
684
765
|
}
|
|
685
766
|
|
|
686
|
-
|
|
687
|
-
|
|
767
|
+
const discoveredWorkflowNames = [...workflowNames].sort((left, right) =>
|
|
768
|
+
left.localeCompare(right),
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
if (discoveredWorkflowNames.length === 0) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`No workflows were found in ${args.absEntryPoint}. Import the workflow files you want to deploy from the entry point, or export a workflow directly from it.`,
|
|
774
|
+
);
|
|
688
775
|
}
|
|
689
776
|
|
|
690
|
-
|
|
777
|
+
const exportedWorkflowNames = new Set(
|
|
778
|
+
getWorkflowsFromModuleExports(loadedModuleExports ?? {}).map(
|
|
779
|
+
(workflow) => workflow.name,
|
|
780
|
+
),
|
|
781
|
+
);
|
|
782
|
+
const nonExportedWorkflowNames = discoveredWorkflowNames.filter(
|
|
783
|
+
(name) => !exportedWorkflowNames.has(name),
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
if (nonExportedWorkflowNames.length > 0) {
|
|
787
|
+
throw new Error(
|
|
788
|
+
`Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.join(", ")}`,
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return discoveredWorkflowNames;
|
|
691
793
|
}
|
|
692
794
|
|
|
693
795
|
function createBootstrapSource(args: {
|
|
694
796
|
bundleBuffer: Buffer;
|
|
695
797
|
deploymentName: string;
|
|
696
|
-
|
|
798
|
+
workflowNames: readonly string[];
|
|
697
799
|
}): string {
|
|
698
800
|
const bundleHash = createHash("sha256")
|
|
699
801
|
.update(args.bundleBuffer)
|
|
@@ -703,17 +805,12 @@ function createBootstrapSource(args: {
|
|
|
703
805
|
"base64",
|
|
704
806
|
);
|
|
705
807
|
const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
|
|
706
|
-
const
|
|
707
|
-
const exportLines = args.exportNames
|
|
708
|
-
.filter((name) => name !== "default")
|
|
808
|
+
const exportLines = args.workflowNames
|
|
709
809
|
.map(
|
|
710
|
-
(name) =>
|
|
711
|
-
`export const ${
|
|
810
|
+
(name, index) =>
|
|
811
|
+
`export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(name)});`,
|
|
712
812
|
)
|
|
713
813
|
.join("\n");
|
|
714
|
-
const defaultExportLine = hasDefaultExport
|
|
715
|
-
? 'export default createWorkflowProxy("default");'
|
|
716
|
-
: "";
|
|
717
814
|
|
|
718
815
|
// The deploy entrypoint is tiny on purpose. Hosted build imports this module
|
|
719
816
|
// to discover workflow exports. The implementation bundle stays embedded in
|
|
@@ -724,7 +821,7 @@ import { existsSync, writeFileSync } from "node:fs";
|
|
|
724
821
|
import { tmpdir } from "node:os";
|
|
725
822
|
import { join } from "node:path";
|
|
726
823
|
import { gunzipSync } from "node:zlib";
|
|
727
|
-
import { workflow } from "libretto";
|
|
824
|
+
import { getWorkflowFromModuleExports, workflow } from "libretto";
|
|
728
825
|
|
|
729
826
|
const BUNDLE_HASH = ${JSON.stringify(bundleHash)};
|
|
730
827
|
const BUNDLE_GZIP_BASE64 = ${JSON.stringify(bundleBase64)};
|
|
@@ -747,13 +844,13 @@ function ensureBundleFile() {
|
|
|
747
844
|
return BUNDLE_FILENAME;
|
|
748
845
|
}
|
|
749
846
|
|
|
750
|
-
function createWorkflowProxy(
|
|
751
|
-
return workflow(
|
|
847
|
+
function createWorkflowProxy(workflowName) {
|
|
848
|
+
return workflow(workflowName, async (ctx, input) => {
|
|
752
849
|
const impl = nativeRequire(ensureBundleFile());
|
|
753
|
-
const target = impl
|
|
850
|
+
const target = getWorkflowFromModuleExports(impl, workflowName);
|
|
754
851
|
if (!target || typeof target.run !== "function") {
|
|
755
852
|
throw new Error(
|
|
756
|
-
\`Expected workflow
|
|
853
|
+
\`Expected exported workflow "\${workflowName}" to be available in the bundled deployment implementation.\`,
|
|
757
854
|
);
|
|
758
855
|
}
|
|
759
856
|
return await target.run(ctx, input);
|
|
@@ -761,7 +858,6 @@ function createWorkflowProxy(exportName) {
|
|
|
761
858
|
}
|
|
762
859
|
|
|
763
860
|
${exportLines}
|
|
764
|
-
${defaultExportLine}
|
|
765
861
|
`;
|
|
766
862
|
}
|
|
767
863
|
|
|
@@ -802,45 +898,19 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
802
898
|
);
|
|
803
899
|
}
|
|
804
900
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
bundle: true,
|
|
811
|
-
entryPoints: [args.absEntryPoint],
|
|
812
|
-
external: [...args.externalPackages],
|
|
813
|
-
format: "esm",
|
|
814
|
-
outfile: "entry-exports.js",
|
|
815
|
-
platform: "node",
|
|
816
|
-
plugins: [
|
|
817
|
-
workspaceSourcePlugin(args.workspacePackages, args.externalPackages),
|
|
818
|
-
],
|
|
819
|
-
splitting: false,
|
|
820
|
-
target: "node20",
|
|
821
|
-
write: false,
|
|
901
|
+
const workflowNames = discoverBundledWorkflowNames({
|
|
902
|
+
absEntryPoint: args.absEntryPoint,
|
|
903
|
+
absSourceDir: args.absSourceDir,
|
|
904
|
+
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
905
|
+
externalPackages: args.externalPackages,
|
|
822
906
|
});
|
|
823
907
|
|
|
824
|
-
const bundledExports = exportBuild.outputFiles?.find((file) =>
|
|
825
|
-
file.path.endsWith("entry-exports.js"),
|
|
826
|
-
);
|
|
827
|
-
if (!bundledExports) {
|
|
828
|
-
throw new Error("Bundler did not produce an export analysis file.");
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const exportNames = extractExportNamesFromEsmBundle(bundledExports.text);
|
|
832
|
-
if (exportNames.length === 0) {
|
|
833
|
-
throw new Error(
|
|
834
|
-
`No named exports were found in ${args.absEntryPoint}. Hosted deploy expects the entry point to export one or more workflows.`,
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
908
|
writeFileSync(
|
|
839
909
|
join(args.outputDir, "index.js"),
|
|
840
910
|
createBootstrapSource({
|
|
841
911
|
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
842
912
|
deploymentName: args.deploymentName,
|
|
843
|
-
|
|
913
|
+
workflowNames,
|
|
844
914
|
}),
|
|
845
915
|
);
|
|
846
916
|
} catch (error) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ProviderApi } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function createBrowserbaseProvider(): ProviderApi {
|
|
4
|
+
const apiKey = process.env.BROWSERBASE_API_KEY;
|
|
5
|
+
if (!apiKey)
|
|
6
|
+
throw new Error(
|
|
7
|
+
"BROWSERBASE_API_KEY is required for Browserbase provider.",
|
|
8
|
+
);
|
|
9
|
+
const projectId = process.env.BROWSERBASE_PROJECT_ID;
|
|
10
|
+
if (!projectId)
|
|
11
|
+
throw new Error(
|
|
12
|
+
"BROWSERBASE_PROJECT_ID is required for Browserbase provider.",
|
|
13
|
+
);
|
|
14
|
+
const endpoint =
|
|
15
|
+
process.env.BROWSERBASE_ENDPOINT ?? "https://api.browserbase.com";
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
async createSession() {
|
|
19
|
+
const resp = await fetch(`${endpoint}/v1/sessions`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: {
|
|
22
|
+
"X-BB-API-Key": apiKey,
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({ projectId }),
|
|
26
|
+
});
|
|
27
|
+
if (!resp.ok) {
|
|
28
|
+
const body = await resp.text();
|
|
29
|
+
throw new Error(`Browserbase API error (${resp.status}): ${body}`);
|
|
30
|
+
}
|
|
31
|
+
const json = (await resp.json()) as {
|
|
32
|
+
id: string;
|
|
33
|
+
connectUrl: string;
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
sessionId: json.id,
|
|
37
|
+
cdpEndpoint: json.connectUrl,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async closeSession(sessionId) {
|
|
41
|
+
const resp = await fetch(`${endpoint}/v1/sessions/${sessionId}`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"X-BB-API-Key": apiKey,
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({ status: "REQUEST_RELEASE" }),
|
|
48
|
+
});
|
|
49
|
+
if (!resp.ok) {
|
|
50
|
+
const body = await resp.text();
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Browserbase API error closing session ${sessionId} (${resp.status}): ${body}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readLibrettoConfig } from "../config.js";
|
|
2
|
+
import { createBrowserbaseProvider } from "./browserbase.js";
|
|
3
|
+
import { createKernelProvider } from "./kernel.js";
|
|
4
|
+
import { createLibrettoCloudProvider } from "./libretto-cloud.js";
|
|
5
|
+
import type { ProviderApi } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const VALID_PROVIDERS = new Set(["local", "kernel", "browserbase", "libretto-cloud"] as const);
|
|
8
|
+
export type ProviderName =
|
|
9
|
+
typeof VALID_PROVIDERS extends Set<infer T> ? T : never;
|
|
10
|
+
|
|
11
|
+
function assertValidProviderName(value: string, source: string): ProviderName {
|
|
12
|
+
if (!VALID_PROVIDERS.has(value as ProviderName)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Invalid provider "${value}" from ${source}. Valid providers: ${[...VALID_PROVIDERS].join(", ")}`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return value as ProviderName;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve which provider to use.
|
|
22
|
+
* Precedence: CLI flag > LIBRETTO_PROVIDER env var > config file > "local" default.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveProviderName(cliFlag?: string): ProviderName {
|
|
25
|
+
if (cliFlag) {
|
|
26
|
+
return assertValidProviderName(cliFlag, "--provider flag");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const envVar = process.env.LIBRETTO_PROVIDER;
|
|
30
|
+
if (envVar) {
|
|
31
|
+
return assertValidProviderName(envVar, "LIBRETTO_PROVIDER env var");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config = readLibrettoConfig();
|
|
35
|
+
if (config.provider) {
|
|
36
|
+
return assertValidProviderName(config.provider, "config file");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return "local";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a ProviderApi instance for a cloud provider.
|
|
44
|
+
* Only call this for non-"local" providers.
|
|
45
|
+
*/
|
|
46
|
+
export function getCloudProviderApi(name: string): ProviderApi {
|
|
47
|
+
switch (name) {
|
|
48
|
+
case "kernel":
|
|
49
|
+
return createKernelProvider();
|
|
50
|
+
case "browserbase":
|
|
51
|
+
return createBrowserbaseProvider();
|
|
52
|
+
case "libretto-cloud":
|
|
53
|
+
console.warn(
|
|
54
|
+
"Note: The libretto-cloud provider is in alpha.",
|
|
55
|
+
);
|
|
56
|
+
return createLibrettoCloudProvider();
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Unknown provider "${name}". Valid cloud providers: kernel, browserbase`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ProviderApi } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function createKernelProvider(): ProviderApi {
|
|
4
|
+
const apiKey = process.env.KERNEL_API_KEY;
|
|
5
|
+
if (!apiKey)
|
|
6
|
+
throw new Error("KERNEL_API_KEY is required for Kernel provider.");
|
|
7
|
+
const endpoint = process.env.KERNEL_ENDPOINT ?? "https://api.onkernel.com";
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
async createSession() {
|
|
11
|
+
const resp = await fetch(`${endpoint}/browsers`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
Authorization: `Bearer ${apiKey}`,
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
headless: process.env.KERNEL_HEADLESS !== "false",
|
|
19
|
+
stealth: process.env.KERNEL_STEALTH === "true",
|
|
20
|
+
timeout_seconds: Number(process.env.KERNEL_TIMEOUT_SECONDS ?? 300),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
if (!resp.ok) {
|
|
24
|
+
const body = await resp.text();
|
|
25
|
+
throw new Error(`Kernel API error (${resp.status}): ${body}`);
|
|
26
|
+
}
|
|
27
|
+
const json = (await resp.json()) as {
|
|
28
|
+
session_id: string;
|
|
29
|
+
cdp_ws_url: string;
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
sessionId: json.session_id,
|
|
33
|
+
cdpEndpoint: json.cdp_ws_url,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
async closeSession(sessionId) {
|
|
37
|
+
const resp = await fetch(`${endpoint}/browsers/${sessionId}`, {
|
|
38
|
+
method: "DELETE",
|
|
39
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
40
|
+
});
|
|
41
|
+
if (!resp.ok) {
|
|
42
|
+
const body = await resp.text();
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Kernel API error closing session ${sessionId} (${resp.status}): ${body}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|