libretto 0.6.31 → 0.6.33

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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/README.template.md +1 -1
  3. package/dist/cli/commands/auth.js +119 -268
  4. package/dist/cli/commands/browser.js +1 -0
  5. package/dist/cli/commands/cloud-credentials.js +5 -17
  6. package/dist/cli/commands/cloud-jobs.js +125 -0
  7. package/dist/cli/commands/cloud-schedules.js +128 -0
  8. package/dist/cli/commands/cloud-settings.js +75 -0
  9. package/dist/cli/commands/cloud-sharing.js +13 -27
  10. package/dist/cli/commands/deploy.js +7 -16
  11. package/dist/cli/commands/execution.js +3 -2
  12. package/dist/cli/commands/profiles.js +8 -21
  13. package/dist/cli/commands/shared.js +17 -0
  14. package/dist/cli/core/browser.js +2 -1
  15. package/dist/cli/core/daemon/daemon.js +2 -1
  16. package/dist/cli/core/daemon/ipc.js +23 -16
  17. package/dist/cli/core/deploy-artifact.js +41 -16
  18. package/dist/cli/core/providers/kernel.js +3 -2
  19. package/dist/cli/core/providers/libretto-cloud.js +2 -1
  20. package/dist/cli/core/telemetry.js +14 -2
  21. package/dist/cli/router.js +6 -0
  22. package/dist/index.d.ts +1 -1
  23. package/dist/shared/workflow/workflow.d.ts +18 -0
  24. package/dist/shared/workflow/workflow.js +9 -0
  25. package/package.json +1 -1
  26. package/skills/libretto/SKILL.md +17 -2
  27. package/skills/libretto/references/website-authentication.md +18 -2
  28. package/skills/libretto-readonly/SKILL.md +1 -1
  29. package/src/cli/commands/auth.ts +169 -382
  30. package/src/cli/commands/browser.ts +1 -0
  31. package/src/cli/commands/cloud-credentials.ts +6 -18
  32. package/src/cli/commands/cloud-jobs.ts +157 -0
  33. package/src/cli/commands/cloud-schedules.ts +164 -0
  34. package/src/cli/commands/cloud-settings.ts +101 -0
  35. package/src/cli/commands/cloud-sharing.ts +20 -28
  36. package/src/cli/commands/deploy.ts +8 -19
  37. package/src/cli/commands/execution.ts +2 -1
  38. package/src/cli/commands/profiles.ts +10 -22
  39. package/src/cli/commands/shared.ts +29 -0
  40. package/src/cli/core/browser.ts +2 -0
  41. package/src/cli/core/daemon/config.ts +1 -0
  42. package/src/cli/core/daemon/daemon.ts +1 -0
  43. package/src/cli/core/daemon/ipc.ts +27 -18
  44. package/src/cli/core/deploy-artifact.ts +63 -14
  45. package/src/cli/core/providers/kernel.ts +3 -2
  46. package/src/cli/core/providers/libretto-cloud.ts +1 -0
  47. package/src/cli/core/providers/types.ts +1 -0
  48. package/src/cli/core/telemetry.ts +15 -1
  49. package/src/cli/router.ts +6 -0
  50. package/src/index.ts +1 -0
  51. package/src/shared/workflow/workflow.ts +22 -0
@@ -0,0 +1,128 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { SimpleCLI } from "affordance";
4
+ import { orpcCall } from "../core/auth-fetch.js";
5
+ import { withCloudApiKey } from "./shared.js";
6
+ const createScheduleUsage = "Usage: libretto cloud schedules create <workflow> --cron <expr> [--params <json> | --params-file <path>]";
7
+ function parseJsonObject(label, raw) {
8
+ let parsed;
9
+ try {
10
+ parsed = JSON.parse(raw);
11
+ } catch (error) {
12
+ throw new Error(
13
+ `Invalid JSON in ${label}: ${error instanceof Error ? error.message : String(error)}`
14
+ );
15
+ }
16
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17
+ throw new Error(`${label} must be a JSON object.`);
18
+ }
19
+ return parsed;
20
+ }
21
+ function readJsonObjectFile(label, filePath) {
22
+ let content;
23
+ try {
24
+ content = readFileSync(filePath, "utf8");
25
+ } catch {
26
+ throw new Error(
27
+ `Could not read ${label} "${filePath}". Ensure the file exists and is readable.`
28
+ );
29
+ }
30
+ return parseJsonObject(label, content);
31
+ }
32
+ const createCloudScheduleInput = SimpleCLI.input({
33
+ positionals: [
34
+ SimpleCLI.positional("workflow", z.string().optional(), {
35
+ help: "Deployed workflow name to schedule"
36
+ })
37
+ ],
38
+ named: {
39
+ cron: SimpleCLI.option(z.string().optional(), {
40
+ help: "Standard 5-field cron expression"
41
+ }),
42
+ timezone: SimpleCLI.option(z.string().optional(), {
43
+ help: "IANA timezone name (default: UTC)"
44
+ }),
45
+ params: SimpleCLI.option(z.string().optional(), {
46
+ help: "Inline JSON params object"
47
+ }),
48
+ paramsFile: SimpleCLI.option(z.string().optional(), {
49
+ name: "params-file",
50
+ help: "Path to a JSON params file"
51
+ }),
52
+ timeoutSeconds: SimpleCLI.option(z.coerce.number().int().min(1).optional(), {
53
+ name: "timeout-seconds",
54
+ help: "Job timeout in seconds for each schedule fire"
55
+ }),
56
+ callbackUrl: SimpleCLI.option(z.string().optional(), {
57
+ name: "callback-url",
58
+ help: "Per-schedule callback URL"
59
+ }),
60
+ callbackSecret: SimpleCLI.option(z.string().optional(), {
61
+ name: "callback-secret",
62
+ help: "Secret used to sign per-schedule callbacks"
63
+ }),
64
+ skipCallbacks: SimpleCLI.flag({
65
+ name: "skip-callbacks",
66
+ help: "Skip stored webhook callbacks for jobs created by this schedule"
67
+ }),
68
+ residentialProxy: SimpleCLI.option(z.string().optional(), {
69
+ name: "residential-proxy",
70
+ help: "Residential proxy config as a JSON object"
71
+ }),
72
+ disabled: SimpleCLI.flag({
73
+ help: "Create the schedule disabled"
74
+ })
75
+ }
76
+ }).refine((input) => Boolean(input.workflow && input.cron), createScheduleUsage).refine(
77
+ (input) => !(input.params && input.paramsFile),
78
+ "Pass either --params or --params-file, not both."
79
+ ).refine(
80
+ (input) => !input.callbackUrl && !input.callbackSecret || Boolean(input.callbackUrl && input.callbackSecret),
81
+ "Pass both --callback-url and --callback-secret, or omit both."
82
+ );
83
+ const createCloudScheduleCommand = SimpleCLI.command({
84
+ description: "Create a recurring schedule for a deployed workflow"
85
+ }).input(createCloudScheduleInput).use(withCloudApiKey("create Libretto Cloud schedules")).handle(async ({ input, ctx }) => {
86
+ const params = input.paramsFile ? readJsonObjectFile("--params-file", input.paramsFile) : input.params ? parseJsonObject("--params", input.params) : {};
87
+ const residentialProxy = input.residentialProxy ? parseJsonObject("--residential-proxy", input.residentialProxy) : void 0;
88
+ const payload = {
89
+ workflow: input.workflow,
90
+ cron_expr: input.cron,
91
+ timezone: input.timezone ?? "UTC",
92
+ params,
93
+ enabled: !input.disabled
94
+ };
95
+ if (input.timeoutSeconds !== void 0) {
96
+ payload.timeout_seconds = input.timeoutSeconds;
97
+ }
98
+ if (input.callbackUrl) payload.callback_url = input.callbackUrl;
99
+ if (input.callbackSecret) payload.callback_secret = input.callbackSecret;
100
+ if (input.skipCallbacks) payload.skip_callbacks = true;
101
+ if (residentialProxy !== void 0) {
102
+ payload.residential_proxy = residentialProxy;
103
+ }
104
+ const response = await orpcCall({
105
+ apiUrl: ctx.apiUrl,
106
+ path: "/v1/schedules/create",
107
+ input: payload,
108
+ credential: ctx.credential
109
+ });
110
+ const { schedule } = response;
111
+ console.log(`Schedule created: ${schedule.id}`);
112
+ console.log(`Workflow: ${schedule.workflow}`);
113
+ console.log(`Cron: ${schedule.cron_expr} (${schedule.timezone})`);
114
+ console.log(`Next fire: ${schedule.next_fire_at}`);
115
+ console.log(`Enabled: ${schedule.enabled ? "yes" : "no"}`);
116
+ return schedule.id;
117
+ });
118
+ const cloudScheduleCommands = SimpleCLI.group({
119
+ description: "Create and manage hosted schedules",
120
+ routes: {
121
+ create: createCloudScheduleCommand
122
+ }
123
+ });
124
+ export {
125
+ cloudScheduleCommands,
126
+ createCloudScheduleCommand,
127
+ createCloudScheduleInput
128
+ };
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import { SimpleCLI } from "affordance";
3
+ import { orpcCall } from "../core/auth-fetch.js";
4
+ import { withCloudApiKey } from "./shared.js";
5
+ const settingState = z.enum(["enabled", "disabled"]);
6
+ function toBoolean(state) {
7
+ return state === "enabled";
8
+ }
9
+ function printTenantSettings(settings) {
10
+ const notificationsEnabled = !settings.disable_job_failure_notifications;
11
+ console.log(
12
+ `Code sharing: ${settings.code_sharing_enabled ? "enabled" : "disabled"}`
13
+ );
14
+ console.log(
15
+ `Job failure notifications: ${notificationsEnabled ? "enabled" : "disabled"}`
16
+ );
17
+ console.log(
18
+ `Notification recipient: ${settings.debug_notification_email ?? "not configured"}`
19
+ );
20
+ return settings;
21
+ }
22
+ async function tenantSettings(ctx, input = {}) {
23
+ return orpcCall({
24
+ apiUrl: ctx.apiUrl,
25
+ path: "/v1/tenant/settings",
26
+ input,
27
+ credential: ctx.credential
28
+ });
29
+ }
30
+ const settingsStatusCommand = SimpleCLI.command({
31
+ description: "Show Libretto Cloud tenant settings"
32
+ }).input(SimpleCLI.input({ positionals: [], named: {} })).use(withCloudApiKey("manage Libretto Cloud settings")).handle(async ({ ctx }) => printTenantSettings(await tenantSettings(ctx)));
33
+ const setSettingsCommand = SimpleCLI.command({
34
+ description: "Update one or more Libretto Cloud tenant settings"
35
+ }).input(
36
+ SimpleCLI.input({
37
+ positionals: [],
38
+ named: {
39
+ codeSharing: SimpleCLI.option(settingState.optional(), {
40
+ help: "Set tenant code sharing: enabled or disabled"
41
+ }),
42
+ jobFailureNotifications: SimpleCLI.option(settingState.optional(), {
43
+ help: "Set hosted job failure notification emails: enabled or disabled"
44
+ })
45
+ }
46
+ })
47
+ ).use(withCloudApiKey("manage Libretto Cloud settings")).handle(async ({ input, ctx }) => {
48
+ const updates = {};
49
+ if (input.codeSharing !== void 0) {
50
+ updates.code_sharing_enabled = toBoolean(input.codeSharing);
51
+ }
52
+ if (input.jobFailureNotifications !== void 0) {
53
+ updates.disable_job_failure_notifications = !toBoolean(
54
+ input.jobFailureNotifications
55
+ );
56
+ }
57
+ if (Object.keys(updates).length === 0) {
58
+ throw new Error(
59
+ "No settings provided. Use one or more flags, for example `libretto cloud settings set --code-sharing enabled --job-failure-notifications disabled`."
60
+ );
61
+ }
62
+ return printTenantSettings(await tenantSettings(ctx, updates));
63
+ });
64
+ const settingsCommands = SimpleCLI.group({
65
+ description: "Manage Libretto Cloud tenant settings",
66
+ routes: {
67
+ status: settingsStatusCommand,
68
+ set: setSettingsCommand
69
+ }
70
+ });
71
+ export {
72
+ setSettingsCommand,
73
+ settingsCommands,
74
+ settingsStatusCommand
75
+ };
@@ -1,18 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { SimpleCLI } from "affordance";
3
- import { orpcCall, resolveApiUrl } from "../core/auth-fetch.js";
4
- function requireCloudApiKey() {
5
- const apiKey = process.env.LIBRETTO_API_KEY?.trim();
6
- if (!apiKey) {
7
- throw new Error(
8
- "LIBRETTO_API_KEY is required to share Libretto Cloud workflow code. Issue one with `libretto cloud auth api-key issue --label <label>`."
9
- );
10
- }
11
- return {
12
- apiUrl: resolveApiUrl(null),
13
- credential: { source: "env-api-key", apiKey }
14
- };
15
- }
3
+ import { orpcCall } from "../core/auth-fetch.js";
4
+ import { withCloudApiKey } from "./shared.js";
16
5
  const shareWorkflowCommand = SimpleCLI.command({
17
6
  description: "Share one hosted workflow's code publicly"
18
7
  }).input(SimpleCLI.input({
@@ -26,13 +15,12 @@ const shareWorkflowCommand = SimpleCLI.command({
26
15
  help: "Refresh an existing share from the workflow's current deployment"
27
16
  })
28
17
  }
29
- })).handle(async ({ input }) => {
30
- const { apiUrl, credential } = requireCloudApiKey();
18
+ })).use(withCloudApiKey("share Libretto Cloud workflow code")).handle(async ({ input, ctx }) => {
31
19
  const response = await orpcCall({
32
- apiUrl,
20
+ apiUrl: ctx.apiUrl,
33
21
  path: "/v1/workflows/share",
34
22
  input: { workflow: input.workflow, refresh: input.refresh },
35
- credential
23
+ credential: ctx.credential
36
24
  });
37
25
  if (response.status === "existing") {
38
26
  console.log(`Workflow is already shared: ${response.workflow}`);
@@ -48,34 +36,32 @@ const shareWorkflowCommand = SimpleCLI.command({
48
36
  });
49
37
  const codeSharingStatusCommand = SimpleCLI.command({
50
38
  description: "Show whether tenant code sharing is enabled"
51
- }).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
52
- const { apiUrl, credential } = requireCloudApiKey();
39
+ }).input(SimpleCLI.input({ positionals: [], named: {} })).use(withCloudApiKey("manage tenant workflow code sharing")).handle(async ({ ctx }) => {
53
40
  const response = await orpcCall({
54
- apiUrl,
41
+ apiUrl: ctx.apiUrl,
55
42
  path: "/v1/tenant/codeSharing",
56
43
  input: {},
57
- credential
44
+ credential: ctx.credential
58
45
  });
59
46
  console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
60
47
  return response.enabled;
61
48
  });
62
- async function updateCodeSharing(enabled) {
63
- const { apiUrl, credential } = requireCloudApiKey();
49
+ async function updateCodeSharing(enabled, ctx) {
64
50
  const response = await orpcCall({
65
- apiUrl,
51
+ apiUrl: ctx.apiUrl,
66
52
  path: "/v1/tenant/updateCodeSharing",
67
53
  input: { enabled },
68
- credential
54
+ credential: ctx.credential
69
55
  });
70
56
  console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
71
57
  return response.enabled;
72
58
  }
73
59
  const enableCodeSharingCommand = SimpleCLI.command({
74
60
  description: "Enable public workflow code sharing for this tenant"
75
- }).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => updateCodeSharing(true));
61
+ }).input(SimpleCLI.input({ positionals: [], named: {} })).use(withCloudApiKey("manage tenant workflow code sharing")).handle(async ({ ctx }) => updateCodeSharing(true, ctx));
76
62
  const disableCodeSharingCommand = SimpleCLI.command({
77
63
  description: "Disable public workflow code sharing for this tenant"
78
- }).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => updateCodeSharing(false));
64
+ }).input(SimpleCLI.input({ positionals: [], named: {} })).use(withCloudApiKey("manage tenant workflow code sharing")).handle(async ({ ctx }) => updateCodeSharing(false, ctx));
79
65
  const codeSharingCommands = SimpleCLI.group({
80
66
  description: "Manage tenant workflow code sharing",
81
67
  routes: {
@@ -1,14 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { z } from "zod";
3
- import {
4
- orpcCall,
5
- resolveApiUrl
6
- } from "../core/auth-fetch.js";
3
+ import { orpcCall } from "../core/auth-fetch.js";
7
4
  import {
8
5
  buildHostedDeployTarball
9
6
  } from "../core/deploy-artifact.js";
10
7
  import { readAuthState } from "../core/auth-storage.js";
11
8
  import { SimpleCLI } from "affordance";
9
+ import { withCloudApiKey } from "./shared.js";
12
10
  function generateDeploymentName() {
13
11
  return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
14
12
  }
@@ -30,16 +28,6 @@ function deployApiKeyRequiredMessage(hasStoredSession) {
30
28
  " \u2022 Add it to your project .env file: `LIBRETTO_API_KEY=<issued-key>`."
31
29
  ].join("\n");
32
30
  }
33
- async function requireDeployApiKey() {
34
- const apiKey = process.env.LIBRETTO_API_KEY?.trim();
35
- if (!apiKey) {
36
- throw new Error(deployApiKeyRequiredMessage(await hasStoredCloudSession()));
37
- }
38
- return {
39
- apiUrl: resolveApiUrl(null),
40
- credential: { source: "env-api-key", apiKey }
41
- };
42
- }
43
31
  async function hasStoredCloudSession() {
44
32
  try {
45
33
  return Boolean((await readAuthState())?.session);
@@ -130,8 +118,11 @@ const deployInput = SimpleCLI.input({
130
118
  });
131
119
  const deployCommand = SimpleCLI.command({
132
120
  description: "Deploy workflows to the hosted platform"
133
- }).input(deployInput).handle(async ({ input }) => {
134
- const { apiUrl, credential } = await requireDeployApiKey();
121
+ }).input(deployInput).use(withCloudApiKey(
122
+ "deploy to Libretto Cloud",
123
+ async () => deployApiKeyRequiredMessage(await hasStoredCloudSession())
124
+ )).handle(async ({ input, ctx }) => {
125
+ const { apiUrl, credential } = ctx;
135
126
  const deploymentName = generateDeploymentName();
136
127
  console.log("Bundling hosted deployment artifact...");
137
128
  const { entryPoint, source, workflows } = await buildHostedDeployTarball({
@@ -58,7 +58,8 @@ function createRunBrowserConfig(args) {
58
58
  if (args.providerName) {
59
59
  return {
60
60
  kind: "provider",
61
- providerName: args.providerName
61
+ providerName: args.providerName,
62
+ headless: args.headless
62
63
  };
63
64
  }
64
65
  return {
@@ -700,7 +701,7 @@ const runCommand = SimpleCLI.command({
700
701
  });
701
702
  console.log(`Connecting to ${providerName} browser...`);
702
703
  }
703
- const headless = daemonProviderName ? true : headlessMode ?? false;
704
+ const headless = headlessMode ?? false;
704
705
  const windowPosition = headless ? void 0 : resolveWindowPosition(ctx.logger);
705
706
  await runIntegrationFromFile(
706
707
  {
@@ -1,28 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import { SimpleCLI } from "affordance";
3
- import { orpcCall, resolveApiUrl } from "../core/auth-fetch.js";
3
+ import { orpcCall } from "../core/auth-fetch.js";
4
4
  import { normalizeProfileName } from "../core/profiles.js";
5
- function requireApiKeyCredential() {
6
- const apiKey = process.env.LIBRETTO_API_KEY?.trim();
7
- if (!apiKey) {
8
- throw new Error(
9
- "LIBRETTO_API_KEY is required to manage Libretto Cloud profiles. Issue one with `libretto cloud auth api-key issue --label <label>`."
10
- );
11
- }
12
- return {
13
- apiUrl: resolveApiUrl(null),
14
- credential: { source: "env-api-key", apiKey }
15
- };
16
- }
5
+ import { withCloudApiKey } from "./shared.js";
17
6
  const listProfilesCommand = SimpleCLI.command({
18
7
  description: "List Libretto Cloud auth profiles"
19
- }).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
20
- const { apiUrl, credential } = requireApiKeyCredential();
8
+ }).input(SimpleCLI.input({ positionals: [], named: {} })).use(withCloudApiKey("manage Libretto Cloud profiles")).handle(async ({ ctx }) => {
21
9
  const response = await orpcCall({
22
- apiUrl,
10
+ apiUrl: ctx.apiUrl,
23
11
  path: "/v1/browserProfiles/list",
24
12
  input: {},
25
- credential
13
+ credential: ctx.credential
26
14
  });
27
15
  if (response.profiles.length === 0) {
28
16
  console.log("No cloud profiles found.");
@@ -42,14 +30,13 @@ const deleteProfileCommand = SimpleCLI.command({
42
30
  })
43
31
  ],
44
32
  named: {}
45
- })).handle(async ({ input }) => {
33
+ })).use(withCloudApiKey("manage Libretto Cloud profiles")).handle(async ({ input, ctx }) => {
46
34
  const profileName = normalizeProfileName(input.profileName);
47
- const { apiUrl, credential } = requireApiKeyCredential();
48
35
  const response = await orpcCall({
49
- apiUrl,
36
+ apiUrl: ctx.apiUrl,
50
37
  path: "/v1/browserProfiles/delete",
51
38
  input: { name: profileName },
52
- credential
39
+ credential: ctx.credential
53
40
  });
54
41
  if (!response.success || response.deleted_count === 0) {
55
42
  console.log(`No cloud profile found for ${profileName}.`);
@@ -6,6 +6,7 @@ import {
6
6
  readSessionStateOrThrow,
7
7
  validateSessionName
8
8
  } from "../core/session.js";
9
+ import { resolveApiUrl } from "../core/auth-fetch.js";
9
10
  import {
10
11
  SimpleCLI
11
12
  } from "affordance";
@@ -24,6 +25,21 @@ function withExperiments() {
24
25
  experiments: resolveExperiments()
25
26
  });
26
27
  }
28
+ function withCloudApiKey(action, formatMissingMessage) {
29
+ return async ({ ctx }) => {
30
+ const apiKey = process.env.LIBRETTO_API_KEY?.trim();
31
+ if (!apiKey) {
32
+ throw new Error(
33
+ formatMissingMessage ? await formatMissingMessage() : `LIBRETTO_API_KEY is required to ${action}. Issue one with \`libretto cloud auth api-key issue --label <label>\`.`
34
+ );
35
+ }
36
+ return {
37
+ ...ctx,
38
+ apiUrl: resolveApiUrl(null),
39
+ credential: { source: "env-api-key", apiKey }
40
+ };
41
+ };
42
+ }
27
43
  function withRequiredSession() {
28
44
  return async ({ input, ctx }) => {
29
45
  if (!input.session) {
@@ -54,6 +70,7 @@ export {
54
70
  pageOption,
55
71
  sessionOption,
56
72
  withAutoSession,
73
+ withCloudApiKey,
57
74
  withExperiments,
58
75
  withRequiredSession
59
76
  };
@@ -387,7 +387,7 @@ async function runOpen(rawUrl, headed, session, logger, options) {
387
387
  });
388
388
  console.log(`Browser open (${browserMode}): ${url}`);
389
389
  }
390
- async function runOpenWithProvider(rawUrl, providerName, session, logger, accessMode, experiments) {
390
+ async function runOpenWithProvider(rawUrl, providerName, headless, session, logger, accessMode, experiments) {
391
391
  const parsedUrl = normalizeUrl(rawUrl);
392
392
  const url = parsedUrl.href;
393
393
  logger.info("open-provider-start", { url, provider: providerName, session });
@@ -408,6 +408,7 @@ async function runOpenWithProvider(rawUrl, providerName, session, logger, access
408
408
  browser: {
409
409
  kind: "provider",
410
410
  providerName,
411
+ headless,
411
412
  initialUrl: url
412
413
  }
413
414
  },
@@ -324,7 +324,8 @@ class BrowserDaemon {
324
324
  try {
325
325
  providerSession = await provider.createSession({
326
326
  authProfileName: config.authProfileName,
327
- authProfilePersist: config.authProfilePersist
327
+ authProfilePersist: config.authProfilePersist,
328
+ headless: config.headless
328
329
  });
329
330
  const browser = await chromium.connectOverCDP(
330
331
  providerSession.cdpEndpoint
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { openSync, closeSync } from "node:fs";
4
- import { createRequire } from "node:module";
4
+ import * as moduleBuiltin from "node:module";
5
5
  import { homedir, userInfo } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createIpcPeer } from "../../../shared/ipc/ipc.js";
@@ -72,22 +72,29 @@ class DaemonClient {
72
72
  const daemonEntryPath = fileURLToPath(
73
73
  new URL("./daemon.js", import.meta.url)
74
74
  );
75
- const require2 = createRequire(import.meta.url);
76
- const tsxCliPath = require2.resolve("tsx/cli");
77
- const childStderrFd = openSync(logPath, "a");
78
- const child = spawn(
79
- process.execPath,
80
- [
81
- tsxCliPath,
82
- ...config.workflow?.tsconfigPath ? ["--tsconfig", config.workflow.tsconfigPath] : [],
83
- daemonEntryPath,
84
- JSON.stringify(config)
85
- ],
86
- {
87
- detached: true,
88
- stdio: ["ignore", "ignore", childStderrFd, "ipc"]
75
+ const childArgs = [daemonEntryPath, JSON.stringify(config)];
76
+ const childEnv = { ...process.env };
77
+ if (config.workflow) {
78
+ const tsxPreflightPath = fileURLToPath(
79
+ import.meta.resolve("tsx/preflight")
80
+ );
81
+ const tsxLoaderFlag = typeof moduleBuiltin.register === "function" ? "--import" : "--loader";
82
+ childArgs.unshift(
83
+ "--require",
84
+ tsxPreflightPath,
85
+ tsxLoaderFlag,
86
+ import.meta.resolve("tsx")
87
+ );
88
+ if (config.workflow.tsconfigPath) {
89
+ childEnv.TSX_TSCONFIG_PATH = config.workflow.tsconfigPath;
89
90
  }
90
- );
91
+ }
92
+ const childStderrFd = openSync(logPath, "a");
93
+ const child = spawn(process.execPath, childArgs, {
94
+ detached: true,
95
+ stdio: ["ignore", "ignore", childStderrFd, "ipc"],
96
+ env: childEnv
97
+ });
91
98
  closeSync(childStderrFd);
92
99
  const pid = child.pid;
93
100
  logger.info("daemon-spawned", { pid, session });
@@ -21,6 +21,9 @@ import {
21
21
  LIBRETTO_WORKFLOW_BRAND
22
22
  } from "../../shared/workflow/workflow.js";
23
23
  import { normalizeCredentialNames } from "../../shared/workflow/credentials.js";
24
+ import {
25
+ ViewportConfigSchema
26
+ } from "./config.js";
24
27
  const DEFAULT_RUNTIME_EXTERNALS = [
25
28
  "playwright",
26
29
  "playwright-core",
@@ -581,7 +584,8 @@ function createDiscoveryLibrettoModule(workflowsByName) {
581
584
  workflowsByName.set(name, {
582
585
  name,
583
586
  ...extractDiscoveryCredentialMetadata(definitionOrHandler),
584
- ...extractDiscoveryAuthProfileMetadata(definitionOrHandler)
587
+ ...extractDiscoveryAuthProfileMetadata(definitionOrHandler),
588
+ ...extractDiscoveryLaunchMetadata(definitionOrHandler)
585
589
  });
586
590
  return {
587
591
  [LIBRETTO_WORKFLOW_BRAND]: true,
@@ -604,6 +608,26 @@ function createDiscoveryLibrettoModule(workflowsByName) {
604
608
  }
605
609
  });
606
610
  }
611
+ function extractDiscoveryLaunchMetadata(definitionOrHandler) {
612
+ if (!definitionOrHandler || typeof definitionOrHandler !== "object") {
613
+ return {};
614
+ }
615
+ const record = definitionOrHandler;
616
+ const metadata = {};
617
+ if (typeof record.startUrl === "string") {
618
+ metadata.startUrl = record.startUrl;
619
+ }
620
+ if (typeof record.gpu === "boolean") {
621
+ metadata.gpu = record.gpu;
622
+ }
623
+ if (record.viewport && typeof record.viewport === "object" && !Array.isArray(record.viewport)) {
624
+ const viewport = ViewportConfigSchema.safeParse(record.viewport);
625
+ if (viewport.success) {
626
+ metadata.viewport = viewport.data;
627
+ }
628
+ }
629
+ return metadata;
630
+ }
607
631
  function extractDiscoveryCredentialMetadata(definitionOrHandler) {
608
632
  if (!definitionOrHandler || typeof definitionOrHandler !== "object" || !("credentials" in definitionOrHandler)) {
609
633
  return { credentialNames: [] };
@@ -692,7 +716,10 @@ function createBootstrapSource(args) {
692
716
  (workflow, index) => `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(workflow.name)}, ${JSON.stringify({
693
717
  credentialNames: workflow.credentialNames,
694
718
  authProfileName: workflow.authProfileName,
695
- authProfileRefresh: workflow.authProfileRefresh
719
+ authProfileRefresh: workflow.authProfileRefresh,
720
+ startUrl: workflow.startUrl,
721
+ gpu: workflow.gpu,
722
+ viewport: workflow.viewport
696
723
  })});`
697
724
  ).join("\n");
698
725
  return `import { createRequire, Module } from "node:module";
@@ -774,23 +801,21 @@ function createWorkflowProxy(workflowName, metadata) {
774
801
  return await target.run(ctx, input);
775
802
  };
776
803
 
777
- if (!metadata?.authProfileName) {
778
- return workflow(workflowName, {
779
- credentials: Array.isArray(metadata?.credentialNames)
780
- ? metadata.credentialNames
781
- : [],
782
- handler,
783
- });
784
- }
785
-
786
804
  return workflow(workflowName, {
787
- credentials: Array.isArray(metadata.credentialNames)
805
+ credentials: Array.isArray(metadata?.credentialNames)
788
806
  ? metadata.credentialNames
789
807
  : [],
790
- authProfile: {
791
- name: metadata.authProfileName,
792
- ...(typeof metadata.authProfileRefresh === "boolean" ? { refresh: metadata.authProfileRefresh } : {}),
793
- },
808
+ ...(metadata?.authProfileName
809
+ ? {
810
+ authProfile: {
811
+ name: metadata.authProfileName,
812
+ ...(typeof metadata.authProfileRefresh === "boolean" ? { refresh: metadata.authProfileRefresh } : {}),
813
+ },
814
+ }
815
+ : {}),
816
+ ...(typeof metadata?.startUrl === "string" ? { startUrl: metadata.startUrl } : {}),
817
+ ...(typeof metadata?.gpu === "boolean" ? { gpu: metadata.gpu } : {}),
818
+ ...(metadata?.viewport ? { viewport: metadata.viewport } : {}),
794
819
  handler,
795
820
  });
796
821
  }
@@ -49,7 +49,8 @@ function createKernelProvider(options = {}) {
49
49
  const enableRecording = options.enableRecording ?? readBooleanEnv("KERNEL_ENABLE_RECORDING", false);
50
50
  const replays = /* @__PURE__ */ new Map();
51
51
  return {
52
- async createSession() {
52
+ async createSession(sessionOptions) {
53
+ const sessionHeadless = sessionOptions?.headless ?? headless;
53
54
  const json = await kernelFetchJson(
54
55
  endpoint,
55
56
  apiKey,
@@ -57,7 +58,7 @@ function createKernelProvider(options = {}) {
57
58
  {
58
59
  method: "POST",
59
60
  body: JSON.stringify({
60
- headless,
61
+ headless: sessionHeadless,
61
62
  stealth,
62
63
  timeout_seconds: timeoutSeconds
63
64
  })
@@ -25,7 +25,8 @@ function createLibrettoCloudProvider() {
25
25
  json: {
26
26
  timeout_seconds: browserSessionTimeoutSeconds,
27
27
  profile_name: options?.authProfileName,
28
- profile_persist: options?.authProfilePersist
28
+ profile_persist: options?.authProfilePersist,
29
+ headless: options?.headless
29
30
  }
30
31
  })
31
32
  });