libretto 0.6.8 → 0.6.10
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/dist/cli/cli.js +2 -0
- package/dist/cli/commands/auth.js +535 -0
- package/dist/cli/commands/billing.js +74 -0
- package/dist/cli/commands/browser.js +8 -3
- package/dist/cli/commands/deploy.js +2 -7
- package/dist/cli/commands/execution.js +112 -137
- package/dist/cli/commands/snapshot.js +38 -126
- package/dist/cli/core/ai-model.js +0 -3
- package/dist/cli/core/auth-fetch.js +195 -0
- package/dist/cli/core/auth-storage.js +52 -0
- package/dist/cli/core/browser.js +151 -206
- package/dist/cli/core/daemon/config.js +6 -0
- package/dist/cli/core/daemon/daemon.js +298 -0
- package/dist/cli/core/daemon/exec.js +86 -0
- package/dist/cli/core/daemon/index.js +16 -0
- package/dist/cli/core/daemon/ipc.js +171 -0
- package/dist/cli/core/daemon/pages.js +15 -0
- package/dist/cli/core/daemon/snapshot.js +86 -0
- package/dist/cli/core/daemon/spawn.js +90 -0
- package/dist/cli/core/exec-compiler.js +111 -0
- package/dist/cli/core/prompt.js +72 -0
- package/dist/cli/core/providers/browserbase.js +1 -0
- package/dist/cli/core/providers/kernel.js +1 -0
- package/dist/cli/core/providers/libretto-cloud.js +6 -7
- package/dist/cli/core/readonly-exec.js +1 -1
- package/dist/cli/router.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +0 -5
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +2 -1
- package/docs/browser-automation-approaches.md +435 -0
- package/docs/releasing.md +117 -0
- package/package.json +4 -3
- package/skills/libretto/SKILL.md +14 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +2 -0
- package/src/cli/commands/auth.ts +787 -0
- package/src/cli/commands/billing.ts +133 -0
- package/src/cli/commands/browser.ts +8 -2
- package/src/cli/commands/deploy.ts +2 -7
- package/src/cli/commands/execution.ts +139 -187
- package/src/cli/commands/snapshot.ts +46 -143
- package/src/cli/core/ai-model.ts +4 -5
- package/src/cli/core/auth-fetch.ts +283 -0
- package/src/cli/core/auth-storage.ts +102 -0
- package/src/cli/core/browser.ts +182 -245
- package/src/cli/core/daemon/config.ts +46 -0
- package/src/cli/core/daemon/daemon.ts +429 -0
- package/src/cli/core/daemon/exec.ts +128 -0
- package/src/cli/core/daemon/index.ts +24 -0
- package/src/cli/core/daemon/ipc.ts +294 -0
- package/src/cli/core/daemon/pages.ts +21 -0
- package/src/cli/core/daemon/snapshot.ts +114 -0
- package/src/cli/core/daemon/spawn.ts +171 -0
- package/src/cli/core/exec-compiler.ts +169 -0
- package/src/cli/core/prompt.ts +94 -0
- package/src/cli/core/providers/browserbase.ts +1 -0
- package/src/cli/core/providers/kernel.ts +1 -0
- package/src/cli/core/providers/libretto-cloud.ts +13 -7
- package/src/cli/core/providers/types.ts +12 -1
- package/src/cli/core/readonly-exec.ts +2 -1
- package/src/cli/router.ts +4 -0
- package/src/cli/workers/run-integration-runtime.ts +0 -6
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/browser-daemon.js +0 -122
- package/src/cli/core/browser-daemon.ts +0 -198
package/dist/cli/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import { resolveAiSetupStatus } from "./core/ai-model.js";
|
|
|
2
2
|
import { ensureLibrettoSetup } from "./core/context.js";
|
|
3
3
|
import { createCLIApp } from "./router.js";
|
|
4
4
|
import { warnIfInstalledSkillOutOfDate } from "./core/skill-version.js";
|
|
5
|
+
import { loadEnv } from "../shared/env/load-env.js";
|
|
5
6
|
function renderUsage(app) {
|
|
6
7
|
return `${app.renderHelp()}
|
|
7
8
|
|
|
@@ -43,6 +44,7 @@ function isRootHelpRequest(rawArgs) {
|
|
|
43
44
|
async function runLibrettoCLI() {
|
|
44
45
|
const rawArgs = process.argv.slice(2);
|
|
45
46
|
let exitCode = 0;
|
|
47
|
+
loadEnv();
|
|
46
48
|
ensureLibrettoSetup();
|
|
47
49
|
const app = createCLIApp();
|
|
48
50
|
try {
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
3
|
+
import {
|
|
4
|
+
ApiCallError,
|
|
5
|
+
betterAuthCall,
|
|
6
|
+
HOSTED_API_URL,
|
|
7
|
+
NOT_AUTHENTICATED_MESSAGE,
|
|
8
|
+
orpcCall,
|
|
9
|
+
pickCredential,
|
|
10
|
+
resolveApiUrl
|
|
11
|
+
} from "../core/auth-fetch.js";
|
|
12
|
+
import {
|
|
13
|
+
authStatePath,
|
|
14
|
+
clearAuthState,
|
|
15
|
+
readAuthState,
|
|
16
|
+
setCookieToCookieHeader,
|
|
17
|
+
writeAuthState
|
|
18
|
+
} from "../core/auth-storage.js";
|
|
19
|
+
import { prompt, promptPassword, slugify } from "../core/prompt.js";
|
|
20
|
+
function isSlugTakenData(data) {
|
|
21
|
+
return !!data && typeof data === "object" && data.reason === "slug_taken";
|
|
22
|
+
}
|
|
23
|
+
async function getCurrentSession(apiUrl, cookie) {
|
|
24
|
+
try {
|
|
25
|
+
const { data } = await betterAuthCall({
|
|
26
|
+
apiUrl,
|
|
27
|
+
path: "/api/auth/get-session",
|
|
28
|
+
method: "GET",
|
|
29
|
+
credential: cookie ? { source: "cookie", cookie } : void 0
|
|
30
|
+
});
|
|
31
|
+
return data && typeof data === "object" && "user" in data ? data : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function pollForVerification(apiUrl, pollIntervalMs = 4e3, maxWaitMs = 10 * 60 * 1e3) {
|
|
37
|
+
const start = Date.now();
|
|
38
|
+
while (Date.now() - start < maxWaitMs) {
|
|
39
|
+
const session = await getCurrentSession(apiUrl);
|
|
40
|
+
if (session?.user.emailVerified) return true;
|
|
41
|
+
process.stdout.write(".");
|
|
42
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
async function persistSignupSession(apiUrl, result) {
|
|
48
|
+
const cookie = setCookieToCookieHeader(result.setCookie);
|
|
49
|
+
if (!cookie) {
|
|
50
|
+
throw new Error("Sign-up did not return a session cookie. Check the server.");
|
|
51
|
+
}
|
|
52
|
+
const next = {
|
|
53
|
+
apiUrl,
|
|
54
|
+
session: {
|
|
55
|
+
cookie,
|
|
56
|
+
userId: result.userId,
|
|
57
|
+
email: result.email,
|
|
58
|
+
expiresAt: null
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
await writeAuthState(next);
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
async function resolveActiveOrgId(apiUrl, credential) {
|
|
65
|
+
const { data: orgs } = await betterAuthCall({
|
|
66
|
+
apiUrl,
|
|
67
|
+
path: "/api/auth/organization/list",
|
|
68
|
+
method: "GET",
|
|
69
|
+
credential
|
|
70
|
+
});
|
|
71
|
+
if (!Array.isArray(orgs) || orgs.length === 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"No organization on this account \u2014 sign up or accept an invite first."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return orgs[0].id;
|
|
77
|
+
}
|
|
78
|
+
async function issueApiKey(apiUrl, name, credential) {
|
|
79
|
+
const { data } = await betterAuthCall({
|
|
80
|
+
apiUrl,
|
|
81
|
+
path: "/api/auth/api-key/create",
|
|
82
|
+
input: { name },
|
|
83
|
+
credential
|
|
84
|
+
});
|
|
85
|
+
if (!data?.key) {
|
|
86
|
+
throw new Error("API-key creation returned no raw key.");
|
|
87
|
+
}
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
const signupCommand = SimpleCLI.command({
|
|
91
|
+
description: "Create a new hosted-platform account and organization",
|
|
92
|
+
experimental: true
|
|
93
|
+
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
|
|
94
|
+
const apiUrl = HOSTED_API_URL;
|
|
95
|
+
console.log("Sign up for libretto cloud");
|
|
96
|
+
console.log();
|
|
97
|
+
console.log("Heads up: a libretto user can only belong to one organization.");
|
|
98
|
+
console.log(
|
|
99
|
+
"If your team already has a libretto org, ask a teammate for an invite instead \u2014 switching orgs later isn't supported."
|
|
100
|
+
);
|
|
101
|
+
console.log("Type 'q' at the name prompt to quit if that applies to you.");
|
|
102
|
+
console.log();
|
|
103
|
+
const name = await prompt("Your name:");
|
|
104
|
+
if (name.toLowerCase() === "q" || name.length === 0) {
|
|
105
|
+
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."
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const email = await prompt("Your email:");
|
|
111
|
+
const password = await promptPassword("Choose a password (8+ chars):");
|
|
112
|
+
const orgName = await prompt("Organization name:");
|
|
113
|
+
const defaultSlug = slugify(orgName);
|
|
114
|
+
let orgSlug = (await prompt("Organization slug:", { defaultValue: defaultSlug })).toLowerCase();
|
|
115
|
+
const debugNotificationEmail = await prompt(
|
|
116
|
+
"Alert email (for hosted workflow failures):",
|
|
117
|
+
{ defaultValue: email }
|
|
118
|
+
);
|
|
119
|
+
console.log();
|
|
120
|
+
console.log("Creating account...");
|
|
121
|
+
let result;
|
|
122
|
+
while (true) {
|
|
123
|
+
try {
|
|
124
|
+
result = await orpcCall({
|
|
125
|
+
apiUrl,
|
|
126
|
+
path: "/v1/auth/signupAndCreateOrg",
|
|
127
|
+
input: {
|
|
128
|
+
name,
|
|
129
|
+
email,
|
|
130
|
+
password,
|
|
131
|
+
organizationName: orgName,
|
|
132
|
+
organizationSlug: orgSlug,
|
|
133
|
+
debugNotificationEmail
|
|
134
|
+
},
|
|
135
|
+
unauthenticated: true
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
if (e instanceof ApiCallError && e.code === "CONFLICT" && isSlugTakenData(e.data)) {
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(e.message);
|
|
142
|
+
orgSlug = (await prompt("Organization slug:")).toLowerCase();
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
await persistSignupSession(apiUrl, result);
|
|
149
|
+
console.log(`Account created. Verification email sent to ${result.email}.`);
|
|
150
|
+
console.log("Click the link in the email to verify, then return here.");
|
|
151
|
+
console.log("Waiting for verification");
|
|
152
|
+
const verified = await pollForVerification(apiUrl);
|
|
153
|
+
if (!verified) {
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(
|
|
156
|
+
"Timed out waiting for email verification. Click the link in the email when you're ready \u2014 your CLI session is already saved, no need to re-run signup."
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log();
|
|
161
|
+
console.log("Email verified. You're logged in.");
|
|
162
|
+
console.log(`Session saved to ${authStatePath()}`);
|
|
163
|
+
console.log();
|
|
164
|
+
console.log("To generate an API key, run:");
|
|
165
|
+
console.log(" libretto experimental auth api-key issue --label <label>");
|
|
166
|
+
console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
|
|
167
|
+
});
|
|
168
|
+
const loginCommand = SimpleCLI.command({
|
|
169
|
+
description: "Sign in to an existing hosted-platform account",
|
|
170
|
+
experimental: true
|
|
171
|
+
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
|
|
172
|
+
const apiUrl = HOSTED_API_URL;
|
|
173
|
+
const email = await prompt("Email:");
|
|
174
|
+
const password = await promptPassword("Password:");
|
|
175
|
+
const { data, setCookie } = await betterAuthCall({
|
|
176
|
+
apiUrl,
|
|
177
|
+
path: "/api/auth/sign-in/email",
|
|
178
|
+
input: { email, password },
|
|
179
|
+
unauthenticated: true
|
|
180
|
+
});
|
|
181
|
+
const cookie = setCookieToCookieHeader(setCookie);
|
|
182
|
+
if (!cookie) {
|
|
183
|
+
throw new Error("Login response did not include a session cookie.");
|
|
184
|
+
}
|
|
185
|
+
const session = await getCurrentSession(apiUrl, cookie);
|
|
186
|
+
const next = {
|
|
187
|
+
apiUrl,
|
|
188
|
+
session: {
|
|
189
|
+
cookie,
|
|
190
|
+
userId: data.user.id,
|
|
191
|
+
email: data.user.email,
|
|
192
|
+
expiresAt: session?.session.expiresAt ?? null
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
await writeAuthState(next);
|
|
196
|
+
console.log(`Logged in as ${data.user.email}.`);
|
|
197
|
+
if (!data.user.emailVerified) {
|
|
198
|
+
console.log(
|
|
199
|
+
"Heads up: your email isn't verified yet. Re-sending the verification link to your inbox \u2014 click it to finish setup."
|
|
200
|
+
);
|
|
201
|
+
try {
|
|
202
|
+
await betterAuthCall({
|
|
203
|
+
apiUrl,
|
|
204
|
+
path: "/api/auth/send-verification-email",
|
|
205
|
+
input: {
|
|
206
|
+
email: data.user.email,
|
|
207
|
+
callbackURL: `${apiUrl}/auth/verified`
|
|
208
|
+
},
|
|
209
|
+
unauthenticated: true
|
|
210
|
+
});
|
|
211
|
+
console.log(`Verification email sent to ${data.user.email}.`);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
214
|
+
console.log(
|
|
215
|
+
`Couldn't resend the verification email (${message}). Try again, or hit /api/auth/send-verification-email directly.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
const logoutCommand = SimpleCLI.command({
|
|
221
|
+
description: "Clear local libretto credentials",
|
|
222
|
+
experimental: true
|
|
223
|
+
}).handle(async () => {
|
|
224
|
+
const state = await readAuthState();
|
|
225
|
+
if (state?.session?.cookie) {
|
|
226
|
+
try {
|
|
227
|
+
await betterAuthCall({
|
|
228
|
+
apiUrl: state.apiUrl,
|
|
229
|
+
path: "/api/auth/sign-out",
|
|
230
|
+
credential: { source: "cookie", cookie: state.session.cookie }
|
|
231
|
+
});
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
await clearAuthState();
|
|
236
|
+
console.log("Logged out.");
|
|
237
|
+
});
|
|
238
|
+
const inviteCommand = SimpleCLI.command({
|
|
239
|
+
description: "Invite a teammate to your active organization",
|
|
240
|
+
experimental: true
|
|
241
|
+
}).input(
|
|
242
|
+
SimpleCLI.input({
|
|
243
|
+
positionals: [
|
|
244
|
+
SimpleCLI.positional("email", z.string().email(), {
|
|
245
|
+
help: "Email address of the person to invite."
|
|
246
|
+
})
|
|
247
|
+
],
|
|
248
|
+
named: {
|
|
249
|
+
role: SimpleCLI.option(
|
|
250
|
+
z.enum(["member", "admin", "owner"]).default("member"),
|
|
251
|
+
{ help: "Role to assign (default: member)." }
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
).handle(async ({ input }) => {
|
|
256
|
+
const state = await readAuthState();
|
|
257
|
+
const apiUrl = resolveApiUrl(state);
|
|
258
|
+
const credential = pickCredential(state);
|
|
259
|
+
if (credential.source === "none") {
|
|
260
|
+
throw new Error(NOT_AUTHENTICATED_MESSAGE);
|
|
261
|
+
}
|
|
262
|
+
const organizationId = await resolveActiveOrgId(apiUrl, credential);
|
|
263
|
+
const body = {
|
|
264
|
+
email: input.email,
|
|
265
|
+
role: input.role,
|
|
266
|
+
organizationId
|
|
267
|
+
};
|
|
268
|
+
const { data } = await betterAuthCall({
|
|
269
|
+
apiUrl,
|
|
270
|
+
path: "/api/auth/organization/invite-member",
|
|
271
|
+
input: body,
|
|
272
|
+
credential
|
|
273
|
+
});
|
|
274
|
+
const { data: orgs } = await betterAuthCall({
|
|
275
|
+
apiUrl,
|
|
276
|
+
path: "/api/auth/organization/list",
|
|
277
|
+
method: "GET",
|
|
278
|
+
credential
|
|
279
|
+
});
|
|
280
|
+
const org = orgs?.find((o) => o.id === data.organizationId);
|
|
281
|
+
const orgName = org?.name ?? "<your-org-name>";
|
|
282
|
+
const orgSlug = org?.slug ?? "<your-org-slug>";
|
|
283
|
+
console.log(`Invitation sent to ${data.email}.`);
|
|
284
|
+
console.log(`Invitation id: ${data.id}`);
|
|
285
|
+
console.log(`Organization: ${orgName} (${orgSlug})`);
|
|
286
|
+
console.log(`Expires at: ${data.expiresAt}`);
|
|
287
|
+
console.log();
|
|
288
|
+
console.log("Tell them to run:");
|
|
289
|
+
console.log(
|
|
290
|
+
` libretto experimental auth accept-invite ${orgSlug} ${data.id}`
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
const acceptInviteCommand = SimpleCLI.command({
|
|
294
|
+
description: "Accept an organization invitation",
|
|
295
|
+
experimental: true
|
|
296
|
+
}).input(
|
|
297
|
+
SimpleCLI.input({
|
|
298
|
+
positionals: [
|
|
299
|
+
SimpleCLI.positional(
|
|
300
|
+
"tenantSlug",
|
|
301
|
+
z.string().min(2).max(60).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, {
|
|
302
|
+
message: "Slug must be lowercase letters, numbers, and hyphens (no leading/trailing hyphen)."
|
|
303
|
+
}),
|
|
304
|
+
{
|
|
305
|
+
help: "Slug of the organization you're joining. Must match the org slug in the invitation email \u2014 acts as a confirmation step."
|
|
306
|
+
}
|
|
307
|
+
),
|
|
308
|
+
SimpleCLI.positional("invitationId", z.string().min(1), {
|
|
309
|
+
help: "Invitation id from the invite email."
|
|
310
|
+
})
|
|
311
|
+
],
|
|
312
|
+
named: {}
|
|
313
|
+
})
|
|
314
|
+
).handle(async ({ input }) => {
|
|
315
|
+
const stored = await readAuthState();
|
|
316
|
+
const apiUrl = HOSTED_API_URL;
|
|
317
|
+
const credential = pickCredential(stored);
|
|
318
|
+
const expectedTenantSlug = input.tenantSlug;
|
|
319
|
+
if (credential.source !== "none") {
|
|
320
|
+
const { data: existingOrgs } = await betterAuthCall({
|
|
321
|
+
apiUrl,
|
|
322
|
+
path: "/api/auth/organization/list",
|
|
323
|
+
method: "GET",
|
|
324
|
+
credential
|
|
325
|
+
});
|
|
326
|
+
if (Array.isArray(existingOrgs) && existingOrgs.length > 0) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
[
|
|
329
|
+
"You're already a member of an organization.",
|
|
330
|
+
"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)."
|
|
332
|
+
].join("\n")
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const { data: invitation } = await betterAuthCall({
|
|
336
|
+
apiUrl,
|
|
337
|
+
path: `/api/auth/organization/get-invitation?id=${encodeURIComponent(input.invitationId)}`,
|
|
338
|
+
method: "GET",
|
|
339
|
+
credential
|
|
340
|
+
});
|
|
341
|
+
if (!invitation?.organizationSlug || invitation.organizationSlug !== expectedTenantSlug) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"Organization slug doesn't match this invitation. Double-check the slug shown in the invitation email."
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
await betterAuthCall({
|
|
347
|
+
apiUrl,
|
|
348
|
+
path: "/api/auth/organization/accept-invitation",
|
|
349
|
+
input: { invitationId: input.invitationId },
|
|
350
|
+
credential
|
|
351
|
+
});
|
|
352
|
+
console.log(`Invitation accepted. You're now a member of ${invitation.organizationName}.`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
console.log("Accepting invite \u2014 let's create your account.");
|
|
356
|
+
const name = await prompt("Your name:");
|
|
357
|
+
const password = await promptPassword("Choose a password (8+ chars):");
|
|
358
|
+
const result = await orpcCall({
|
|
359
|
+
apiUrl,
|
|
360
|
+
path: "/v1/auth/acceptInviteAndSignup",
|
|
361
|
+
input: {
|
|
362
|
+
invitationId: input.invitationId,
|
|
363
|
+
tenantSlug: input.tenantSlug,
|
|
364
|
+
name,
|
|
365
|
+
password
|
|
366
|
+
},
|
|
367
|
+
unauthenticated: true
|
|
368
|
+
});
|
|
369
|
+
await persistSignupSession(apiUrl, result);
|
|
370
|
+
console.log(`Account created. Verification email sent to ${result.email}.`);
|
|
371
|
+
console.log("Click the link in the email and return here.");
|
|
372
|
+
console.log("Waiting for verification");
|
|
373
|
+
const verified = await pollForVerification(apiUrl);
|
|
374
|
+
if (!verified) {
|
|
375
|
+
console.log();
|
|
376
|
+
console.log(
|
|
377
|
+
"Timed out waiting for email verification. Click the link in the email when ready \u2014 your CLI session is already saved."
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
console.log();
|
|
382
|
+
console.log("Email verified. You're logged in and a member of the organization.");
|
|
383
|
+
console.log("To generate an API key, run:");
|
|
384
|
+
console.log(" libretto experimental auth api-key issue --label <label>");
|
|
385
|
+
console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
|
|
386
|
+
});
|
|
387
|
+
const apiKeyIssueCommand = SimpleCLI.command({
|
|
388
|
+
description: "Issue a new API key for the active organization",
|
|
389
|
+
experimental: true
|
|
390
|
+
}).input(
|
|
391
|
+
SimpleCLI.input({
|
|
392
|
+
positionals: [],
|
|
393
|
+
named: {
|
|
394
|
+
label: SimpleCLI.option(z.string(), {
|
|
395
|
+
help: "Label to identify this key (e.g. 'laptop-dev', 'github-actions')."
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
).handle(async ({ input }) => {
|
|
400
|
+
const stored = await readAuthState();
|
|
401
|
+
const apiUrl = resolveApiUrl(stored);
|
|
402
|
+
const credential = pickCredential(stored);
|
|
403
|
+
if (credential.source === "none") {
|
|
404
|
+
throw new Error(NOT_AUTHENTICATED_MESSAGE);
|
|
405
|
+
}
|
|
406
|
+
const key = await issueApiKey(apiUrl, input.label, credential);
|
|
407
|
+
console.log(`API key issued (id: ${key.id}, label: ${key.name ?? input.label}).`);
|
|
408
|
+
console.log(`Key (shown once \u2014 keep it safe):`);
|
|
409
|
+
console.log(` ${key.key}`);
|
|
410
|
+
console.log();
|
|
411
|
+
console.log("Add the following to your project's .env file:");
|
|
412
|
+
console.log(` LIBRETTO_API_KEY=${key.key}`);
|
|
413
|
+
console.log();
|
|
414
|
+
console.log(
|
|
415
|
+
"The key is not stored on disk by the CLI \u2014 losing it means revoking + re-issuing."
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
const apiKeyListCommand = SimpleCLI.command({
|
|
419
|
+
description: "List API keys for the active organization",
|
|
420
|
+
experimental: true
|
|
421
|
+
}).handle(async () => {
|
|
422
|
+
const stored = await readAuthState();
|
|
423
|
+
const apiUrl = resolveApiUrl(stored);
|
|
424
|
+
const credential = pickCredential(stored);
|
|
425
|
+
if (credential.source === "none") {
|
|
426
|
+
throw new Error(NOT_AUTHENTICATED_MESSAGE);
|
|
427
|
+
}
|
|
428
|
+
const { data } = await betterAuthCall({
|
|
429
|
+
apiUrl,
|
|
430
|
+
path: "/api/auth/api-key/list",
|
|
431
|
+
method: "GET",
|
|
432
|
+
credential
|
|
433
|
+
});
|
|
434
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
435
|
+
console.log("No API keys.");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
for (const key of data) {
|
|
439
|
+
const enabled = key.enabled === false ? " [disabled]" : "";
|
|
440
|
+
const last = key.lastRequest ? ` last-used ${key.lastRequest}` : "";
|
|
441
|
+
console.log(
|
|
442
|
+
`${key.id} ${key.name ?? "(unnamed)"} ${key.start ?? key.prefix ?? ""}\u2026 created ${key.createdAt}${enabled}${last}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
const apiKeyRevokeCommand = SimpleCLI.command({
|
|
447
|
+
description: "Revoke an API key by id",
|
|
448
|
+
experimental: true
|
|
449
|
+
}).input(
|
|
450
|
+
SimpleCLI.input({
|
|
451
|
+
positionals: [
|
|
452
|
+
SimpleCLI.positional("id", z.string().min(1), {
|
|
453
|
+
help: "API key id (from `auth api-key list`)."
|
|
454
|
+
})
|
|
455
|
+
],
|
|
456
|
+
named: {}
|
|
457
|
+
})
|
|
458
|
+
).handle(async ({ input }) => {
|
|
459
|
+
const stored = await readAuthState();
|
|
460
|
+
const apiUrl = resolveApiUrl(stored);
|
|
461
|
+
const credential = pickCredential(stored);
|
|
462
|
+
if (credential.source === "none") {
|
|
463
|
+
throw new Error(NOT_AUTHENTICATED_MESSAGE);
|
|
464
|
+
}
|
|
465
|
+
await betterAuthCall({
|
|
466
|
+
apiUrl,
|
|
467
|
+
path: "/api/auth/api-key/delete",
|
|
468
|
+
input: { keyId: input.id },
|
|
469
|
+
credential
|
|
470
|
+
});
|
|
471
|
+
console.log(`API key ${input.id} revoked.`);
|
|
472
|
+
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>`."
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
const whoamiCommand = SimpleCLI.command({
|
|
477
|
+
description: "Print the active session and credential source",
|
|
478
|
+
experimental: true
|
|
479
|
+
}).handle(async () => {
|
|
480
|
+
const stored = await readAuthState();
|
|
481
|
+
const credential = pickCredential(stored);
|
|
482
|
+
const envKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
483
|
+
if (credential.source === "none") {
|
|
484
|
+
console.log(
|
|
485
|
+
"Not authenticated. Run `libretto experimental auth signup`, `login`, or set LIBRETTO_API_KEY in your env."
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
console.log(`Auth source: ${credential.source}`);
|
|
490
|
+
console.log(`API URL: ${HOSTED_API_URL}`);
|
|
491
|
+
console.log(
|
|
492
|
+
`LIBRETTO_API_KEY: ${envKey ? `set in env (${envKey.slice(0, 6)}\u2026)` : "not set in env"}`
|
|
493
|
+
);
|
|
494
|
+
if (stored?.session) {
|
|
495
|
+
console.log(`Session email: ${stored.session.email}`);
|
|
496
|
+
console.log(`Session user id: ${stored.session.userId}`);
|
|
497
|
+
if (stored.session.expiresAt) {
|
|
498
|
+
console.log(`Session expires: ${stored.session.expiresAt}`);
|
|
499
|
+
}
|
|
500
|
+
console.log(`Session file: ${authStatePath()}`);
|
|
501
|
+
} else {
|
|
502
|
+
console.log("Session file: (none on disk)");
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
const authCommands = SimpleCLI.group({
|
|
506
|
+
description: "Hosted-platform auth commands",
|
|
507
|
+
routes: {
|
|
508
|
+
signup: signupCommand,
|
|
509
|
+
login: loginCommand,
|
|
510
|
+
logout: logoutCommand,
|
|
511
|
+
invite: inviteCommand,
|
|
512
|
+
"accept-invite": acceptInviteCommand,
|
|
513
|
+
whoami: whoamiCommand,
|
|
514
|
+
"api-key": SimpleCLI.group({
|
|
515
|
+
description: "Manage API keys",
|
|
516
|
+
routes: {
|
|
517
|
+
issue: apiKeyIssueCommand,
|
|
518
|
+
list: apiKeyListCommand,
|
|
519
|
+
revoke: apiKeyRevokeCommand
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
export {
|
|
525
|
+
acceptInviteCommand,
|
|
526
|
+
apiKeyIssueCommand,
|
|
527
|
+
apiKeyListCommand,
|
|
528
|
+
apiKeyRevokeCommand,
|
|
529
|
+
authCommands,
|
|
530
|
+
inviteCommand,
|
|
531
|
+
loginCommand,
|
|
532
|
+
logoutCommand,
|
|
533
|
+
signupCommand,
|
|
534
|
+
whoamiCommand
|
|
535
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
2
|
+
import {
|
|
3
|
+
NOT_AUTHENTICATED_MESSAGE,
|
|
4
|
+
orpcCall,
|
|
5
|
+
pickCredential,
|
|
6
|
+
resolveApiUrl
|
|
7
|
+
} from "../core/auth-fetch.js";
|
|
8
|
+
import { readAuthState } from "../core/auth-storage.js";
|
|
9
|
+
const CONTACT_URL = "https://libretto.sh";
|
|
10
|
+
async function requireAuth() {
|
|
11
|
+
const stored = await readAuthState();
|
|
12
|
+
const apiUrl = resolveApiUrl(stored);
|
|
13
|
+
const credential = pickCredential(stored);
|
|
14
|
+
if (credential.source === "none") {
|
|
15
|
+
throw new Error(NOT_AUTHENTICATED_MESSAGE);
|
|
16
|
+
}
|
|
17
|
+
return { apiUrl, credential };
|
|
18
|
+
}
|
|
19
|
+
function formatLimit(limit) {
|
|
20
|
+
return limit === null ? "\u221E" : String(limit);
|
|
21
|
+
}
|
|
22
|
+
const billingPortalCommand = SimpleCLI.command({
|
|
23
|
+
description: "Open the libretto plans page (current plan + switch options)",
|
|
24
|
+
experimental: true
|
|
25
|
+
}).handle(async () => {
|
|
26
|
+
const { apiUrl, credential } = await requireAuth();
|
|
27
|
+
const { url } = await orpcCall({
|
|
28
|
+
apiUrl,
|
|
29
|
+
path: "/v1/billing/openPlansPage",
|
|
30
|
+
credential
|
|
31
|
+
});
|
|
32
|
+
console.log("Open this URL in your browser to choose or change your plan:");
|
|
33
|
+
console.log(` ${url}`);
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(
|
|
36
|
+
"(Shows all tiers with features, your current plan, and a Manage payment / invoices link.)"
|
|
37
|
+
);
|
|
38
|
+
console.log(
|
|
39
|
+
`For a BAA or Enterprise pricing, contact us at ${CONTACT_URL}.`
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
const billingStatusCommand = SimpleCLI.command({
|
|
43
|
+
description: "Print the current plan, status, and browser-hour usage",
|
|
44
|
+
experimental: true
|
|
45
|
+
}).handle(async () => {
|
|
46
|
+
const { apiUrl, credential } = await requireAuth();
|
|
47
|
+
const sub = await orpcCall({
|
|
48
|
+
apiUrl,
|
|
49
|
+
path: "/v1/billing/subscription",
|
|
50
|
+
credential
|
|
51
|
+
});
|
|
52
|
+
const used = sub.browserHoursUsedThisPeriod.toFixed(2);
|
|
53
|
+
const limit = formatLimit(sub.browserHoursLimit);
|
|
54
|
+
console.log(`Plan: ${sub.plan} (${sub.status})`);
|
|
55
|
+
console.log(`Usage: ${used} / ${limit} browser hours this period`);
|
|
56
|
+
if (sub.currentPeriodEnd) {
|
|
57
|
+
console.log(`Period: ends ${sub.currentPeriodEnd.slice(0, 10)}`);
|
|
58
|
+
}
|
|
59
|
+
if (sub.cancelAtPeriodEnd) {
|
|
60
|
+
console.log("Note: cancellation scheduled at period end.");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const billingCommands = SimpleCLI.group({
|
|
64
|
+
description: "Hosted-platform subscription + usage commands",
|
|
65
|
+
routes: {
|
|
66
|
+
portal: billingPortalCommand,
|
|
67
|
+
status: billingStatusCommand
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
export {
|
|
71
|
+
billingCommands,
|
|
72
|
+
billingPortalCommand,
|
|
73
|
+
billingStatusCommand
|
|
74
|
+
};
|
|
@@ -68,6 +68,10 @@ const openInput = SimpleCLI.input({
|
|
|
68
68
|
name: "write-access",
|
|
69
69
|
help: "Create the session in write-access mode (overrides config default)"
|
|
70
70
|
}),
|
|
71
|
+
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
72
|
+
name: "auth-profile",
|
|
73
|
+
help: "Override the domain used for auth profile lookup (e.g. use login.example.com's profile when opening app.example.com)"
|
|
74
|
+
}),
|
|
71
75
|
viewport: SimpleCLI.option(z.string().optional(), {
|
|
72
76
|
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
|
|
73
77
|
}),
|
|
@@ -78,7 +82,7 @@ const openInput = SimpleCLI.input({
|
|
|
78
82
|
}
|
|
79
83
|
}).refine(
|
|
80
84
|
(input) => Boolean(input.url),
|
|
81
|
-
`Usage: libretto open <url> [--headless] [--read-only|--write-access] [--viewport WxH] [--session <name>]`
|
|
85
|
+
`Usage: libretto open <url> [--headless] [--read-only|--write-access] [--auth-profile <domain>] [--viewport WxH] [--session <name>]`
|
|
82
86
|
).refine(
|
|
83
87
|
(input) => !(input.headed && input.headless),
|
|
84
88
|
"Cannot pass both --headed and --headless."
|
|
@@ -87,7 +91,7 @@ const openInput = SimpleCLI.input({
|
|
|
87
91
|
"Cannot pass both --read-only and --write-access."
|
|
88
92
|
);
|
|
89
93
|
const openCommand = SimpleCLI.command({
|
|
90
|
-
description: "Launch browser and open URL (headed by default)"
|
|
94
|
+
description: "Launch browser and open URL (headed by default). Automatically loads a saved auth profile for the URL's domain if one exists."
|
|
91
95
|
}).input(openInput).use(withAutoSession()).handle(async ({ input, ctx }) => {
|
|
92
96
|
warnIfInstalledSkillOutOfDate();
|
|
93
97
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
@@ -100,7 +104,8 @@ const openCommand = SimpleCLI.command({
|
|
|
100
104
|
accessMode: resolveRequestedSessionMode(
|
|
101
105
|
input.readOnly,
|
|
102
106
|
input.writeAccess
|
|
103
|
-
)
|
|
107
|
+
),
|
|
108
|
+
authProfileDomain: input.authProfile
|
|
104
109
|
});
|
|
105
110
|
} else {
|
|
106
111
|
const provider = getCloudProviderApi(providerName);
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { HOSTED_API_URL } from "../core/auth-fetch.js";
|
|
3
4
|
import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
|
|
4
5
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
5
6
|
function generateDeploymentName() {
|
|
6
7
|
return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
7
8
|
}
|
|
8
9
|
function getConfig() {
|
|
9
|
-
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
10
10
|
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
11
|
-
if (!apiUrl) {
|
|
12
|
-
throw new Error(
|
|
13
|
-
"LIBRETTO_API_URL environment variable is required."
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
11
|
if (!apiKey) {
|
|
17
12
|
throw new Error(
|
|
18
13
|
"LIBRETTO_API_KEY environment variable is required."
|
|
19
14
|
);
|
|
20
15
|
}
|
|
21
|
-
return { apiUrl:
|
|
16
|
+
return { apiUrl: HOSTED_API_URL, apiKey };
|
|
22
17
|
}
|
|
23
18
|
async function postJson(apiUrl, apiKey, path, input = {}) {
|
|
24
19
|
return fetch(`${apiUrl}${path}`, {
|