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.
- package/README.md +1 -1
- package/README.template.md +1 -1
- package/dist/cli/commands/auth.js +119 -268
- package/dist/cli/commands/browser.js +1 -0
- package/dist/cli/commands/cloud-credentials.js +5 -17
- package/dist/cli/commands/cloud-jobs.js +125 -0
- package/dist/cli/commands/cloud-schedules.js +128 -0
- package/dist/cli/commands/cloud-settings.js +75 -0
- package/dist/cli/commands/cloud-sharing.js +13 -27
- package/dist/cli/commands/deploy.js +7 -16
- package/dist/cli/commands/execution.js +3 -2
- package/dist/cli/commands/profiles.js +8 -21
- package/dist/cli/commands/shared.js +17 -0
- package/dist/cli/core/browser.js +2 -1
- package/dist/cli/core/daemon/daemon.js +2 -1
- package/dist/cli/core/daemon/ipc.js +23 -16
- package/dist/cli/core/deploy-artifact.js +41 -16
- package/dist/cli/core/providers/kernel.js +3 -2
- package/dist/cli/core/providers/libretto-cloud.js +2 -1
- package/dist/cli/core/telemetry.js +14 -2
- package/dist/cli/router.js +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/shared/workflow/workflow.d.ts +18 -0
- package/dist/shared/workflow/workflow.js +9 -0
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +17 -2
- package/skills/libretto/references/website-authentication.md +18 -2
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/auth.ts +169 -382
- package/src/cli/commands/browser.ts +1 -0
- package/src/cli/commands/cloud-credentials.ts +6 -18
- package/src/cli/commands/cloud-jobs.ts +157 -0
- package/src/cli/commands/cloud-schedules.ts +164 -0
- package/src/cli/commands/cloud-settings.ts +101 -0
- package/src/cli/commands/cloud-sharing.ts +20 -28
- package/src/cli/commands/deploy.ts +8 -19
- package/src/cli/commands/execution.ts +2 -1
- package/src/cli/commands/profiles.ts +10 -22
- package/src/cli/commands/shared.ts +29 -0
- package/src/cli/core/browser.ts +2 -0
- package/src/cli/core/daemon/config.ts +1 -0
- package/src/cli/core/daemon/daemon.ts +1 -0
- package/src/cli/core/daemon/ipc.ts +27 -18
- package/src/cli/core/deploy-artifact.ts +63 -14
- package/src/cli/core/providers/kernel.ts +3 -2
- package/src/cli/core/providers/libretto-cloud.ts +1 -0
- package/src/cli/core/providers/types.ts +1 -0
- package/src/cli/core/telemetry.ts +15 -1
- package/src/cli/router.ts +6 -0
- package/src/index.ts +1 -0
- package/src/shared/workflow/workflow.ts +22 -0
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ All Libretto state lives in a `.libretto/` directory at your project root. See t
|
|
|
95
95
|
|
|
96
96
|
## Telemetry
|
|
97
97
|
|
|
98
|
-
Libretto records
|
|
98
|
+
Libretto records CLI telemetry to help understand CLI usage and help us prioritize improvements. Each resolved command can send only an install id, timestamp, command event name such as `libretto run`, error boolean, package version, build channel (`node_modules`, `source`, or `unknown`), and, when signed into Libretto Cloud, the configured cloud user id. Libretto does not send command arguments, URLs, project paths, session cookies, API keys, error messages or details, or emails.
|
|
99
99
|
|
|
100
100
|
The install id is stored in the telemetry file at `~/.libretto/telemetry.json`. The implementation lives in [`src/cli/core/telemetry.ts`](src/cli/core/telemetry.ts).
|
|
101
101
|
|
package/README.template.md
CHANGED
|
@@ -93,7 +93,7 @@ All Libretto state lives in a `.libretto/` directory at your project root. See t
|
|
|
93
93
|
|
|
94
94
|
## Telemetry
|
|
95
95
|
|
|
96
|
-
Libretto records
|
|
96
|
+
Libretto records CLI telemetry to help understand CLI usage and help us prioritize improvements. Each resolved command can send only an install id, timestamp, command event name such as `libretto run`, error boolean, package version, build channel (`node_modules`, `source`, or `unknown`), and, when signed into Libretto Cloud, the configured cloud user id. Libretto does not send command arguments, URLs, project paths, session cookies, API keys, error messages or details, or emails.
|
|
97
97
|
|
|
98
98
|
The install id is stored in the telemetry file at `~/.libretto/telemetry.json`. The implementation lives in [`{{LIBRETTO_PATH_PREFIX}}src/cli/core/telemetry.ts`]({{LIBRETTO_PATH_PREFIX}}src/cli/core/telemetry.ts).
|
|
99
99
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { SimpleCLI } from "affordance";
|
|
3
4
|
import {
|
|
4
|
-
ApiCallError,
|
|
5
5
|
betterAuthCall,
|
|
6
6
|
NOT_AUTHENTICATED_MESSAGE,
|
|
7
7
|
orpcCall,
|
|
@@ -13,12 +13,29 @@ import {
|
|
|
13
13
|
authStatePath,
|
|
14
14
|
clearAuthState,
|
|
15
15
|
readAuthState,
|
|
16
|
-
setCookieToCookieHeader,
|
|
17
16
|
writeAuthState
|
|
18
17
|
} from "../core/auth-storage.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
function resolveHostedWebsiteUrl() {
|
|
19
|
+
return process.env.LIBRETTO_WEBSITE_URL?.trim() || "https://libretto.sh";
|
|
20
|
+
}
|
|
21
|
+
function sleep(ms) {
|
|
22
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
function openBrowser(url) {
|
|
25
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
26
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
27
|
+
try {
|
|
28
|
+
const child = spawn(command, args, {
|
|
29
|
+
detached: true,
|
|
30
|
+
stdio: "ignore"
|
|
31
|
+
});
|
|
32
|
+
child.on("error", () => {
|
|
33
|
+
});
|
|
34
|
+
child.unref();
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
22
39
|
}
|
|
23
40
|
async function getCurrentSession(apiUrl, cookie) {
|
|
24
41
|
try {
|
|
@@ -33,33 +50,89 @@ async function getCurrentSession(apiUrl, cookie) {
|
|
|
33
50
|
return null;
|
|
34
51
|
}
|
|
35
52
|
}
|
|
36
|
-
async function
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
async function runBrowserAuthFlow(options) {
|
|
54
|
+
const login = await orpcCall({
|
|
55
|
+
apiUrl: options.apiUrl,
|
|
56
|
+
path: "/v1/auth/cliLoginCreate",
|
|
57
|
+
unauthenticated: true
|
|
58
|
+
});
|
|
59
|
+
const loginUrl = new URL("/signin", options.websiteUrl);
|
|
60
|
+
loginUrl.searchParams.set("cliLoginId", login.requestId);
|
|
61
|
+
loginUrl.searchParams.set("cliLoginSecret", login.secret);
|
|
62
|
+
if (options.mode === "signup") {
|
|
63
|
+
loginUrl.searchParams.set("mode", "signup");
|
|
43
64
|
}
|
|
65
|
+
console.log(
|
|
66
|
+
options.mode === "signup" ? "Sign up for Libretto Cloud in your browser:" : "Sign in to Libretto Cloud in your browser:"
|
|
67
|
+
);
|
|
68
|
+
console.log(` ${loginUrl.toString()}`);
|
|
44
69
|
console.log();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
70
|
+
if (openBrowser(loginUrl.toString())) {
|
|
71
|
+
console.log("Opened the page in your default browser.");
|
|
72
|
+
console.log("If it didn't open, copy the link above into your browser.");
|
|
73
|
+
console.log();
|
|
74
|
+
} else {
|
|
75
|
+
console.log("Copy the link above into your browser.");
|
|
76
|
+
console.log();
|
|
51
77
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
78
|
+
console.log(
|
|
79
|
+
options.mode === "signup" ? "Waiting for browser sign-up" : "Waiting for browser sign-in"
|
|
80
|
+
);
|
|
81
|
+
const expiresAt = new Date(login.expiresAt).getTime();
|
|
82
|
+
let verificationHintShown = false;
|
|
83
|
+
while (Date.now() < expiresAt) {
|
|
84
|
+
const result = await orpcCall({
|
|
85
|
+
apiUrl: options.apiUrl,
|
|
86
|
+
path: "/v1/auth/cliLoginPoll",
|
|
87
|
+
input: {
|
|
88
|
+
requestId: login.requestId,
|
|
89
|
+
secret: login.secret
|
|
90
|
+
},
|
|
91
|
+
unauthenticated: true
|
|
92
|
+
});
|
|
93
|
+
if (result.status === "expired") {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Auth request expired. Run \`libretto cloud auth ${options.mode}\` again.`
|
|
96
|
+
);
|
|
59
97
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
98
|
+
if (result.status === "approved") {
|
|
99
|
+
const session = await getCurrentSession(options.apiUrl, result.cookieHeader);
|
|
100
|
+
if (!session?.user?.id) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Browser auth succeeded, but the returned session could not be verified."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const next = {
|
|
106
|
+
apiUrl: options.apiUrl,
|
|
107
|
+
session: {
|
|
108
|
+
cookie: result.cookieHeader,
|
|
109
|
+
userId: result.userId,
|
|
110
|
+
email: result.email,
|
|
111
|
+
expiresAt: session.session.expiresAt ?? result.sessionExpiresAt
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
await writeAuthState(next);
|
|
115
|
+
console.log();
|
|
116
|
+
console.log(`Logged in as ${result.email}.`);
|
|
117
|
+
if (!result.emailVerified) {
|
|
118
|
+
console.log(
|
|
119
|
+
"Heads up: your email isn't verified yet. Click the verification link in your inbox to finish setup."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (options.mode === "signup" && !verificationHintShown) {
|
|
125
|
+
console.log();
|
|
126
|
+
console.log("After signing up with email/password, verify your email to finish CLI auth.");
|
|
127
|
+
verificationHintShown = true;
|
|
128
|
+
}
|
|
129
|
+
process.stdout.write(".");
|
|
130
|
+
await sleep(2e3);
|
|
131
|
+
}
|
|
132
|
+
console.log();
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Auth request expired. Run \`libretto cloud auth ${options.mode}\` again.`
|
|
135
|
+
);
|
|
63
136
|
}
|
|
64
137
|
async function resolveActiveOrgId(apiUrl, credential) {
|
|
65
138
|
const { data: orgs } = await betterAuthCall({
|
|
@@ -88,149 +161,22 @@ async function issueApiKey(apiUrl, name, credential) {
|
|
|
88
161
|
return data;
|
|
89
162
|
}
|
|
90
163
|
const signupCommand = SimpleCLI.command({
|
|
91
|
-
description: "
|
|
164
|
+
description: "Open the hosted-platform sign-up page"
|
|
92
165
|
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.log(
|
|
98
|
-
"If your team already has a libretto org, ask a teammate for an invite instead \u2014 switching orgs later isn't supported."
|
|
99
|
-
);
|
|
100
|
-
console.log("Type 'q' at the name prompt to quit if that applies to you.");
|
|
101
|
-
console.log();
|
|
102
|
-
const name = await prompt("Your name:");
|
|
103
|
-
if (name.toLowerCase() === "q" || name.length === 0) {
|
|
104
|
-
console.log(
|
|
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."
|
|
106
|
-
);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const email = await prompt("Your email:");
|
|
110
|
-
const password = await promptPassword("Choose a password (8+ chars):");
|
|
111
|
-
const orgName = await prompt("Organization name:");
|
|
112
|
-
const defaultSlug = slugify(orgName);
|
|
113
|
-
let orgSlug = (await prompt("Organization slug:", { defaultValue: defaultSlug })).toLowerCase();
|
|
114
|
-
const debugNotificationEmail = await prompt(
|
|
115
|
-
"Alert email (for hosted workflow failures):",
|
|
116
|
-
{ defaultValue: email }
|
|
117
|
-
);
|
|
118
|
-
console.log();
|
|
119
|
-
console.log("Creating account...");
|
|
120
|
-
let result;
|
|
121
|
-
while (true) {
|
|
122
|
-
try {
|
|
123
|
-
result = await orpcCall({
|
|
124
|
-
apiUrl,
|
|
125
|
-
path: "/v1/auth/signupAndCreateOrg",
|
|
126
|
-
input: {
|
|
127
|
-
name,
|
|
128
|
-
email,
|
|
129
|
-
password,
|
|
130
|
-
organizationName: orgName,
|
|
131
|
-
organizationSlug: orgSlug,
|
|
132
|
-
debugNotificationEmail
|
|
133
|
-
},
|
|
134
|
-
unauthenticated: true
|
|
135
|
-
});
|
|
136
|
-
break;
|
|
137
|
-
} catch (e) {
|
|
138
|
-
if (e instanceof ApiCallError && e.code === "CONFLICT" && isSlugTakenData(e.data)) {
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(e.message);
|
|
141
|
-
orgSlug = (await prompt("Organization slug:")).toLowerCase();
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
throw e;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
await persistSignupSession(apiUrl, result);
|
|
148
|
-
console.log(`Account created. Verification email sent to ${result.email}.`);
|
|
149
|
-
console.log("Click the link in the email to verify, then return here.");
|
|
150
|
-
console.log("Waiting for verification");
|
|
151
|
-
const verified = await pollForVerification(apiUrl);
|
|
152
|
-
if (!verified) {
|
|
153
|
-
console.log();
|
|
154
|
-
console.log(
|
|
155
|
-
"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."
|
|
156
|
-
);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
console.log();
|
|
160
|
-
console.log("Email verified. You're logged in.");
|
|
161
|
-
console.log(`Session saved to ${authStatePath()}`);
|
|
162
|
-
console.log();
|
|
163
|
-
console.log("To generate an API key, run:");
|
|
164
|
-
console.log(" libretto cloud auth api-key issue --label <label>");
|
|
165
|
-
console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
|
|
166
|
-
});
|
|
167
|
-
const loginCommand = SimpleCLI.command({
|
|
168
|
-
description: "Sign in to an existing hosted-platform account"
|
|
169
|
-
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
|
|
170
|
-
const apiUrl = resolveHostedApiUrl();
|
|
171
|
-
const email = await prompt("Email:");
|
|
172
|
-
const password = await promptPassword("Password:");
|
|
173
|
-
const { data, setCookie } = await betterAuthCall({
|
|
174
|
-
apiUrl,
|
|
175
|
-
path: "/api/auth/sign-in/email",
|
|
176
|
-
input: { email, password },
|
|
177
|
-
unauthenticated: true
|
|
166
|
+
await runBrowserAuthFlow({
|
|
167
|
+
mode: "signup",
|
|
168
|
+
apiUrl: resolveHostedApiUrl(),
|
|
169
|
+
websiteUrl: resolveHostedWebsiteUrl()
|
|
178
170
|
});
|
|
179
|
-
const cookie = setCookieToCookieHeader(setCookie);
|
|
180
|
-
if (!cookie) {
|
|
181
|
-
throw new Error("Login response did not include a session cookie.");
|
|
182
|
-
}
|
|
183
|
-
const session = await getCurrentSession(apiUrl, cookie);
|
|
184
|
-
const next = {
|
|
185
|
-
apiUrl,
|
|
186
|
-
session: {
|
|
187
|
-
cookie,
|
|
188
|
-
userId: data.user.id,
|
|
189
|
-
email: data.user.email,
|
|
190
|
-
expiresAt: session?.session.expiresAt ?? null
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
await writeAuthState(next);
|
|
194
|
-
console.log(`Logged in as ${data.user.email}.`);
|
|
195
|
-
if (!data.user.emailVerified) {
|
|
196
|
-
console.log(
|
|
197
|
-
"Heads up: your email isn't verified yet. Re-sending the verification link to your inbox \u2014 click it to finish setup."
|
|
198
|
-
);
|
|
199
|
-
try {
|
|
200
|
-
await betterAuthCall({
|
|
201
|
-
apiUrl,
|
|
202
|
-
path: "/api/auth/send-verification-email",
|
|
203
|
-
input: {
|
|
204
|
-
email: data.user.email,
|
|
205
|
-
callbackURL: `${apiUrl}/auth/verified`
|
|
206
|
-
},
|
|
207
|
-
unauthenticated: true
|
|
208
|
-
});
|
|
209
|
-
console.log(`Verification email sent to ${data.user.email}.`);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
212
|
-
console.log(
|
|
213
|
-
`Couldn't resend the verification email (${message}). Try again, or hit /api/auth/send-verification-email directly.`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
171
|
});
|
|
218
|
-
const
|
|
219
|
-
description: "
|
|
172
|
+
const loginCommand = SimpleCLI.command({
|
|
173
|
+
description: "Open the hosted-platform sign-in page"
|
|
220
174
|
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
path: "/v1/auth/requestPasswordReset",
|
|
226
|
-
input: { email },
|
|
227
|
-
unauthenticated: true
|
|
175
|
+
await runBrowserAuthFlow({
|
|
176
|
+
mode: "login",
|
|
177
|
+
apiUrl: resolveHostedApiUrl(),
|
|
178
|
+
websiteUrl: resolveHostedWebsiteUrl()
|
|
228
179
|
});
|
|
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
180
|
});
|
|
235
181
|
const logoutCommand = SimpleCLI.command({
|
|
236
182
|
description: "Clear local libretto credentials"
|
|
@@ -260,7 +206,7 @@ const inviteCommand = SimpleCLI.command({
|
|
|
260
206
|
],
|
|
261
207
|
named: {
|
|
262
208
|
role: SimpleCLI.option(
|
|
263
|
-
z.enum(["member", "
|
|
209
|
+
z.enum(["member", "owner"]).default("member"),
|
|
264
210
|
{ help: "Role to assign (default: member)." }
|
|
265
211
|
)
|
|
266
212
|
}
|
|
@@ -293,108 +239,17 @@ const inviteCommand = SimpleCLI.command({
|
|
|
293
239
|
const org = orgs?.find((o) => o.id === data.organizationId);
|
|
294
240
|
const orgName = org?.name ?? "<your-org-name>";
|
|
295
241
|
const orgSlug = org?.slug ?? "<your-org-slug>";
|
|
242
|
+
const inviteUrl = new URL("/invite", resolveHostedWebsiteUrl());
|
|
243
|
+
inviteUrl.searchParams.set("tenantSlug", orgSlug);
|
|
244
|
+
inviteUrl.searchParams.set("invitationId", data.id);
|
|
245
|
+
inviteUrl.searchParams.set("accept", "1");
|
|
296
246
|
console.log(`Invitation sent to ${data.email}.`);
|
|
297
247
|
console.log(`Invitation id: ${data.id}`);
|
|
298
248
|
console.log(`Organization: ${orgName} (${orgSlug})`);
|
|
299
249
|
console.log(`Expires at: ${data.expiresAt}`);
|
|
300
250
|
console.log();
|
|
301
|
-
console.log("
|
|
302
|
-
console.log(
|
|
303
|
-
` libretto cloud auth accept-invite ${orgSlug} ${data.id}`
|
|
304
|
-
);
|
|
305
|
-
});
|
|
306
|
-
const acceptInviteCommand = SimpleCLI.command({
|
|
307
|
-
description: "Accept an organization invitation"
|
|
308
|
-
}).input(
|
|
309
|
-
SimpleCLI.input({
|
|
310
|
-
positionals: [
|
|
311
|
-
SimpleCLI.positional(
|
|
312
|
-
"tenantSlug",
|
|
313
|
-
z.string().min(2).max(60).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, {
|
|
314
|
-
message: "Slug must be lowercase letters, numbers, and hyphens (no leading/trailing hyphen)."
|
|
315
|
-
}),
|
|
316
|
-
{
|
|
317
|
-
help: "Slug of the organization you're joining. Must match the org slug in the invitation email \u2014 acts as a confirmation step."
|
|
318
|
-
}
|
|
319
|
-
),
|
|
320
|
-
SimpleCLI.positional("invitationId", z.string().min(1), {
|
|
321
|
-
help: "Invitation id from the invite email."
|
|
322
|
-
})
|
|
323
|
-
],
|
|
324
|
-
named: {}
|
|
325
|
-
})
|
|
326
|
-
).handle(async ({ input }) => {
|
|
327
|
-
const stored = await readAuthState();
|
|
328
|
-
const apiUrl = resolveHostedApiUrl();
|
|
329
|
-
const credential = pickCredential(stored);
|
|
330
|
-
const expectedTenantSlug = input.tenantSlug;
|
|
331
|
-
if (credential.source !== "none") {
|
|
332
|
-
const { data: existingOrgs } = await betterAuthCall({
|
|
333
|
-
apiUrl,
|
|
334
|
-
path: "/api/auth/organization/list",
|
|
335
|
-
method: "GET",
|
|
336
|
-
credential
|
|
337
|
-
});
|
|
338
|
-
if (Array.isArray(existingOrgs) && existingOrgs.length > 0) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
[
|
|
341
|
-
"You're already a member of an organization.",
|
|
342
|
-
"A libretto user can only belong to one organization at a time.",
|
|
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)."
|
|
344
|
-
].join("\n")
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
const { data: invitation } = await betterAuthCall({
|
|
348
|
-
apiUrl,
|
|
349
|
-
path: `/api/auth/organization/get-invitation?id=${encodeURIComponent(input.invitationId)}`,
|
|
350
|
-
method: "GET",
|
|
351
|
-
credential
|
|
352
|
-
});
|
|
353
|
-
if (!invitation?.organizationSlug || invitation.organizationSlug !== expectedTenantSlug) {
|
|
354
|
-
throw new Error(
|
|
355
|
-
"Organization slug doesn't match this invitation. Double-check the slug shown in the invitation email."
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
await betterAuthCall({
|
|
359
|
-
apiUrl,
|
|
360
|
-
path: "/api/auth/organization/accept-invitation",
|
|
361
|
-
input: { invitationId: input.invitationId },
|
|
362
|
-
credential
|
|
363
|
-
});
|
|
364
|
-
console.log(`Invitation accepted. You're now a member of ${invitation.organizationName}.`);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
console.log("Accepting invite \u2014 let's create your account.");
|
|
368
|
-
const name = await prompt("Your name:");
|
|
369
|
-
const password = await promptPassword("Choose a password (8+ chars):");
|
|
370
|
-
const result = await orpcCall({
|
|
371
|
-
apiUrl,
|
|
372
|
-
path: "/v1/auth/acceptInviteAndSignup",
|
|
373
|
-
input: {
|
|
374
|
-
invitationId: input.invitationId,
|
|
375
|
-
tenantSlug: input.tenantSlug,
|
|
376
|
-
name,
|
|
377
|
-
password
|
|
378
|
-
},
|
|
379
|
-
unauthenticated: true
|
|
380
|
-
});
|
|
381
|
-
await persistSignupSession(apiUrl, result);
|
|
382
|
-
console.log(`Account created. Verification email sent to ${result.email}.`);
|
|
383
|
-
console.log("Click the link in the email and return here.");
|
|
384
|
-
console.log("Waiting for verification");
|
|
385
|
-
const verified = await pollForVerification(apiUrl);
|
|
386
|
-
if (!verified) {
|
|
387
|
-
console.log();
|
|
388
|
-
console.log(
|
|
389
|
-
"Timed out waiting for email verification. Click the link in the email when ready \u2014 your CLI session is already saved."
|
|
390
|
-
);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
console.log();
|
|
394
|
-
console.log("Email verified. You're logged in and a member of the organization.");
|
|
395
|
-
console.log("To generate an API key, run:");
|
|
396
|
-
console.log(" libretto cloud auth api-key issue --label <label>");
|
|
397
|
-
console.log("Then add LIBRETTO_API_KEY=<key> to your project's .env file.");
|
|
251
|
+
console.log("Invite link:");
|
|
252
|
+
console.log(` ${inviteUrl.toString()}`);
|
|
398
253
|
});
|
|
399
254
|
const apiKeyIssueCommand = SimpleCLI.command({
|
|
400
255
|
description: "Issue a new API key for the active organization"
|
|
@@ -515,10 +370,8 @@ const authCommands = SimpleCLI.group({
|
|
|
515
370
|
routes: {
|
|
516
371
|
signup: signupCommand,
|
|
517
372
|
login: loginCommand,
|
|
518
|
-
"forgot-password": forgotPasswordCommand,
|
|
519
373
|
logout: logoutCommand,
|
|
520
374
|
invite: inviteCommand,
|
|
521
|
-
"accept-invite": acceptInviteCommand,
|
|
522
375
|
whoami: whoamiCommand,
|
|
523
376
|
"api-key": SimpleCLI.group({
|
|
524
377
|
description: "Manage API keys",
|
|
@@ -531,12 +384,10 @@ const authCommands = SimpleCLI.group({
|
|
|
531
384
|
}
|
|
532
385
|
});
|
|
533
386
|
export {
|
|
534
|
-
acceptInviteCommand,
|
|
535
387
|
apiKeyIssueCommand,
|
|
536
388
|
apiKeyListCommand,
|
|
537
389
|
apiKeyRevokeCommand,
|
|
538
390
|
authCommands,
|
|
539
|
-
forgotPasswordCommand,
|
|
540
391
|
inviteCommand,
|
|
541
392
|
loginCommand,
|
|
542
393
|
logoutCommand,
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import { SimpleCLI } from "affordance";
|
|
2
|
-
import { orpcCall
|
|
2
|
+
import { orpcCall } from "../core/auth-fetch.js";
|
|
3
|
+
import { withCloudApiKey } from "./shared.js";
|
|
3
4
|
const CLOUD_CREDENTIAL_ENV_PREFIX = "LIBRETTO_CLOUD_";
|
|
4
|
-
function requireApiKeyCredential() {
|
|
5
|
-
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
6
|
-
if (!apiKey) {
|
|
7
|
-
throw new Error(
|
|
8
|
-
"LIBRETTO_API_KEY is required to manage Libretto Cloud credentials. 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
|
-
}
|
|
16
5
|
function parseEnvCredentials(prefix) {
|
|
17
6
|
const credentials = {};
|
|
18
7
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -29,22 +18,21 @@ const pushCredentialCommand = SimpleCLI.command({
|
|
|
29
18
|
}).input(SimpleCLI.input({
|
|
30
19
|
positionals: [],
|
|
31
20
|
named: {}
|
|
32
|
-
})).handle(async () => {
|
|
21
|
+
})).use(withCloudApiKey("manage Libretto Cloud credentials")).handle(async ({ ctx }) => {
|
|
33
22
|
const credentials = parseEnvCredentials(CLOUD_CREDENTIAL_ENV_PREFIX);
|
|
34
23
|
if (credentials.length === 0) {
|
|
35
24
|
throw new Error(
|
|
36
25
|
`No non-empty env vars found with prefix ${CLOUD_CREDENTIAL_ENV_PREFIX}.`
|
|
37
26
|
);
|
|
38
27
|
}
|
|
39
|
-
const { apiUrl, credential } = requireApiKeyCredential();
|
|
40
28
|
let created = 0;
|
|
41
29
|
let updated = 0;
|
|
42
30
|
for (const item of credentials) {
|
|
43
31
|
const response = await orpcCall({
|
|
44
|
-
apiUrl,
|
|
32
|
+
apiUrl: ctx.apiUrl,
|
|
45
33
|
path: "/v1/credentials/upsert",
|
|
46
34
|
input: item,
|
|
47
|
-
credential
|
|
35
|
+
credential: ctx.credential
|
|
48
36
|
});
|
|
49
37
|
if (response.overwritten) {
|
|
50
38
|
updated += 1;
|
|
@@ -0,0 +1,125 @@
|
|
|
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 createJobUsage = "Usage: libretto cloud jobs create <workflow> [--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 createCloudJobInput = SimpleCLI.input({
|
|
33
|
+
positionals: [
|
|
34
|
+
SimpleCLI.positional("workflow", z.string().optional(), {
|
|
35
|
+
help: "Deployed workflow name to run"
|
|
36
|
+
})
|
|
37
|
+
],
|
|
38
|
+
named: {
|
|
39
|
+
params: SimpleCLI.option(z.string().optional(), {
|
|
40
|
+
help: "Inline JSON params object"
|
|
41
|
+
}),
|
|
42
|
+
paramsFile: SimpleCLI.option(z.string().optional(), {
|
|
43
|
+
name: "params-file",
|
|
44
|
+
help: "Path to a JSON params file"
|
|
45
|
+
}),
|
|
46
|
+
credentialId: SimpleCLI.option(z.string().optional(), {
|
|
47
|
+
name: "credential-id",
|
|
48
|
+
help: "Stored cloud credential id to pass to the workflow"
|
|
49
|
+
}),
|
|
50
|
+
timeoutSeconds: SimpleCLI.option(z.coerce.number().int().min(1).optional(), {
|
|
51
|
+
name: "timeout-seconds",
|
|
52
|
+
help: "Job timeout in seconds"
|
|
53
|
+
}),
|
|
54
|
+
headed: SimpleCLI.flag({ help: "Run browser in headed mode" }),
|
|
55
|
+
headless: SimpleCLI.flag({ help: "Run browser in headless mode" }),
|
|
56
|
+
callbackUrl: SimpleCLI.option(z.string().optional(), {
|
|
57
|
+
name: "callback-url",
|
|
58
|
+
help: "Per-job callback URL"
|
|
59
|
+
}),
|
|
60
|
+
callbackSecret: SimpleCLI.option(z.string().optional(), {
|
|
61
|
+
name: "callback-secret",
|
|
62
|
+
help: "Secret used to sign the per-job callback"
|
|
63
|
+
}),
|
|
64
|
+
skipCallbacks: SimpleCLI.flag({
|
|
65
|
+
name: "skip-callbacks",
|
|
66
|
+
help: "Skip stored webhook callbacks for this job"
|
|
67
|
+
}),
|
|
68
|
+
residentialProxy: SimpleCLI.option(z.string().optional(), {
|
|
69
|
+
name: "residential-proxy",
|
|
70
|
+
help: "Residential proxy config as a JSON object"
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}).refine((input) => Boolean(input.workflow), createJobUsage).refine(
|
|
74
|
+
(input) => !(input.params && input.paramsFile),
|
|
75
|
+
"Pass either --params or --params-file, not both."
|
|
76
|
+
).refine(
|
|
77
|
+
(input) => !(input.headed && input.headless),
|
|
78
|
+
"Cannot pass both --headed and --headless."
|
|
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 createCloudJobCommand = SimpleCLI.command({
|
|
84
|
+
description: "Create a Libretto Cloud job for a deployed workflow"
|
|
85
|
+
}).input(createCloudJobInput).use(withCloudApiKey("create Libretto Cloud jobs")).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
|
+
params
|
|
91
|
+
};
|
|
92
|
+
if (input.credentialId) payload.credential_id = input.credentialId;
|
|
93
|
+
if (input.timeoutSeconds !== void 0) {
|
|
94
|
+
payload.timeout_seconds = input.timeoutSeconds;
|
|
95
|
+
}
|
|
96
|
+
if (input.headed) payload.headless = false;
|
|
97
|
+
if (input.headless) payload.headless = true;
|
|
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/jobs/create",
|
|
107
|
+
input: payload,
|
|
108
|
+
credential: ctx.credential
|
|
109
|
+
});
|
|
110
|
+
console.log(`Job created: ${response.job_id}`);
|
|
111
|
+
console.log(`Status: ${response.status}`);
|
|
112
|
+
console.log(response.message);
|
|
113
|
+
return response.job_id;
|
|
114
|
+
});
|
|
115
|
+
const cloudJobCommands = SimpleCLI.group({
|
|
116
|
+
description: "Create and manage hosted jobs",
|
|
117
|
+
routes: {
|
|
118
|
+
create: createCloudJobCommand
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
export {
|
|
122
|
+
cloudJobCommands,
|
|
123
|
+
createCloudJobCommand,
|
|
124
|
+
createCloudJobInput
|
|
125
|
+
};
|