libretto 0.6.13 → 0.6.15

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 (57) hide show
  1. package/dist/cli/commands/auth.js +43 -33
  2. package/dist/cli/commands/billing.js +3 -5
  3. package/dist/cli/commands/browser.js +3 -6
  4. package/dist/cli/commands/deploy.js +54 -45
  5. package/dist/cli/commands/execution.js +7 -4
  6. package/dist/cli/commands/experiments.js +1 -1
  7. package/dist/cli/commands/setup.js +1 -1
  8. package/dist/cli/commands/shared.js +1 -1
  9. package/dist/cli/commands/snapshot.js +1 -1
  10. package/dist/cli/commands/status.js +1 -1
  11. package/dist/cli/core/auth-fetch.js +11 -6
  12. package/dist/cli/core/browser.js +10 -5
  13. package/dist/cli/core/daemon/daemon.js +63 -10
  14. package/dist/cli/core/daemon/exec-repl.js +133 -0
  15. package/dist/cli/core/daemon/exec.js +6 -21
  16. package/dist/cli/core/daemon/ipc.js +47 -4
  17. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  18. package/dist/cli/core/exec-compiler.js +8 -3
  19. package/dist/cli/core/providers/index.js +13 -4
  20. package/dist/cli/core/providers/kernel.js +3 -3
  21. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  22. package/dist/cli/router.js +9 -4
  23. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  24. package/dist/shared/ipc/socket-transport.js +16 -5
  25. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  26. package/package.json +2 -2
  27. package/skills/libretto/SKILL.md +33 -29
  28. package/skills/libretto/references/code-generation-rules.md +6 -0
  29. package/skills/libretto/references/configuration-file-reference.md +8 -0
  30. package/skills/libretto/references/site-security-review.md +6 -6
  31. package/skills/libretto-readonly/SKILL.md +1 -1
  32. package/src/cli/commands/auth.ts +46 -33
  33. package/src/cli/commands/billing.ts +3 -5
  34. package/src/cli/commands/browser.ts +5 -9
  35. package/src/cli/commands/deploy.ts +55 -49
  36. package/src/cli/commands/execution.ts +7 -4
  37. package/src/cli/commands/experiments.ts +1 -1
  38. package/src/cli/commands/setup.ts +1 -1
  39. package/src/cli/commands/shared.ts +1 -1
  40. package/src/cli/commands/snapshot.ts +1 -1
  41. package/src/cli/commands/status.ts +1 -1
  42. package/src/cli/core/auth-fetch.ts +9 -4
  43. package/src/cli/core/browser.ts +12 -5
  44. package/src/cli/core/daemon/daemon.ts +81 -9
  45. package/src/cli/core/daemon/exec-repl.ts +189 -0
  46. package/src/cli/core/daemon/exec.ts +8 -43
  47. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  48. package/src/cli/core/daemon/ipc.ts +76 -7
  49. package/src/cli/core/exec-compiler.ts +8 -3
  50. package/src/cli/core/providers/index.ts +17 -4
  51. package/src/cli/core/providers/kernel.ts +4 -3
  52. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  53. package/src/cli/router.ts +9 -4
  54. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  55. package/src/shared/ipc/socket-transport.ts +20 -5
  56. package/dist/cli/framework/simple-cli.js +0 -880
  57. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -1,13 +1,13 @@
1
1
  import { z } from "zod";
2
- import { SimpleCLI } from "../framework/simple-cli.js";
2
+ import { SimpleCLI } from "affordance";
3
3
  import {
4
4
  ApiCallError,
5
5
  betterAuthCall,
6
- HOSTED_API_URL,
7
6
  NOT_AUTHENTICATED_MESSAGE,
8
7
  orpcCall,
9
8
  pickCredential,
10
- resolveApiUrl
9
+ resolveApiUrl,
10
+ resolveHostedApiUrl
11
11
  } from "../core/auth-fetch.js";
12
12
  import {
13
13
  authStatePath,
@@ -88,10 +88,9 @@ async function issueApiKey(apiUrl, name, credential) {
88
88
  return data;
89
89
  }
90
90
  const signupCommand = SimpleCLI.command({
91
- description: "Create a new hosted-platform account and organization",
92
- experimental: true
91
+ description: "Create a new hosted-platform account and organization"
93
92
  }).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
94
- const apiUrl = HOSTED_API_URL;
93
+ const apiUrl = resolveHostedApiUrl();
95
94
  console.log("Sign up for libretto cloud");
96
95
  console.log();
97
96
  console.log("Heads up: a libretto user can only belong to one organization.");
@@ -103,7 +102,7 @@ const signupCommand = SimpleCLI.command({
103
102
  const name = await prompt("Your name:");
104
103
  if (name.toLowerCase() === "q" || name.length === 0) {
105
104
  console.log(
106
- "OK \u2014 ask an existing teammate to run `libretto experimental auth invite <your-email>` and then run `libretto experimental auth accept-invite <slug> <invitation-id>` from this machine."
105
+ "OK \u2014 ask an existing teammate to run `libretto cloud auth invite <your-email>` and then run `libretto cloud auth accept-invite <slug> <invitation-id>` from this machine."
107
106
  );
108
107
  return;
109
108
  }
@@ -162,14 +161,13 @@ const signupCommand = SimpleCLI.command({
162
161
  console.log(`Session saved to ${authStatePath()}`);
163
162
  console.log();
164
163
  console.log("To generate an API key, run:");
165
- console.log(" libretto experimental auth api-key issue --label <label>");
164
+ console.log(" libretto cloud auth api-key issue --label <label>");
166
165
  console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
167
166
  });
168
167
  const loginCommand = SimpleCLI.command({
169
- description: "Sign in to an existing hosted-platform account",
170
- experimental: true
168
+ description: "Sign in to an existing hosted-platform account"
171
169
  }).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
172
- const apiUrl = HOSTED_API_URL;
170
+ const apiUrl = resolveHostedApiUrl();
173
171
  const email = await prompt("Email:");
174
172
  const password = await promptPassword("Password:");
175
173
  const { data, setCookie } = await betterAuthCall({
@@ -217,9 +215,25 @@ const loginCommand = SimpleCLI.command({
217
215
  }
218
216
  }
219
217
  });
218
+ const forgotPasswordCommand = SimpleCLI.command({
219
+ description: "Send a password reset email"
220
+ }).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
221
+ const apiUrl = resolveHostedApiUrl();
222
+ const email = await prompt("Email:");
223
+ const result = await orpcCall({
224
+ apiUrl,
225
+ path: "/v1/auth/requestPasswordReset",
226
+ input: { email },
227
+ unauthenticated: true
228
+ });
229
+ if (result.status === "not_found") {
230
+ console.log(`No Libretto account exists for ${email}.`);
231
+ return;
232
+ }
233
+ console.log(`Password reset link sent to ${email}.`);
234
+ });
220
235
  const logoutCommand = SimpleCLI.command({
221
- description: "Clear local libretto credentials",
222
- experimental: true
236
+ description: "Clear local libretto credentials"
223
237
  }).handle(async () => {
224
238
  const state = await readAuthState();
225
239
  if (state?.session?.cookie) {
@@ -236,8 +250,7 @@ const logoutCommand = SimpleCLI.command({
236
250
  console.log("Logged out.");
237
251
  });
238
252
  const inviteCommand = SimpleCLI.command({
239
- description: "Invite a teammate to your active organization",
240
- experimental: true
253
+ description: "Invite a teammate to your active organization"
241
254
  }).input(
242
255
  SimpleCLI.input({
243
256
  positionals: [
@@ -287,12 +300,11 @@ const inviteCommand = SimpleCLI.command({
287
300
  console.log();
288
301
  console.log("Tell them to run:");
289
302
  console.log(
290
- ` libretto experimental auth accept-invite ${orgSlug} ${data.id}`
303
+ ` libretto cloud auth accept-invite ${orgSlug} ${data.id}`
291
304
  );
292
305
  });
293
306
  const acceptInviteCommand = SimpleCLI.command({
294
- description: "Accept an organization invitation",
295
- experimental: true
307
+ description: "Accept an organization invitation"
296
308
  }).input(
297
309
  SimpleCLI.input({
298
310
  positionals: [
@@ -313,7 +325,7 @@ const acceptInviteCommand = SimpleCLI.command({
313
325
  })
314
326
  ).handle(async ({ input }) => {
315
327
  const stored = await readAuthState();
316
- const apiUrl = HOSTED_API_URL;
328
+ const apiUrl = resolveHostedApiUrl();
317
329
  const credential = pickCredential(stored);
318
330
  const expectedTenantSlug = input.tenantSlug;
319
331
  if (credential.source !== "none") {
@@ -328,7 +340,7 @@ const acceptInviteCommand = SimpleCLI.command({
328
340
  [
329
341
  "You're already a member of an organization.",
330
342
  "A libretto user can only belong to one organization at a time.",
331
- "To accept this invite: log out, delete the existing account, and re-run `auth accept-invite` with a new account (or a fresh email)."
343
+ "To accept this invite: log out, delete the existing account, and re-run `libretto cloud auth accept-invite` with a new account (or a fresh email)."
332
344
  ].join("\n")
333
345
  );
334
346
  }
@@ -381,12 +393,11 @@ const acceptInviteCommand = SimpleCLI.command({
381
393
  console.log();
382
394
  console.log("Email verified. You're logged in and a member of the organization.");
383
395
  console.log("To generate an API key, run:");
384
- console.log(" libretto experimental auth api-key issue --label <label>");
396
+ console.log(" libretto cloud auth api-key issue --label <label>");
385
397
  console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
386
398
  });
387
399
  const apiKeyIssueCommand = SimpleCLI.command({
388
- description: "Issue a new API key for the active organization",
389
- experimental: true
400
+ description: "Issue a new API key for the active organization"
390
401
  }).input(
391
402
  SimpleCLI.input({
392
403
  positionals: [],
@@ -416,8 +427,7 @@ const apiKeyIssueCommand = SimpleCLI.command({
416
427
  );
417
428
  });
418
429
  const apiKeyListCommand = SimpleCLI.command({
419
- description: "List API keys for the active organization",
420
- experimental: true
430
+ description: "List API keys for the active organization"
421
431
  }).handle(async () => {
422
432
  const stored = await readAuthState();
423
433
  const apiUrl = resolveApiUrl(stored);
@@ -444,13 +454,12 @@ const apiKeyListCommand = SimpleCLI.command({
444
454
  }
445
455
  });
446
456
  const apiKeyRevokeCommand = SimpleCLI.command({
447
- description: "Revoke an API key by id",
448
- experimental: true
457
+ description: "Revoke an API key by id"
449
458
  }).input(
450
459
  SimpleCLI.input({
451
460
  positionals: [
452
461
  SimpleCLI.positional("id", z.string().min(1), {
453
- help: "API key id (from `auth api-key list`)."
462
+ help: "API key id (from `libretto cloud auth api-key list`)."
454
463
  })
455
464
  ],
456
465
  named: {}
@@ -470,24 +479,23 @@ const apiKeyRevokeCommand = SimpleCLI.command({
470
479
  });
471
480
  console.log(`API key ${input.id} revoked.`);
472
481
  console.log(
473
- "If this key was in your .env, remove the LIBRETTO_API_KEY value and issue a new one with `auth api-key issue --label <label>`."
482
+ "If this key was in your .env, remove the LIBRETTO_API_KEY value and issue a new one with `libretto cloud auth api-key issue --label <label>`."
474
483
  );
475
484
  });
476
485
  const whoamiCommand = SimpleCLI.command({
477
- description: "Print the active session and credential source",
478
- experimental: true
486
+ description: "Print the active session and credential source"
479
487
  }).handle(async () => {
480
488
  const stored = await readAuthState();
481
489
  const credential = pickCredential(stored);
482
490
  const envKey = process.env.LIBRETTO_API_KEY?.trim();
483
491
  if (credential.source === "none") {
484
492
  console.log(
485
- "Not authenticated. Run `libretto experimental auth signup`, `login`, or set LIBRETTO_API_KEY in your env."
493
+ "Not authenticated. Run `libretto cloud auth signup`, `libretto cloud auth login`, or set LIBRETTO_API_KEY in your env."
486
494
  );
487
495
  return;
488
496
  }
489
497
  console.log(`Auth source: ${credential.source}`);
490
- console.log(`API URL: ${HOSTED_API_URL}`);
498
+ console.log(`API URL: ${resolveHostedApiUrl()}`);
491
499
  console.log(
492
500
  `LIBRETTO_API_KEY: ${envKey ? `set in env (${envKey.slice(0, 6)}\u2026)` : "not set in env"}`
493
501
  );
@@ -507,6 +515,7 @@ const authCommands = SimpleCLI.group({
507
515
  routes: {
508
516
  signup: signupCommand,
509
517
  login: loginCommand,
518
+ "forgot-password": forgotPasswordCommand,
510
519
  logout: logoutCommand,
511
520
  invite: inviteCommand,
512
521
  "accept-invite": acceptInviteCommand,
@@ -527,6 +536,7 @@ export {
527
536
  apiKeyListCommand,
528
537
  apiKeyRevokeCommand,
529
538
  authCommands,
539
+ forgotPasswordCommand,
530
540
  inviteCommand,
531
541
  loginCommand,
532
542
  logoutCommand,
@@ -1,4 +1,4 @@
1
- import { SimpleCLI } from "../framework/simple-cli.js";
1
+ import { SimpleCLI } from "affordance";
2
2
  import {
3
3
  NOT_AUTHENTICATED_MESSAGE,
4
4
  orpcCall,
@@ -20,8 +20,7 @@ function formatLimit(limit) {
20
20
  return limit === null ? "\u221E" : String(limit);
21
21
  }
22
22
  const billingPortalCommand = SimpleCLI.command({
23
- description: "Open the libretto plans page (current plan + switch options)",
24
- experimental: true
23
+ description: "Open the libretto plans page (current plan + switch options)"
25
24
  }).handle(async () => {
26
25
  const { apiUrl, credential } = await requireAuth();
27
26
  const { url } = await orpcCall({
@@ -40,8 +39,7 @@ const billingPortalCommand = SimpleCLI.command({
40
39
  );
41
40
  });
42
41
  const billingStatusCommand = SimpleCLI.command({
43
- description: "Print the current plan, status, and browser-hour usage",
44
- experimental: true
42
+ description: "Print the current plan, status, and browser-hour usage"
45
43
  }).handle(async () => {
46
44
  const { apiUrl, credential } = await requireAuth();
47
45
  const sub = await orpcCall({
@@ -19,7 +19,7 @@ import {
19
19
  validateSessionName
20
20
  } from "../core/session.js";
21
21
  import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
22
- import { SimpleCLI } from "../framework/simple-cli.js";
22
+ import { SimpleCLI } from "affordance";
23
23
  import {
24
24
  sessionOption,
25
25
  withAutoSession,
@@ -51,8 +51,8 @@ function resolveRequestedSessionMode(readOnly, writeAccess) {
51
51
  }
52
52
  const openInput = SimpleCLI.input({
53
53
  positionals: [
54
- SimpleCLI.positional("url", z.string().optional(), {
55
- help: "URL to open"
54
+ SimpleCLI.positional("url", z.string().default("about:blank"), {
55
+ help: "URL to open (defaults to about:blank)"
56
56
  })
57
57
  ],
58
58
  named: {
@@ -80,9 +80,6 @@ const openInput = SimpleCLI.input({
80
80
  })
81
81
  }
82
82
  }).refine(
83
- (input) => Boolean(input.url),
84
- `Usage: ${librettoCommand("open <url> [--headless] [--read-only|--write-access] [--auth-profile <domain>] [--viewport WxH] [--session <name>]")}`
85
- ).refine(
86
83
  (input) => !(input.headed && input.headless),
87
84
  "Cannot pass both --headed and --headless."
88
85
  ).refine(
@@ -1,31 +1,51 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { z } from "zod";
3
- import { HOSTED_API_URL } from "../core/auth-fetch.js";
3
+ import {
4
+ orpcCall,
5
+ resolveApiUrl
6
+ } from "../core/auth-fetch.js";
4
7
  import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
5
- import { SimpleCLI } from "../framework/simple-cli.js";
8
+ import { readAuthState } from "../core/auth-storage.js";
9
+ import { SimpleCLI } from "affordance";
6
10
  function generateDeploymentName() {
7
11
  return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
8
12
  }
9
- function getConfig() {
10
- const apiKey = process.env.LIBRETTO_API_KEY;
13
+ function deployApiKeyRequiredMessage(hasStoredSession) {
14
+ if (hasStoredSession) {
15
+ return [
16
+ "LIBRETTO_API_KEY is required to deploy to Libretto Cloud.",
17
+ "You are logged in locally, but deploy endpoints require API-key auth.",
18
+ " \u2022 Generate a key: run `libretto cloud auth api-key issue --label <label>`.",
19
+ " \u2022 Add it to your project .env file: `LIBRETTO_API_KEY=<issued-key>`."
20
+ ].join("\n");
21
+ }
22
+ return [
23
+ "LIBRETTO_API_KEY is required to deploy to Libretto Cloud.",
24
+ "No local cloud session was found.",
25
+ " \u2022 New account: run `libretto cloud auth signup`, then verify your email.",
26
+ " \u2022 Existing account: run `libretto cloud auth login`.",
27
+ " \u2022 Generate a key: run `libretto cloud auth api-key issue --label <label>`.",
28
+ " \u2022 Add it to your project .env file: `LIBRETTO_API_KEY=<issued-key>`."
29
+ ].join("\n");
30
+ }
31
+ async function requireDeployApiKey() {
32
+ const apiKey = process.env.LIBRETTO_API_KEY?.trim();
11
33
  if (!apiKey) {
12
- throw new Error(
13
- "LIBRETTO_API_KEY environment variable is required."
14
- );
34
+ throw new Error(deployApiKeyRequiredMessage(await hasStoredCloudSession()));
15
35
  }
16
- return { apiUrl: HOSTED_API_URL, apiKey };
36
+ return {
37
+ apiUrl: resolveApiUrl(null),
38
+ credential: { source: "env-api-key", apiKey }
39
+ };
17
40
  }
18
- async function postJson(apiUrl, apiKey, path, input = {}) {
19
- return fetch(`${apiUrl}${path}`, {
20
- method: "POST",
21
- headers: {
22
- "x-api-key": apiKey,
23
- "Content-Type": "application/json"
24
- },
25
- body: JSON.stringify({ json: input })
26
- });
41
+ async function hasStoredCloudSession() {
42
+ try {
43
+ return Boolean((await readAuthState())?.session);
44
+ } catch {
45
+ return false;
46
+ }
27
47
  }
28
- async function pollDeployment(apiUrl, apiKey, deploymentId, pollIntervalMs, maxWaitMs) {
48
+ async function pollDeployment(apiUrl, credential, deploymentId, pollIntervalMs, maxWaitMs) {
29
49
  const start = Date.now();
30
50
  const workflowWaitMs = 6e4;
31
51
  let status = "building";
@@ -37,18 +57,14 @@ async function pollDeployment(apiUrl, apiKey, deploymentId, pollIntervalMs, maxW
37
57
  if (status === "ready" && workflows?.length) break;
38
58
  if (status === "ready" && readyAt && Date.now() - readyAt > workflowWaitMs) break;
39
59
  await new Promise((r) => setTimeout(r, pollIntervalMs));
40
- const res = await postJson(apiUrl, apiKey, "/v1/deployments/sync", {
41
- id: deploymentId
60
+ deployment = await orpcCall({
61
+ apiUrl,
62
+ path: "/v1/deployments/sync",
63
+ input: { id: deploymentId },
64
+ credential
42
65
  });
43
- const body = await res.json();
44
- if (res.status !== 200) {
45
- throw new Error(
46
- `Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`
47
- );
48
- }
49
- status = body.json.status;
50
- workflows = body.json.workflows;
51
- deployment = body.json;
66
+ status = deployment.status;
67
+ workflows = deployment.workflows;
52
68
  if (status === "ready" && readyAt === null) readyAt = Date.now();
53
69
  process.stdout.write(".");
54
70
  }
@@ -88,10 +104,9 @@ const deployInput = SimpleCLI.input({
88
104
  }
89
105
  });
90
106
  const deployCommand = SimpleCLI.command({
91
- description: "Deploy workflows to the hosted platform",
92
- experimental: true
107
+ description: "Deploy workflows to the hosted platform"
93
108
  }).input(deployInput).handle(async ({ input }) => {
94
- const { apiUrl, apiKey } = getConfig();
109
+ const { apiUrl, credential } = await requireDeployApiKey();
95
110
  const deploymentName = generateDeploymentName();
96
111
  console.log("Bundling hosted deployment artifact...");
97
112
  const { entryPoint, source } = await buildHostedDeployTarball({
@@ -106,26 +121,20 @@ const deployCommand = SimpleCLI.command({
106
121
  };
107
122
  if (input.description) createPayload.description = input.description;
108
123
  console.log("Uploading deployment...");
109
- const res = await postJson(
124
+ const body = await orpcCall({
110
125
  apiUrl,
111
- apiKey,
112
- "/v1/deployments/create",
113
- createPayload
114
- );
115
- const body = await res.json();
116
- if (res.status !== 200) {
117
- throw new Error(
118
- `Failed to create deployment (${res.status}): ${JSON.stringify(body)}`
119
- );
120
- }
121
- const { deployment_id, status } = body.json;
126
+ path: "/v1/deployments/create",
127
+ input: createPayload,
128
+ credential
129
+ });
130
+ const { deployment_id, status } = body;
122
131
  console.log(`Deployment created: ${deployment_id}`);
123
132
  console.log(`Status: ${status}`);
124
133
  if (status === "building") {
125
134
  process.stdout.write("Waiting for build");
126
135
  const deployment = await pollDeployment(
127
136
  apiUrl,
128
- apiKey,
137
+ credential,
129
138
  deployment_id,
130
139
  1e4,
131
140
  5 * 60 * 1e3
@@ -27,7 +27,10 @@ import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
27
27
  import { readLibrettoConfig } from "../core/config.js";
28
28
  import { librettoCommand } from "../../shared/package-manager.js";
29
29
  import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
30
- import { resolveProviderName } from "../core/providers/index.js";
30
+ import {
31
+ getProviderStartupTimeoutMs,
32
+ resolveProviderName
33
+ } from "../core/providers/index.js";
31
34
  import { getAbsoluteIntegrationPath } from "../core/workflow-runtime.js";
32
35
  import {
33
36
  compileExecFunction,
@@ -42,7 +45,7 @@ import {
42
45
  readNetworkLog,
43
46
  wrapPageForActionLogging
44
47
  } from "../core/telemetry.js";
45
- import { SimpleCLI } from "../framework/simple-cli.js";
48
+ import { SimpleCLI } from "affordance";
46
49
  import {
47
50
  pageOption,
48
51
  sessionOption,
@@ -459,7 +462,7 @@ async function runIntegrationFromFile(args, logger) {
459
462
  },
460
463
  logger,
461
464
  logPath: runLogPath,
462
- startupTimeoutMs: 6e4,
465
+ startupTimeoutMs: getProviderStartupTimeoutMs(args.providerName),
463
466
  handlers
464
467
  });
465
468
  writeSessionState(
@@ -598,7 +601,7 @@ const readonlyExecCommand = SimpleCLI.command({
598
601
  mode: "readonly-exec"
599
602
  });
600
603
  });
601
- const runUsage = `Usage: ${librettoCommand("run <integrationFile> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--read-only|--write-access] [--no-visualize] [--stay-open-on-success] [--viewport WxH]")}`;
604
+ const runUsage = `Usage: ${librettoCommand("run <integrationFile> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--read-only|--write-access] [--no-visualize] [--stay-open-on-success] [--viewport WxH] [--provider <provider>]")}`;
602
605
  const runInput = SimpleCLI.input({
603
606
  positionals: [
604
607
  SimpleCLI.positional("integrationFile", z.string().optional(), {
@@ -6,7 +6,7 @@ import {
6
6
  resolveExperiments,
7
7
  setExperimentEnabled
8
8
  } from "../core/experiments.js";
9
- import { SimpleCLI } from "../framework/simple-cli.js";
9
+ import { SimpleCLI } from "affordance";
10
10
  const experimentNames = Object.keys(EXPERIMENTS);
11
11
  const experimentsUsage = [
12
12
  "Usage:",
@@ -8,7 +8,7 @@ import {
8
8
  REPO_ROOT
9
9
  } from "../core/context.js";
10
10
  import { librettoCommand } from "../../shared/package-manager.js";
11
- import { SimpleCLI } from "../framework/simple-cli.js";
11
+ import { SimpleCLI } from "affordance";
12
12
  function installBrowsers() {
13
13
  console.log("Installing Playwright Chromium...");
14
14
  const result = spawnSync("npx", ["playwright", "install", "chromium"], {
@@ -8,7 +8,7 @@ import {
8
8
  } from "../core/session.js";
9
9
  import {
10
10
  SimpleCLI
11
- } from "../framework/simple-cli.js";
11
+ } from "affordance";
12
12
  function sessionOption(help = "Session name") {
13
13
  return SimpleCLI.option(z.string().optional(), { help });
14
14
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { readSessionState } from "../core/session.js";
3
- import { SimpleCLI } from "../framework/simple-cli.js";
3
+ import { SimpleCLI } from "affordance";
4
4
  import {
5
5
  pageOption,
6
6
  sessionOption,
@@ -1,5 +1,5 @@
1
1
  import { listRunningSessions } from "../core/session.js";
2
- import { SimpleCLI } from "../framework/simple-cli.js";
2
+ import { SimpleCLI } from "affordance";
3
3
  function printOpenSessions(sessions) {
4
4
  console.log("\nOpen sessions:");
5
5
  if (sessions.length === 0) {
@@ -1,9 +1,13 @@
1
1
  import { readAuthState, writeAuthState } from "./auth-storage.js";
2
- const HOSTED_API_URL = "https://api.libretto.sh";
2
+ const DEFAULT_HOSTED_API_URL = "https://api.libretto.sh";
3
+ function resolveHostedApiUrl() {
4
+ return process.env.LIBRETTO_API_URL?.trim() || DEFAULT_HOSTED_API_URL;
5
+ }
3
6
  const NOT_AUTHENTICATED_MESSAGE = [
4
7
  "Not authenticated.",
5
- " \u2022 Cookie expired or never set: run `libretto experimental auth login` to refresh it.",
6
- " \u2022 Or set LIBRETTO_API_KEY in your .env (issue one with `libretto experimental auth api-key issue --label <label>` after logging in)."
8
+ " \u2022 New account: run `libretto cloud auth signup`.",
9
+ " \u2022 Existing account: run `libretto cloud auth login`.",
10
+ " \u2022 Automation: set LIBRETTO_API_KEY in your env (issue one with `libretto cloud auth api-key issue --label <label>` after signing in)."
7
11
  ].join("\n");
8
12
  function pickCredential(state) {
9
13
  const envKey = process.env.LIBRETTO_API_KEY?.trim();
@@ -14,7 +18,7 @@ function pickCredential(state) {
14
18
  return { source: "none" };
15
19
  }
16
20
  function resolveApiUrl(_state) {
17
- return HOSTED_API_URL;
21
+ return resolveHostedApiUrl();
18
22
  }
19
23
  async function authFetch(options) {
20
24
  const headers = {
@@ -184,12 +188,13 @@ async function ensureAuthState(apiUrl) {
184
188
  }
185
189
  export {
186
190
  ApiCallError,
187
- HOSTED_API_URL,
191
+ DEFAULT_HOSTED_API_URL,
188
192
  NOT_AUTHENTICATED_MESSAGE,
189
193
  authFetch,
190
194
  betterAuthCall,
191
195
  ensureAuthState,
192
196
  orpcCall,
193
197
  pickCredential,
194
- resolveApiUrl
198
+ resolveApiUrl,
199
+ resolveHostedApiUrl
195
200
  };
@@ -5,10 +5,14 @@ import { existsSync, readFileSync, unlinkSync } from "node:fs";
5
5
  import { mkdir, writeFile } from "node:fs/promises";
6
6
  import { dirname, join } from "node:path";
7
7
  import { createServer } from "node:net";
8
+ import { isWindowsNamedPipePath } from "../../shared/ipc/socket-transport.js";
8
9
  import { getSessionProviderClosePath, PROFILES_DIR } from "./context.js";
9
10
  import { readLibrettoConfig } from "./config.js";
10
11
  import { librettoCommand } from "../../shared/package-manager.js";
11
- import { getCloudProviderApi } from "./providers/index.js";
12
+ import {
13
+ getCloudProviderApi,
14
+ getProviderStartupTimeoutMs
15
+ } from "./providers/index.js";
12
16
  import {
13
17
  assertSessionAvailableForStart,
14
18
  clearSessionState,
@@ -64,14 +68,14 @@ function normalizeUrl(url) {
64
68
  if (!parsedUrl) {
65
69
  return new URL(`https://${url}`);
66
70
  }
67
- if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:" || parsedUrl.protocol === "file:") {
71
+ if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:" || parsedUrl.protocol === "file:" || parsedUrl.href === "about:blank") {
68
72
  return parsedUrl;
69
73
  }
70
74
  if (isLikelyHostWithPort(parsedUrl, url)) {
71
75
  return new URL(`https://${url}`);
72
76
  }
73
77
  throw new Error(
74
- `Unsupported URL protocol: ${parsedUrl.protocol}. Use http://, https://, or file://.`
78
+ `Unsupported URL protocol: ${parsedUrl.protocol}. Use http://, https://, file://, or about:blank.`
75
79
  );
76
80
  }
77
81
  function normalizeDomain(url) {
@@ -398,8 +402,8 @@ async function runOpenWithProvider(rawUrl, providerName, session, logger, access
398
402
  },
399
403
  logger,
400
404
  logPath: runLogPath,
401
- // Remote CDP connection + navigation; must cover both.
402
- startupTimeoutMs: 6e4
405
+ // Remote provider creation can wait for cloud capacity before CDP exists.
406
+ startupTimeoutMs: getProviderStartupTimeoutMs(providerName)
403
407
  });
404
408
  client.destroy();
405
409
  if (!providerSession) {
@@ -727,6 +731,7 @@ function resolveClosableSessions(logger) {
727
731
  }
728
732
  function unlinkDaemonSocket(socketPath, logger, session) {
729
733
  if (!socketPath) return;
734
+ if (isWindowsNamedPipePath(socketPath)) return;
730
735
  try {
731
736
  unlinkSync(socketPath);
732
737
  } catch (err) {